1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-24 01:18:01 +02:00

feat: what's new in Unleash (#7497) (#7525)

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)

---------

<!-- Thanks for creating a PR! To make it easier for reviewers and
everyone else to understand what your changes relate to, please add some
relevant content to the headings below. Feel free to ignore or delete
sections that you don't think are relevant. Thank you! ❤️ -->

## About the changes
<!-- Describe the changes introduced. What are they and why are they
being introduced? Feel free to also add screenshots or steps to view the
changes if they're visual. -->

<!-- Does it close an issue? Multiple? -->
Closes #

<!-- (For internal contributors): Does it relate to an issue on public
roadmap? -->
<!--
Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item:
#
-->

### Important files
<!-- PRs can contain a lot of changes, but not all changes are equally
important. Where should a reviewer start looking to get an overview of
the changes? Are any files particularly important? -->


## Discussion points
<!-- Anything about the PR you'd like to discuss before it gets merged?
Got any questions or doubts? -->

Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
Gastón Fournier 2024-07-03 12:56:08 +02:00 committed by GitHub
parent e69072b1e3
commit 44eb540c24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 388 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,20 +244,89 @@ export const SignalEndpointsTable = () => {
<PageHeader <PageHeader
title={`Signal endpoints (${signalEndpoints.length})`} title={`Signal endpoints (${signalEndpoints.length})`}
actions={ actions={
<>
<ConditionallyRender
condition={!hasSubmittedFeedback}
show={
<Button <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' variant='contained'
color='primary' color='primary'
permission={ADMIN}
onClick={() => { onClick={() => {
setSelectedSignalEndpoint(undefined); setSelectedSignalEndpoint(undefined);
setModalOpen(true); setModalOpen(true);
}} }}
> >
New signal endpoint New signal endpoint
</Button> </PermissionButton>
</>
} }
/> />
} }
> >
<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 <VirtualizedTable
rows={rows} rows={rows}
headerGroups={headerGroups} headerGroups={headerGroups}
@ -250,8 +336,8 @@ export const SignalEndpointsTable = () => {
condition={rows.length === 0} condition={rows.length === 0}
show={ show={
<TablePlaceholder> <TablePlaceholder>
No signal endpoints available. Get started by adding No signal endpoints available. Get started by
one. adding one.
</TablePlaceholder> </TablePlaceholder>
} }
/> />
@ -259,7 +345,10 @@ export const SignalEndpointsTable = () => {
signalEndpoint={selectedSignalEndpoint} signalEndpoint={selectedSignalEndpoint}
open={modalOpen} open={modalOpen}
setOpen={setModalOpen} setOpen={setModalOpen}
newToken={(token: string, signalEndpoint: ISignalEndpoint) => { newToken={(
token: string,
signalEndpoint: ISignalEndpoint,
) => {
setNewToken(token); setNewToken(token);
setSelectedSignalEndpoint(signalEndpoint); setSelectedSignalEndpoint(signalEndpoint);
setTokenDialog(true); setTokenDialog(true);
@ -290,6 +379,8 @@ export const SignalEndpointsTable = () => {
setOpen={setDeleteOpen} setOpen={setDeleteOpen}
onConfirm={onDeleteConfirm} 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

@ -61,7 +61,10 @@ export type CustomEvents =
| 'insights-share' | 'insights-share'
| 'many-strategies' | 'many-strategies'
| 'sdk-banner' | 'sdk-banner'
| 'feature-lifecycle'; | 'feature-lifecycle'
| '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}`;