mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: what's new in Unleash (#7497)
https://linear.app/unleash/issue/2-2354/new-in-unleash-section-in-sidebar Add a "New in Unleash" section in the side bar and use it to announce signals and actions.  Inside signals page we're also including a feedback button to try to collect some insights.  --------- Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
		
							parent
							
								
									06971375cb
								
							
						
					
					
						commit
						5832fc7d81
					
				| @ -15,6 +15,7 @@ import { | ||||
| import { useInitialPathname } from './useInitialPathname'; | ||||
| import { useLastViewedProject } from 'hooks/useLastViewedProject'; | ||||
| import { useLastViewedFlags } from 'hooks/useLastViewedFlags'; | ||||
| import { NewInUnleash } from './NewInUnleash/NewInUnleash'; | ||||
| 
 | ||||
| export const MobileNavigationSidebar: FC<{ onClick: () => void }> = ({ | ||||
|     onClick, | ||||
| @ -23,6 +24,7 @@ export const MobileNavigationSidebar: FC<{ onClick: () => void }> = ({ | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <NewInUnleash onItemClick={onClick} /> | ||||
|             <PrimaryNavigationList mode='full' onClick={onClick} /> | ||||
|             <SecondaryNavigationList | ||||
|                 routes={routes.mainNavRoutes} | ||||
| @ -67,6 +69,7 @@ export const NavigationSidebar = () => { | ||||
| 
 | ||||
|     return ( | ||||
|         <StretchContainer> | ||||
|             <NewInUnleash mode={mode} onMiniModeClick={() => setMode('full')} /> | ||||
|             <PrimaryNavigationList | ||||
|                 mode={mode} | ||||
|                 onClick={setActiveItem} | ||||
|  | ||||
| @ -0,0 +1,161 @@ | ||||
| import type { ReactNode } from 'react'; | ||||
| import { useUiFlag } from 'hooks/useUiFlag'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import { useLocalStorageState } from 'hooks/useLocalStorageState'; | ||||
| import { | ||||
|     Badge, | ||||
|     Icon, | ||||
|     ListItem, | ||||
|     ListItemButton, | ||||
|     ListItemIcon, | ||||
|     Tooltip, | ||||
|     styled, | ||||
| } from '@mui/material'; | ||||
| import Signals from '@mui/icons-material/Sensors'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import type { NavigationMode } from 'component/layout/MainLayout/NavigationSidebar/NavigationMode'; | ||||
| import { NewInUnleashItem } from './NewInUnleashItem'; | ||||
| import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; | ||||
| 
 | ||||
| const StyledNewInUnleash = styled('div')(({ theme }) => ({ | ||||
|     borderRadius: theme.shape.borderRadiusMedium, | ||||
|     [theme.breakpoints.down('lg')]: { | ||||
|         margin: theme.spacing(2), | ||||
|         marginBottom: theme.spacing(1), | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledNewInUnleashHeader = styled('p')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     alignItems: 'center', | ||||
|     lineHeight: 1, | ||||
|     gap: theme.spacing(1), | ||||
|     '& > span': { | ||||
|         color: theme.palette.neutral.main, | ||||
|     }, | ||||
|     padding: theme.spacing(1, 2), | ||||
| })); | ||||
| 
 | ||||
| const StyledNewInUnleashList = styled('ul')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     flexDirection: 'column', | ||||
|     padding: theme.spacing(1), | ||||
|     listStyle: 'none', | ||||
|     margin: 0, | ||||
|     gap: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| const StyledMiniItemButton = styled(ListItemButton)(({ theme }) => ({ | ||||
|     borderRadius: theme.spacing(0.5), | ||||
|     borderLeft: `${theme.spacing(0.5)} solid transparent`, | ||||
|     '&.Mui-selected': { | ||||
|         borderLeft: `${theme.spacing(0.5)} solid ${theme.palette.primary.main}`, | ||||
|     }, | ||||
| })); | ||||
| 
 | ||||
| const StyledMiniItemIcon = styled(ListItemIcon)(({ theme }) => ({ | ||||
|     minWidth: theme.spacing(4), | ||||
|     margin: theme.spacing(0.25, 0), | ||||
| })); | ||||
| 
 | ||||
| const StyledSignalsIcon = styled(Signals)(({ theme }) => ({ | ||||
|     color: theme.palette.primary.main, | ||||
| })); | ||||
| 
 | ||||
| type NewItem = { | ||||
|     label: string; | ||||
|     icon: ReactNode; | ||||
|     link: string; | ||||
|     show: boolean; | ||||
| }; | ||||
| 
 | ||||
| interface INewInUnleashProps { | ||||
|     mode?: NavigationMode; | ||||
|     onItemClick?: () => void; | ||||
|     onMiniModeClick?: () => void; | ||||
| } | ||||
| 
 | ||||
| export const NewInUnleash = ({ | ||||
|     mode = 'full', | ||||
|     onItemClick, | ||||
|     onMiniModeClick, | ||||
| }: INewInUnleashProps) => { | ||||
|     const { trackEvent } = usePlausibleTracker(); | ||||
|     const navigate = useNavigate(); | ||||
|     const [seenItems, setSeenItems] = useLocalStorageState( | ||||
|         'new-in-unleash-seen:v1', | ||||
|         new Set(), | ||||
|     ); | ||||
|     const { isEnterprise } = useUiConfig(); | ||||
|     const signalsEnabled = useUiFlag('signals'); | ||||
| 
 | ||||
|     const items: NewItem[] = [ | ||||
|         { | ||||
|             label: 'Signals & Actions', | ||||
|             icon: <StyledSignalsIcon />, | ||||
|             link: '/integrations/signals', | ||||
|             show: isEnterprise() && signalsEnabled, | ||||
|         }, | ||||
|     ]; | ||||
| 
 | ||||
|     const visibleItems = items.filter( | ||||
|         (item) => item.show && !seenItems.has(item.label), | ||||
|     ); | ||||
| 
 | ||||
|     if (!visibleItems.length) return null; | ||||
| 
 | ||||
|     if (mode === 'mini' && onMiniModeClick) { | ||||
|         return ( | ||||
|             <ListItem disablePadding onClick={onMiniModeClick}> | ||||
|                 <StyledMiniItemButton dense> | ||||
|                     <Tooltip title='New in Unleash' placement='right'> | ||||
|                         <StyledMiniItemIcon> | ||||
|                             <Badge | ||||
|                                 badgeContent={visibleItems.length} | ||||
|                                 color='primary' | ||||
|                             > | ||||
|                                 <Icon>new_releases</Icon> | ||||
|                             </Badge> | ||||
|                         </StyledMiniItemIcon> | ||||
|                     </Tooltip> | ||||
|                 </StyledMiniItemButton> | ||||
|             </ListItem> | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <StyledNewInUnleash> | ||||
|             <StyledNewInUnleashHeader> | ||||
|                 <Icon>new_releases</Icon> | ||||
|                 New in Unleash | ||||
|             </StyledNewInUnleashHeader> | ||||
|             <StyledNewInUnleashList> | ||||
|                 {visibleItems.map(({ label, icon, link }) => ( | ||||
|                     <NewInUnleashItem | ||||
|                         key={label} | ||||
|                         icon={icon} | ||||
|                         onClick={() => { | ||||
|                             trackEvent('new-in-unleash-click', { | ||||
|                                 props: { | ||||
|                                     label, | ||||
|                                 }, | ||||
|                             }); | ||||
|                             navigate(link); | ||||
|                             onItemClick?.(); | ||||
|                         }} | ||||
|                         onDismiss={() => { | ||||
|                             trackEvent('new-in-unleash-dismiss', { | ||||
|                                 props: { | ||||
|                                     label, | ||||
|                                 }, | ||||
|                             }); | ||||
|                             setSeenItems(new Set([...seenItems, label])); | ||||
|                         }} | ||||
|                     > | ||||
|                         {label} | ||||
|                     </NewInUnleashItem> | ||||
|                 ))} | ||||
|             </StyledNewInUnleashList> | ||||
|         </StyledNewInUnleash> | ||||
|     ); | ||||
| }; | ||||
| @ -0,0 +1,66 @@ | ||||
| import type { ReactNode } from 'react'; | ||||
| import { | ||||
|     IconButton, | ||||
|     ListItem, | ||||
|     ListItemButton, | ||||
|     Tooltip, | ||||
|     styled, | ||||
| } from '@mui/material'; | ||||
| import Close from '@mui/icons-material/Close'; | ||||
| 
 | ||||
| const StyledItemButton = styled(ListItemButton)(({ theme }) => ({ | ||||
|     justifyContent: 'space-between', | ||||
|     outline: `1px solid ${theme.palette.divider}`, | ||||
|     borderRadius: theme.shape.borderRadiusMedium, | ||||
|     padding: theme.spacing(1), | ||||
| })); | ||||
| 
 | ||||
| const StyledItemButtonContent = styled('div')(({ theme }) => ({ | ||||
|     display: 'flex', | ||||
|     alignItems: 'center', | ||||
|     gap: theme.spacing(1), | ||||
|     fontSize: theme.fontSizes.smallBody, | ||||
| })); | ||||
| 
 | ||||
| const StyledItemButtonClose = styled(IconButton)(({ theme }) => ({ | ||||
|     padding: theme.spacing(0.25), | ||||
| })); | ||||
| 
 | ||||
| interface INewInUnleashItemProps { | ||||
|     icon: ReactNode; | ||||
|     onClick: () => void; | ||||
|     onDismiss: () => void; | ||||
|     children: ReactNode; | ||||
| } | ||||
| 
 | ||||
| export const NewInUnleashItem = ({ | ||||
|     icon, | ||||
|     onClick, | ||||
|     onDismiss, | ||||
|     children, | ||||
| }: INewInUnleashItemProps) => { | ||||
|     const onDismissClick = (e: React.MouseEvent) => { | ||||
|         e.stopPropagation(); | ||||
|         onDismiss(); | ||||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <ListItem disablePadding onClick={onClick}> | ||||
|             <StyledItemButton> | ||||
|                 <StyledItemButtonContent> | ||||
|                     {icon} | ||||
|                     {children} | ||||
|                 </StyledItemButtonContent> | ||||
|                 <Tooltip title='Dismiss' arrow> | ||||
|                     <StyledItemButtonClose | ||||
|                         aria-label='dismiss' | ||||
|                         onClick={onDismissClick} | ||||
|                         size='small' | ||||
|                     > | ||||
|                         <Close fontSize='inherit' /> | ||||
|                     </StyledItemButtonClose> | ||||
|                 </Tooltip> | ||||
|             </StyledItemButton> | ||||
|         </ListItem> | ||||
|     ); | ||||
| }; | ||||
| @ -3,7 +3,8 @@ import { TablePlaceholder, VirtualizedTable } from 'component/common/Table'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| import { Button, useMediaQuery } from '@mui/material'; | ||||
| import { Alert, Button, styled, useMediaQuery } from '@mui/material'; | ||||
| import ReviewsOutlined from '@mui/icons-material/ReviewsOutlined'; | ||||
| import { useFlexLayout, useSortBy, useTable } from 'react-table'; | ||||
| import { sortTypes } from 'utils/sortTypes'; | ||||
| import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; | ||||
| @ -25,10 +26,18 @@ import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; | ||||
| import { SignalEndpointsSignalsModal } from '../SignalEndpointsSignals/SignalEndpointsSignalsModal'; | ||||
| import { PageContent } from 'component/common/PageContent/PageContent'; | ||||
| import { PageHeader } from 'component/common/PageHeader/PageHeader'; | ||||
| import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard'; | ||||
| import { ADMIN } from '@server/types/permissions'; | ||||
| import PermissionButton from 'component/common/PermissionButton/PermissionButton'; | ||||
| import { useFeedback } from 'component/feedbackNew/useFeedback'; | ||||
| 
 | ||||
| export const SignalEndpointsTable = () => { | ||||
|     const { setToastData, setToastApiError } = useToast(); | ||||
|     const { uiConfig } = useUiConfig(); | ||||
|     const { openFeedback, hasSubmittedFeedback } = useFeedback( | ||||
|         'signals', | ||||
|         'automatic', | ||||
|     ); | ||||
| 
 | ||||
|     const { signalEndpoints, refetch } = useSignalEndpoints(); | ||||
|     const { toggleSignalEndpoint, removeSignalEndpoint } = | ||||
| @ -44,6 +53,14 @@ export const SignalEndpointsTable = () => { | ||||
| 
 | ||||
|     const [signalsModalOpen, setSignalsModalOpen] = useState(false); | ||||
| 
 | ||||
|     const StyledAlert = styled(Alert)(({ theme }) => ({ | ||||
|         marginBottom: theme.spacing(3), | ||||
|     })); | ||||
| 
 | ||||
|     const StyledParagraph = styled('p')(({ theme }) => ({ | ||||
|         marginBottom: theme.spacing(2), | ||||
|     })); | ||||
| 
 | ||||
|     const onToggleSignalEndpoint = async ( | ||||
|         { id, name }: ISignalEndpoint, | ||||
|         enabled: boolean, | ||||
| @ -227,69 +244,143 @@ export const SignalEndpointsTable = () => { | ||||
|                 <PageHeader | ||||
|                     title={`Signal endpoints (${signalEndpoints.length})`} | ||||
|                     actions={ | ||||
|                         <Button | ||||
|                             variant='contained' | ||||
|                             color='primary' | ||||
|                             onClick={() => { | ||||
|                                 setSelectedSignalEndpoint(undefined); | ||||
|                                 setModalOpen(true); | ||||
|                             }} | ||||
|                         > | ||||
|                             New signal endpoint | ||||
|                         </Button> | ||||
|                         <> | ||||
|                             <ConditionallyRender | ||||
|                                 condition={!hasSubmittedFeedback} | ||||
|                                 show={ | ||||
|                                     <Button | ||||
|                                         startIcon={<ReviewsOutlined />} | ||||
|                                         variant='outlined' | ||||
|                                         onClick={() => { | ||||
|                                             openFeedback({ | ||||
|                                                 title: 'Do you find signals and actions easy to use?', | ||||
|                                                 positiveLabel: | ||||
|                                                     'What do you like most about signals and actions?', | ||||
|                                                 areasForImprovementsLabel: | ||||
|                                                     'What needs to change to use signals and actions the way you want?', | ||||
|                                             }); | ||||
|                                         }} | ||||
|                                     > | ||||
|                                         Provide feedback | ||||
|                                     </Button> | ||||
|                                 } | ||||
|                             /> | ||||
|                             <PermissionButton | ||||
|                                 variant='contained' | ||||
|                                 color='primary' | ||||
|                                 permission={ADMIN} | ||||
|                                 onClick={() => { | ||||
|                                     setSelectedSignalEndpoint(undefined); | ||||
|                                     setModalOpen(true); | ||||
|                                 }} | ||||
|                             > | ||||
|                                 New signal endpoint | ||||
|                             </PermissionButton> | ||||
|                         </> | ||||
|                     } | ||||
|                 /> | ||||
|             } | ||||
|         > | ||||
|             <VirtualizedTable | ||||
|                 rows={rows} | ||||
|                 headerGroups={headerGroups} | ||||
|                 prepareRow={prepareRow} | ||||
|             /> | ||||
|             <ConditionallyRender | ||||
|                 condition={rows.length === 0} | ||||
|                 show={ | ||||
|                     <TablePlaceholder> | ||||
|                         No signal endpoints available. Get started by adding | ||||
|                         one. | ||||
|                     </TablePlaceholder> | ||||
|                 } | ||||
|             /> | ||||
|             <SignalEndpointsModal | ||||
|                 signalEndpoint={selectedSignalEndpoint} | ||||
|                 open={modalOpen} | ||||
|                 setOpen={setModalOpen} | ||||
|                 newToken={(token: string, signalEndpoint: ISignalEndpoint) => { | ||||
|                     setNewToken(token); | ||||
|                     setSelectedSignalEndpoint(signalEndpoint); | ||||
|                     setTokenDialog(true); | ||||
|                 }} | ||||
|                 onOpenSignals={() => { | ||||
|                     setModalOpen(false); | ||||
|                     setSignalsModalOpen(true); | ||||
|                 }} | ||||
|             /> | ||||
|             <SignalEndpointsSignalsModal | ||||
|                 signalEndpoint={selectedSignalEndpoint} | ||||
|                 open={signalsModalOpen} | ||||
|                 setOpen={setSignalsModalOpen} | ||||
|                 onOpenConfiguration={() => { | ||||
|                     setSignalsModalOpen(false); | ||||
|                     setModalOpen(true); | ||||
|                 }} | ||||
|             /> | ||||
|             <SignalEndpointsTokensDialog | ||||
|                 open={tokenDialog} | ||||
|                 setOpen={setTokenDialog} | ||||
|                 token={newToken} | ||||
|                 signalEndpoint={selectedSignalEndpoint} | ||||
|             /> | ||||
|             <SignalEndpointsDeleteDialog | ||||
|                 signalEndpoint={selectedSignalEndpoint} | ||||
|                 open={deleteOpen} | ||||
|                 setOpen={setDeleteOpen} | ||||
|                 onConfirm={onDeleteConfirm} | ||||
|             /> | ||||
|             <StyledAlert severity='info'> | ||||
|                 <StyledParagraph> | ||||
|                     Signals and actions allow you to respond to events in your | ||||
|                     real-time monitoring system by automating tasks such as | ||||
|                     disabling a beta feature in response to an increase in | ||||
|                     errors or a drop in conversion rates. | ||||
|                 </StyledParagraph> | ||||
| 
 | ||||
|                 <StyledParagraph> | ||||
|                     <ul> | ||||
|                         <li> | ||||
|                             <b>Signal endpoints</b> are used to send signals to | ||||
|                             Unleash. This allows you to integrate Unleash with | ||||
|                             any external tool. | ||||
|                         </li> | ||||
| 
 | ||||
|                         <li> | ||||
|                             <b>Actions</b>, which are configured inside | ||||
|                             projects, allow you to react to those signals and | ||||
|                             enable or disable flags based on certain conditions. | ||||
|                         </li> | ||||
|                     </ul> | ||||
|                 </StyledParagraph> | ||||
| 
 | ||||
|                 <StyledParagraph> | ||||
|                     Read more about these features in our documentation:{' '} | ||||
|                     <a | ||||
|                         href='https://docs.getunleash.io/reference/signals' | ||||
|                         target='_blank' | ||||
|                         rel='noreferrer' | ||||
|                     > | ||||
|                         Signals | ||||
|                     </a>{' '} | ||||
|                     and{' '} | ||||
|                     <a | ||||
|                         href='https://docs.getunleash.io/reference/actions' | ||||
|                         target='_blank' | ||||
|                         rel='noreferrer' | ||||
|                     > | ||||
|                         Actions | ||||
|                     </a> | ||||
|                 </StyledParagraph> | ||||
|             </StyledAlert> | ||||
| 
 | ||||
|             <PermissionGuard permissions={ADMIN}> | ||||
|                 <> | ||||
|                     <VirtualizedTable | ||||
|                         rows={rows} | ||||
|                         headerGroups={headerGroups} | ||||
|                         prepareRow={prepareRow} | ||||
|                     /> | ||||
|                     <ConditionallyRender | ||||
|                         condition={rows.length === 0} | ||||
|                         show={ | ||||
|                             <TablePlaceholder> | ||||
|                                 No signal endpoints available. Get started by | ||||
|                                 adding one. | ||||
|                             </TablePlaceholder> | ||||
|                         } | ||||
|                     /> | ||||
|                     <SignalEndpointsModal | ||||
|                         signalEndpoint={selectedSignalEndpoint} | ||||
|                         open={modalOpen} | ||||
|                         setOpen={setModalOpen} | ||||
|                         newToken={( | ||||
|                             token: string, | ||||
|                             signalEndpoint: ISignalEndpoint, | ||||
|                         ) => { | ||||
|                             setNewToken(token); | ||||
|                             setSelectedSignalEndpoint(signalEndpoint); | ||||
|                             setTokenDialog(true); | ||||
|                         }} | ||||
|                         onOpenSignals={() => { | ||||
|                             setModalOpen(false); | ||||
|                             setSignalsModalOpen(true); | ||||
|                         }} | ||||
|                     /> | ||||
|                     <SignalEndpointsSignalsModal | ||||
|                         signalEndpoint={selectedSignalEndpoint} | ||||
|                         open={signalsModalOpen} | ||||
|                         setOpen={setSignalsModalOpen} | ||||
|                         onOpenConfiguration={() => { | ||||
|                             setSignalsModalOpen(false); | ||||
|                             setModalOpen(true); | ||||
|                         }} | ||||
|                     /> | ||||
|                     <SignalEndpointsTokensDialog | ||||
|                         open={tokenDialog} | ||||
|                         setOpen={setTokenDialog} | ||||
|                         token={newToken} | ||||
|                         signalEndpoint={selectedSignalEndpoint} | ||||
|                     /> | ||||
|                     <SignalEndpointsDeleteDialog | ||||
|                         signalEndpoint={selectedSignalEndpoint} | ||||
|                         open={deleteOpen} | ||||
|                         setOpen={setDeleteOpen} | ||||
|                         onConfirm={onDeleteConfirm} | ||||
|                     /> | ||||
|                 </> | ||||
|             </PermissionGuard> | ||||
|         </PageContent> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -1,5 +1,3 @@ | ||||
| import { ADMIN } from 'component/providers/AccessProvider/permissions'; | ||||
| import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard'; | ||||
| import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; | ||||
| import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature'; | ||||
| import { SignalEndpointsTable } from './SignalEndpointsTable/SignalEndpointsTable'; | ||||
| @ -11,11 +9,5 @@ export const Signals = () => { | ||||
|         return <PremiumFeature feature='signals' />; | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|         <div> | ||||
|             <PermissionGuard permissions={ADMIN}> | ||||
|                 <SignalEndpointsTable /> | ||||
|             </PermissionGuard> | ||||
|         </div> | ||||
|     ); | ||||
|     return <SignalEndpointsTable />; | ||||
| }; | ||||
|  | ||||
| @ -62,7 +62,9 @@ export type CustomEvents = | ||||
|     | 'many-strategies' | ||||
|     | 'sdk-banner' | ||||
|     | 'feature-lifecycle' | ||||
|     | 'command-bar'; | ||||
|     | 'command-bar' | ||||
|     | 'new-in-unleash-click' | ||||
|     | 'new-in-unleash-dismiss'; | ||||
| 
 | ||||
| export const usePlausibleTracker = () => { | ||||
|     const plausible = useContext(PlausibleContext); | ||||
|  | ||||
| @ -4,7 +4,8 @@ export type IFeedbackCategory = | ||||
|     | 'search' | ||||
|     | 'insights' | ||||
|     | 'applicationOverview' | ||||
|     | 'newProjectOverview'; | ||||
|     | 'newProjectOverview' | ||||
|     | 'signals'; | ||||
| 
 | ||||
| export const useUserSubmittedFeedback = (category: IFeedbackCategory) => { | ||||
|     const key = `unleash-userSubmittedFeedback:${category}`; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user