1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +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.


![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 { 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}

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

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 { 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 />;
};

View File

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

View File

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