mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
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.  Inside signals page we're also including a feedback button to try to collect some insights.  --------- <!-- 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:
parent
e69072b1e3
commit
44eb540c24
@ -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,20 +244,89 @@ export const SignalEndpointsTable = () => {
|
||||
<PageHeader
|
||||
title={`Signal endpoints (${signalEndpoints.length})`}
|
||||
actions={
|
||||
<>
|
||||
<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
|
||||
</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
|
||||
rows={rows}
|
||||
headerGroups={headerGroups}
|
||||
@ -250,8 +336,8 @@ export const SignalEndpointsTable = () => {
|
||||
condition={rows.length === 0}
|
||||
show={
|
||||
<TablePlaceholder>
|
||||
No signal endpoints available. Get started by adding
|
||||
one.
|
||||
No signal endpoints available. Get started by
|
||||
adding one.
|
||||
</TablePlaceholder>
|
||||
}
|
||||
/>
|
||||
@ -259,7 +345,10 @@ export const SignalEndpointsTable = () => {
|
||||
signalEndpoint={selectedSignalEndpoint}
|
||||
open={modalOpen}
|
||||
setOpen={setModalOpen}
|
||||
newToken={(token: string, signalEndpoint: ISignalEndpoint) => {
|
||||
newToken={(
|
||||
token: string,
|
||||
signalEndpoint: ISignalEndpoint,
|
||||
) => {
|
||||
setNewToken(token);
|
||||
setSelectedSignalEndpoint(signalEndpoint);
|
||||
setTokenDialog(true);
|
||||
@ -290,6 +379,8 @@ export const SignalEndpointsTable = () => {
|
||||
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 />;
|
||||
};
|
||||
|
@ -61,7 +61,10 @@ export type CustomEvents =
|
||||
| 'insights-share'
|
||||
| 'many-strategies'
|
||||
| 'sdk-banner'
|
||||
| 'feature-lifecycle';
|
||||
| 'feature-lifecycle'
|
||||
| '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