1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-29 01:15:48 +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,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

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