(
key: K,
- value: EventTimelineState[K],
+ value: EventTimelinePersistentState[K],
) => {
setState((prevState) => ({ ...prevState, [key]: value }));
};
+ const onSetHighlighted = (highlighted: boolean) => {
+ setHighlighted(highlighted);
+ if (highlighted) {
+ setTimeout(() => setHighlighted(false), 3000);
+ }
+ };
+
const contextValue: IEventTimelineContext = {
...state,
+ highlighted,
setOpen: (open: boolean) => setField('open', open),
setTimeSpan: (timeSpan: TimeSpanOption) =>
setField('timeSpan', timeSpan),
@@ -110,6 +126,7 @@ export const EventTimelineProvider = ({
setField('environment', environment),
setSignalsSuggestionSeen: (seen: boolean) =>
setField('signalsSuggestionSeen', seen),
+ setHighlighted: onSetHighlighted,
};
return (
diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.test.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.test.tsx
index 943f7f08155b..ea0aacaf5876 100644
--- a/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.test.tsx
+++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/NavigationSidebar.test.tsx
@@ -11,6 +11,7 @@ import {
import { type FC, useEffect } from 'react';
import { useLastViewedProject } from 'hooks/useLastViewedProject';
import { testServerRoute, testServerSetup } from 'utils/testServer';
+import { EventTimelineProvider } from 'component/events/EventTimeline/EventTimelineProvider';
const server = testServerSetup();
@@ -18,8 +19,29 @@ beforeEach(() => {
window.localStorage.clear();
});
+const TestNavigationSidebar: FC<{
+ project?: string;
+ flags?: LastViewedFlag[];
+}> = ({ project, flags }) => {
+ const { setLastViewed: setProject } = useLastViewedProject();
+ const { setLastViewed: setFlag } = useLastViewedFlags();
+
+ useEffect(() => {
+ setProject(project);
+ flags?.forEach((flag) => {
+ setFlag(flag);
+ });
+ }, []);
+
+ return (
+
+
+
+ );
+};
+
test('switch full mode and mini mode', () => {
- render();
+ render();
expect(screen.queryByText('Projects')).toBeInTheDocument();
expect(screen.queryByText('Applications')).toBeInTheDocument();
@@ -42,7 +64,7 @@ test('switch full mode and mini mode', () => {
});
test('persist navigation mode and expansion selection in storage', async () => {
- render();
+ render();
const { value } = createLocalStorage('navigation-mode:v1', {});
expect(value).toBe('full');
@@ -70,7 +92,7 @@ test('persist navigation mode and expansion selection in storage', async () => {
test('select active item', async () => {
render(
- } />
+ } />
,
{ route: '/search' },
);
@@ -80,30 +102,13 @@ test('select active item', async () => {
expect(links[1]).toHaveClass(classes.selected);
});
-const SetupComponent: FC<{ project: string; flags: LastViewedFlag[] }> = ({
- project,
- flags,
-}) => {
- const { setLastViewed: setProject } = useLastViewedProject();
- const { setLastViewed: setFlag } = useLastViewedFlags();
-
- useEffect(() => {
- setProject(project);
- flags.forEach((flag) => {
- setFlag(flag);
- });
- }, []);
-
- return ;
-};
-
test('print recent projects and flags', async () => {
testServerRoute(server, `/api/admin/projects/projectA/overview`, {
name: 'projectNameA',
});
render(
- ,
diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleash.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleash.tsx
index c61f48490435..b4813f449f60 100644
--- a/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleash.tsx
+++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleash.tsx
@@ -16,6 +16,10 @@ import type { NavigationMode } from 'component/layout/MainLayout/NavigationSideb
import { NewInUnleashItem } from './NewInUnleashItem';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { ReactComponent as SignalsPreview } from 'assets/img/signals.svg';
+import LinearScaleIcon from '@mui/icons-material/LinearScale';
+import { useNavigate } from 'react-router-dom';
+import { useEventTimelineContext } from 'component/events/EventTimeline/EventTimelineContext';
+import { ReactComponent as EventTimelinePreview } from 'assets/img/eventTimeline.svg';
const StyledNewInUnleash = styled('div')(({ theme }) => ({
margin: theme.spacing(2, 0, 1, 0),
@@ -67,11 +71,15 @@ const StyledSignalsIcon = styled(Signals)(({ theme }) => ({
color: theme.palette.primary.main,
}));
+const StyledLinearScaleIcon = styled(LinearScaleIcon)(({ theme }) => ({
+ color: theme.palette.primary.main,
+}));
+
type NewItem = {
label: string;
summary: string;
icon: ReactNode;
- link: string;
+ onCheckItOut: () => void;
docsLink: string;
show: boolean;
longDescription: ReactNode;
@@ -89,13 +97,17 @@ export const NewInUnleash = ({
onItemClick,
onMiniModeClick,
}: INewInUnleashProps) => {
+ const navigate = useNavigate();
const { trackEvent } = usePlausibleTracker();
const [seenItems, setSeenItems] = useLocalStorageState(
'new-in-unleash-seen:v1',
new Set(),
);
- const { isEnterprise } = useUiConfig();
+ const { isOss, isEnterprise } = useUiConfig();
const signalsEnabled = useUiFlag('signals');
+ const eventTimelineEnabled = useUiFlag('eventTimeline');
+
+ const { setHighlighted } = useEventTimelineContext();
const items: NewItem[] = [
{
@@ -103,7 +115,7 @@ export const NewInUnleash = ({
summary: 'Listen to signals via Webhooks',
icon: ,
preview: ,
- link: '/integrations/signals',
+ onCheckItOut: () => navigate('/integrations/signals'),
docsLink: 'https://docs.getunleash.io/reference/signals',
show: isEnterprise() && signalsEnabled,
longDescription: (
@@ -134,6 +146,35 @@ export const NewInUnleash = ({
>
),
},
+ {
+ label: 'Event timeline',
+ summary: 'Keep track of recent events across all your projects',
+ icon: ,
+ preview: ,
+ onCheckItOut: () => {
+ setHighlighted(true);
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+ },
+ docsLink: 'https://docs.getunleash.io/reference/events',
+ show: !isOss() && eventTimelineEnabled,
+ longDescription: (
+ <>
+
+ Monitor recent events across all your projects in one
+ unified timeline.
+
+
+
+ You can access the event timeline from the top menu to
+ get an overview of changes and quickly identify and
+ debug any issues.
+
+ >
+ ),
+ },
];
const visibleItems = items.filter(
@@ -172,7 +213,7 @@ export const NewInUnleash = ({
({
label,
icon,
- link,
+ onCheckItOut,
longDescription,
docsLink,
preview,
@@ -197,7 +238,7 @@ export const NewInUnleash = ({
}}
label={label}
icon={icon}
- link={link}
+ onCheckItOut={onCheckItOut}
preview={preview}
longDescription={longDescription}
docsLink={docsLink}
diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleashItem.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleashItem.tsx
index fb09483d850f..cf79f1fb6420 100644
--- a/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleashItem.tsx
+++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleashItem.tsx
@@ -36,7 +36,7 @@ interface INewInUnleashItemProps {
onDismiss: () => void;
label: string;
longDescription: ReactNode;
- link: string;
+ onCheckItOut: () => void;
docsLink: string;
preview?: ReactNode;
summary: string;
@@ -62,7 +62,7 @@ export const NewInUnleashItem = ({
onDismiss,
label,
longDescription,
- link,
+ onCheckItOut,
docsLink,
preview,
summary,
@@ -87,7 +87,7 @@ export const NewInUnleashItem = ({
onClose={handleTooltipClose}
title={label}
longDescription={longDescription}
- link={link}
+ onCheckItOut={onCheckItOut}
docsLink={docsLink}
preview={preview}
>
diff --git a/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleashTooltip.tsx b/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleashTooltip.tsx
index 027087763fe7..761f994dfb18 100644
--- a/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleashTooltip.tsx
+++ b/frontend/src/component/layout/MainLayout/NavigationSidebar/NewInUnleash/NewInUnleashTooltip.tsx
@@ -9,7 +9,7 @@ import {
Typography,
ClickAwayListener,
} from '@mui/material';
-import { type Link as RouterLink, useNavigate } from 'react-router-dom';
+import type { Link as RouterLink } from 'react-router-dom';
import OpenInNew from '@mui/icons-material/OpenInNew';
import { ReactComponent as UnleashLogo } from 'assets/img/logoWithWhiteText.svg';
@@ -22,6 +22,7 @@ const Header = styled(Box)(({ theme }) => ({
const Body = styled(Box)(({ theme }) => ({
padding: theme.spacing(2),
+ lineHeight: 1.5,
}));
const StyledLink = styled(Link)(({ theme }) => ({
@@ -57,17 +58,22 @@ const CenteredPreview = styled(Box)(({ theme }) => ({
}));
const LongDescription = styled(Box)(({ theme }) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: theme.spacing(1.5),
ul: {
+ margin: 0,
paddingLeft: theme.spacing(2),
},
}));
const Title = styled(Typography)(({ theme }) => ({
padding: theme.spacing(1, 0, 2, 0),
+ lineHeight: 1.5,
}));
const ReadMore = styled(Box)(({ theme }) => ({
- padding: theme.spacing(2, 0, 4, 0),
+ padding: theme.spacing(3, 0),
}));
export const NewInUnleashTooltip: FC<{
@@ -75,7 +81,7 @@ export const NewInUnleashTooltip: FC<{
title: string;
longDescription: ReactNode;
docsLink: string;
- link: string;
+ onCheckItOut: () => void;
open: boolean;
preview?: ReactNode;
onClose: () => void;
@@ -83,72 +89,68 @@ export const NewInUnleashTooltip: FC<{
children,
title,
longDescription,
- link,
+ onCheckItOut,
docsLink,
preview,
open,
onClose,
-}) => {
- const navigate = useNavigate();
-
- return (
-
-
-
- {preview ? (
- {preview}
- ) : (
-
-
-
- )}
-
-
- {title}
- {longDescription}
-
-
- Read more in our
- documentation
-
-
-
-
-
-
- }
- >
- {children}
-
- );
-};
+ Read more in our
+ documentation
+
+
+
+
+
+
+ }
+ >
+ {children}
+
+);
diff --git a/frontend/src/component/menu/Header/Header.tsx b/frontend/src/component/menu/Header/Header.tsx
index cd3be43cc682..0e7aff9a59ab 100644
--- a/frontend/src/component/menu/Header/Header.tsx
+++ b/frontend/src/component/menu/Header/Header.tsx
@@ -33,9 +33,7 @@ import { useAdminRoutes } from 'component/admin/useAdminRoutes';
import InviteLinkButton from './InviteLink/InviteLinkButton/InviteLinkButton';
import { useUiFlag } from 'hooks/useUiFlag';
import { CommandBar } from 'component/commandBar/CommandBar';
-import LinearScaleIcon from '@mui/icons-material/LinearScale';
-import { useEventTimelineContext } from 'component/events/EventTimeline/EventTimelineContext';
-import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
+import { HeaderEventTimelineButton } from './HeaderEventTimelineButton';
const HeaderComponent = styled(AppBar)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
@@ -109,10 +107,6 @@ const Header = () => {
const [openDrawer, setOpenDrawer] = useState(false);
const toggleDrawer = () => setOpenDrawer((prev) => !prev);
const celebatoryUnleash = useUiFlag('celebrateUnleash');
- const eventTimeline = useUiFlag('eventTimeline') && !isOss();
- const { open: showTimeline, setOpen: setShowTimeline } =
- useEventTimelineContext();
- const { trackEvent } = usePlausibleTracker();
const routes = getRoutes();
const adminRoutes = useAdminRoutes();
@@ -187,35 +181,7 @@ const Header = () => {
-
- {
- trackEvent('event-timeline', {
- props: {
- eventType: showTimeline
- ? 'close'
- : 'open',
- },
- });
- setShowTimeline(!showTimeline);
- }}
- size='large'
- >
-
-
-
- }
- />
+
prop !== 'highlighted',
+})<{
+ component?: 'a' | 'button';
+ href?: string;
+ target?: string;
+ highlighted?: boolean;
+}>(({ theme, highlighted }) => ({
+ animation: highlighted ? 'pulse 1.5s infinite linear' : 'none',
+ zIndex: highlighted ? theme.zIndex.tooltip : 'auto',
+ '@keyframes pulse': {
+ '0%': {
+ boxShadow: `0 0 0 0px ${alpha(theme.palette.primary.main, 0.5)}`,
+ transform: 'scale(1)',
+ },
+ '50%': {
+ boxShadow: `0 0 0 15px ${alpha(theme.palette.primary.main, 0.2)}`,
+ transform: 'scale(1.1)',
+ },
+ '100%': {
+ boxShadow: `0 0 0 30px ${alpha(theme.palette.primary.main, 0)}`,
+ transform: 'scale(1)',
+ },
+ },
+}));
+
+export const HeaderEventTimelineButton = () => {
+ const { trackEvent } = usePlausibleTracker();
+ const { isOss } = useUiConfig();
+ const eventTimeline = useUiFlag('eventTimeline') && !isOss();
+ const {
+ open: showTimeline,
+ setOpen: setShowTimeline,
+ highlighted,
+ } = useEventTimelineContext();
+
+ if (!eventTimeline) return null;
+
+ return (
+
+ {
+ trackEvent('event-timeline', {
+ props: {
+ eventType: showTimeline ? 'close' : 'open',
+ },
+ });
+ setShowTimeline(!showTimeline);
+ }}
+ size='large'
+ >
+
+
+
+ );
+};
diff --git a/frontend/src/component/menu/Header/OldHeader.tsx b/frontend/src/component/menu/Header/OldHeader.tsx
index 8ff9d7975112..d156f6c95d46 100644
--- a/frontend/src/component/menu/Header/OldHeader.tsx
+++ b/frontend/src/component/menu/Header/OldHeader.tsx
@@ -36,8 +36,7 @@ import { Notifications } from 'component/common/Notifications/Notifications';
import { useAdminRoutes } from 'component/admin/useAdminRoutes';
import InviteLinkButton from './InviteLink/InviteLinkButton/InviteLinkButton';
import { useUiFlag } from 'hooks/useUiFlag';
-import LinearScaleIcon from '@mui/icons-material/LinearScale';
-import { useEventTimelineContext } from 'component/events/EventTimeline/EventTimelineContext';
+import { HeaderEventTimelineButton } from './HeaderEventTimelineButton';
const HeaderComponent = styled(AppBar)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
@@ -148,9 +147,6 @@ const OldHeader = () => {
const onAdminClose = () => setAdminRef(null);
const onConfigureClose = () => setConfigRef(null);
const celebatoryUnleash = useUiFlag('celebrateUnleash');
- const eventTimeline = useUiFlag('eventTimeline') && !isOss();
- const { open: showTimeline, setOpen: setShowTimeline } =
- useEventTimelineContext();
const routes = getRoutes();
const adminRoutes = useAdminRoutes();
@@ -250,28 +246,7 @@ const OldHeader = () => {
/>
-
-
- setShowTimeline(!showTimeline)
- }
- size='large'
- >
-
-
-
- }
- />
+