From 5832fc7d812c0366cdfc81c11d8093091c98e6b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 3 Jul 2024 09:54:38 +0200 Subject: [PATCH] feat: what's new in Unleash (#7497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. ![image](https://github.com/Unleash/unleash/assets/14320932/b2b5b65a-1812-4fc9-addf-c47c3cc90af3) Inside signals page we're also including a feedback button to try to collect some insights. ![image](https://github.com/Unleash/unleash/assets/14320932/a2edb355-55e8-4939-b29d-2ba4e1f68001) --------- Co-authored-by: Nuno Góis --- .../NavigationSidebar/NavigationSidebar.tsx | 3 + .../NewInUnleash/NewInUnleash.tsx | 161 +++++++++++++ .../NewInUnleash/NewInUnleashItem.tsx | 66 ++++++ .../SignalEndpointsTable.tsx | 211 +++++++++++++----- frontend/src/component/signals/Signals.tsx | 10 +- frontend/src/hooks/usePlausibleTracker.ts | 4 +- frontend/src/hooks/useSubmittedFeedback.ts | 3 +- 7 files changed, 387 insertions(+), 71 deletions(-) create mode 100644 frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleash.tsx create mode 100644 frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleashItem.tsx diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.tsx index 869cffea7e..614522f83d 100644 --- a/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.tsx +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.tsx @@ -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 ( <> + { return ( + setMode('full')} /> ({ + 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: , + 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 ( + + + + + + new_releases + + + + + + ); + } + + return ( + + + new_releases + New in Unleash + + + {visibleItems.map(({ label, icon, link }) => ( + { + trackEvent('new-in-unleash-click', { + props: { + label, + }, + }); + navigate(link); + onItemClick?.(); + }} + onDismiss={() => { + trackEvent('new-in-unleash-dismiss', { + props: { + label, + }, + }); + setSeenItems(new Set([...seenItems, label])); + }} + > + {label} + + ))} + + + ); +}; diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleashItem.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleashItem.tsx new file mode 100644 index 0000000000..93cc126540 --- /dev/null +++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleashItem.tsx @@ -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 ( + + + + {icon} + {children} + + + + + + + + + ); +}; diff --git a/frontend/src/component/signals/SignalEndpointsTable/SignalEndpointsTable.tsx b/frontend/src/component/signals/SignalEndpointsTable/SignalEndpointsTable.tsx index 370a72ad4c..d72f2d4c4d 100644 --- a/frontend/src/component/signals/SignalEndpointsTable/SignalEndpointsTable.tsx +++ b/frontend/src/component/signals/SignalEndpointsTable/SignalEndpointsTable.tsx @@ -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 = () => { { - setSelectedSignalEndpoint(undefined); - setModalOpen(true); - }} - > - New signal endpoint - + <> + } + 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 + + } + /> + { + setSelectedSignalEndpoint(undefined); + setModalOpen(true); + }} + > + New signal endpoint + + } /> } > - - - No signal endpoints available. Get started by adding - one. - - } - /> - { - setNewToken(token); - setSelectedSignalEndpoint(signalEndpoint); - setTokenDialog(true); - }} - onOpenSignals={() => { - setModalOpen(false); - setSignalsModalOpen(true); - }} - /> - { - setSignalsModalOpen(false); - setModalOpen(true); - }} - /> - - + + + 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. + + + +
    +
  • + Signal endpoints are used to send signals to + Unleash. This allows you to integrate Unleash with + any external tool. +
  • + +
  • + Actions, which are configured inside + projects, allow you to react to those signals and + enable or disable flags based on certain conditions. +
  • +
+
+ + + Read more about these features in our documentation:{' '} + + Signals + {' '} + and{' '} + + Actions + + +
+ + + <> + + + No signal endpoints available. Get started by + adding one. + + } + /> + { + setNewToken(token); + setSelectedSignalEndpoint(signalEndpoint); + setTokenDialog(true); + }} + onOpenSignals={() => { + setModalOpen(false); + setSignalsModalOpen(true); + }} + /> + { + setSignalsModalOpen(false); + setModalOpen(true); + }} + /> + + + + ); }; diff --git a/frontend/src/component/signals/Signals.tsx b/frontend/src/component/signals/Signals.tsx index 5738938311..b4dff9e6a4 100644 --- a/frontend/src/component/signals/Signals.tsx +++ b/frontend/src/component/signals/Signals.tsx @@ -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 ; } - return ( -
- - - -
- ); + return ; }; diff --git a/frontend/src/hooks/usePlausibleTracker.ts b/frontend/src/hooks/usePlausibleTracker.ts index c7152ab37a..3e70d7b36a 100644 --- a/frontend/src/hooks/usePlausibleTracker.ts +++ b/frontend/src/hooks/usePlausibleTracker.ts @@ -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); diff --git a/frontend/src/hooks/useSubmittedFeedback.ts b/frontend/src/hooks/useSubmittedFeedback.ts index 6e4626c02e..612e238d4a 100644 --- a/frontend/src/hooks/useSubmittedFeedback.ts +++ b/frontend/src/hooks/useSubmittedFeedback.ts @@ -4,7 +4,8 @@ export type IFeedbackCategory = | 'search' | 'insights' | 'applicationOverview' - | 'newProjectOverview'; + | 'newProjectOverview' + | 'signals'; export const useUserSubmittedFeedback = (category: IFeedbackCategory) => { const key = `unleash-userSubmittedFeedback:${category}`;