1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-03 01:18:43 +02: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.


![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 <github@nunogois.com>
This commit is contained in:
Gastón Fournier 2024-07-03 09:54:38 +02:00 committed by GitHub
parent 06971375cb
commit 5832fc7d81
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 387 additions and 71 deletions

View File

@ -15,6 +15,7 @@ import {
import { useInitialPathname } from './useInitialPathname'; import { useInitialPathname } from './useInitialPathname';
import { useLastViewedProject } from 'hooks/useLastViewedProject'; import { useLastViewedProject } from 'hooks/useLastViewedProject';
import { useLastViewedFlags } from 'hooks/useLastViewedFlags'; import { useLastViewedFlags } from 'hooks/useLastViewedFlags';
import { NewInUnleash } from './NewInUnleash/NewInUnleash';
export const MobileNavigationSidebar: FC<{ onClick: () => void }> = ({ export const MobileNavigationSidebar: FC<{ onClick: () => void }> = ({
onClick, onClick,
@ -23,6 +24,7 @@ export const MobileNavigationSidebar: FC<{ onClick: () => void }> = ({
return ( return (
<> <>
<NewInUnleash onItemClick={onClick} />
<PrimaryNavigationList mode='full' onClick={onClick} /> <PrimaryNavigationList mode='full' onClick={onClick} />
<SecondaryNavigationList <SecondaryNavigationList
routes={routes.mainNavRoutes} routes={routes.mainNavRoutes}
@ -67,6 +69,7 @@ export const NavigationSidebar = () => {
return ( return (
<StretchContainer> <StretchContainer>
<NewInUnleash mode={mode} onMiniModeClick={() => setMode('full')} />
<PrimaryNavigationList <PrimaryNavigationList
mode={mode} mode={mode}
onClick={setActiveItem} onClick={setActiveItem}

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -3,7 +3,8 @@ import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; 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 { useFlexLayout, useSortBy, useTable } from 'react-table';
import { sortTypes } from 'utils/sortTypes'; import { sortTypes } from 'utils/sortTypes';
import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; 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 { SignalEndpointsSignalsModal } from '../SignalEndpointsSignals/SignalEndpointsSignalsModal';
import { PageContent } from 'component/common/PageContent/PageContent'; import { PageContent } from 'component/common/PageContent/PageContent';
import { PageHeader } from 'component/common/PageHeader/PageHeader'; 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 = () => { export const SignalEndpointsTable = () => {
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const { openFeedback, hasSubmittedFeedback } = useFeedback(
'signals',
'automatic',
);
const { signalEndpoints, refetch } = useSignalEndpoints(); const { signalEndpoints, refetch } = useSignalEndpoints();
const { toggleSignalEndpoint, removeSignalEndpoint } = const { toggleSignalEndpoint, removeSignalEndpoint } =
@ -44,6 +53,14 @@ export const SignalEndpointsTable = () => {
const [signalsModalOpen, setSignalsModalOpen] = useState(false); 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 ( const onToggleSignalEndpoint = async (
{ id, name }: ISignalEndpoint, { id, name }: ISignalEndpoint,
enabled: boolean, enabled: boolean,
@ -227,69 +244,143 @@ export const SignalEndpointsTable = () => {
<PageHeader <PageHeader
title={`Signal endpoints (${signalEndpoints.length})`} title={`Signal endpoints (${signalEndpoints.length})`}
actions={ actions={
<Button <>
variant='contained' <ConditionallyRender
color='primary' condition={!hasSubmittedFeedback}
onClick={() => { show={
setSelectedSignalEndpoint(undefined); <Button
setModalOpen(true); startIcon={<ReviewsOutlined />}
}} variant='outlined'
> onClick={() => {
New signal endpoint openFeedback({
</Button> 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 <StyledAlert severity='info'>
rows={rows} <StyledParagraph>
headerGroups={headerGroups} Signals and actions allow you to respond to events in your
prepareRow={prepareRow} real-time monitoring system by automating tasks such as
/> disabling a beta feature in response to an increase in
<ConditionallyRender errors or a drop in conversion rates.
condition={rows.length === 0} </StyledParagraph>
show={
<TablePlaceholder> <StyledParagraph>
No signal endpoints available. Get started by adding <ul>
one. <li>
</TablePlaceholder> <b>Signal endpoints</b> are used to send signals to
} Unleash. This allows you to integrate Unleash with
/> any external tool.
<SignalEndpointsModal </li>
signalEndpoint={selectedSignalEndpoint}
open={modalOpen} <li>
setOpen={setModalOpen} <b>Actions</b>, which are configured inside
newToken={(token: string, signalEndpoint: ISignalEndpoint) => { projects, allow you to react to those signals and
setNewToken(token); enable or disable flags based on certain conditions.
setSelectedSignalEndpoint(signalEndpoint); </li>
setTokenDialog(true); </ul>
}} </StyledParagraph>
onOpenSignals={() => {
setModalOpen(false); <StyledParagraph>
setSignalsModalOpen(true); Read more about these features in our documentation:{' '}
}} <a
/> href='https://docs.getunleash.io/reference/signals'
<SignalEndpointsSignalsModal target='_blank'
signalEndpoint={selectedSignalEndpoint} rel='noreferrer'
open={signalsModalOpen} >
setOpen={setSignalsModalOpen} Signals
onOpenConfiguration={() => { </a>{' '}
setSignalsModalOpen(false); and{' '}
setModalOpen(true); <a
}} href='https://docs.getunleash.io/reference/actions'
/> target='_blank'
<SignalEndpointsTokensDialog rel='noreferrer'
open={tokenDialog} >
setOpen={setTokenDialog} Actions
token={newToken} </a>
signalEndpoint={selectedSignalEndpoint} </StyledParagraph>
/> </StyledAlert>
<SignalEndpointsDeleteDialog
signalEndpoint={selectedSignalEndpoint} <PermissionGuard permissions={ADMIN}>
open={deleteOpen} <>
setOpen={setDeleteOpen} <VirtualizedTable
onConfirm={onDeleteConfirm} 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> </PageContent>
); );
}; };

View File

@ -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 useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature'; import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
import { SignalEndpointsTable } from './SignalEndpointsTable/SignalEndpointsTable'; import { SignalEndpointsTable } from './SignalEndpointsTable/SignalEndpointsTable';
@ -11,11 +9,5 @@ export const Signals = () => {
return <PremiumFeature feature='signals' />; return <PremiumFeature feature='signals' />;
} }
return ( return <SignalEndpointsTable />;
<div>
<PermissionGuard permissions={ADMIN}>
<SignalEndpointsTable />
</PermissionGuard>
</div>
);
}; };

View File

@ -62,7 +62,9 @@ export type CustomEvents =
| 'many-strategies' | 'many-strategies'
| 'sdk-banner' | 'sdk-banner'
| 'feature-lifecycle' | 'feature-lifecycle'
| 'command-bar'; | 'command-bar'
| 'new-in-unleash-click'
| 'new-in-unleash-dismiss';
export const usePlausibleTracker = () => { export const usePlausibleTracker = () => {
const plausible = useContext(PlausibleContext); const plausible = useContext(PlausibleContext);

View File

@ -4,7 +4,8 @@ export type IFeedbackCategory =
| 'search' | 'search'
| 'insights' | 'insights'
| 'applicationOverview' | 'applicationOverview'
| 'newProjectOverview'; | 'newProjectOverview'
| 'signals';
export const useUserSubmittedFeedback = (category: IFeedbackCategory) => { export const useUserSubmittedFeedback = (category: IFeedbackCategory) => {
const key = `unleash-userSubmittedFeedback:${category}`; const key = `unleash-userSubmittedFeedback:${category}`;