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:
parent
06971375cb
commit
5832fc7d81
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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 />;
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -4,7 +4,8 @@ export type IFeedbackCategory =
|
||||
| 'search'
|
||||
| 'insights'
|
||||
| 'applicationOverview'
|
||||
| 'newProjectOverview';
|
||||
| 'newProjectOverview'
|
||||
| 'signals';
|
||||
|
||||
export const useUserSubmittedFeedback = (category: IFeedbackCategory) => {
|
||||
const key = `unleash-userSubmittedFeedback:${category}`;
|
||||
|
Loading…
Reference in New Issue
Block a user