From a46fee9137fd8ca257b08ec6bc3b8d97ef6153fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Thu, 3 Oct 2024 12:49:28 +0100 Subject: [PATCH 1/5] chore: add event timeline to new in unleash --- frontend/src/assets/img/eventTimeline.svg | 11 ++ .../EventTimeline/EventTimelineProvider.tsx | 47 ++++-- .../MainLayout/MainLayoutEventTimeline.tsx | 30 +++- .../NewInUnleash/NewInUnleash.tsx | 51 ++++++- .../NewInUnleash/NewInUnleashItem.tsx | 6 +- .../NewInUnleash/NewInUnleashTooltip.tsx | 136 +++++++++--------- 6 files changed, 191 insertions(+), 90 deletions(-) create mode 100644 frontend/src/assets/img/eventTimeline.svg diff --git a/frontend/src/assets/img/eventTimeline.svg b/frontend/src/assets/img/eventTimeline.svg new file mode 100644 index 000000000000..b36d84c5a61f --- /dev/null +++ b/frontend/src/assets/img/eventTimeline.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/src/component/events/EventTimeline/EventTimelineProvider.tsx b/frontend/src/component/events/EventTimeline/EventTimelineProvider.tsx index da3714ea6baf..968165610c23 100644 --- a/frontend/src/component/events/EventTimeline/EventTimelineProvider.tsx +++ b/frontend/src/component/events/EventTimeline/EventTimelineProvider.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from 'react'; +import { useState, type ReactNode } from 'react'; import { EventTimelineContext } from './EventTimelineContext'; import { useLocalStorageState } from 'hooks/useLocalStorageState'; import type { IEnvironment } from 'interfaces/environments'; @@ -10,22 +10,31 @@ type TimeSpanOption = { markers: string[]; }; -type EventTimelineState = { +type EventTimelinePersistentState = { open: boolean; timeSpan: TimeSpanOption; environment?: IEnvironment; signalsSuggestionSeen?: boolean; }; +type EventTimelineTemporaryState = { + highlighted: boolean; +}; + +type OpenOptions = { + highlight?: boolean; +}; + type EventTimelineStateSetters = { - setOpen: (open: boolean) => void; + setOpen: (open: boolean, options?: OpenOptions) => void; setTimeSpan: (timeSpan: TimeSpanOption) => void; setEnvironment: (environment: IEnvironment) => void; setSignalsSuggestionSeen: (seen: boolean) => void; }; export interface IEventTimelineContext - extends EventTimelineState, + extends EventTimelinePersistentState, + EventTimelineTemporaryState, EventTimelineStateSetters {} export const timeSpanOptions: TimeSpanOption[] = [ @@ -77,7 +86,7 @@ export const timeSpanOptions: TimeSpanOption[] = [ }, ]; -const defaultState: EventTimelineState = { +const defaultState: EventTimelinePersistentState = { open: false, timeSpan: timeSpanOptions[0], }; @@ -89,21 +98,35 @@ interface IEventTimelineProviderProps { export const EventTimelineProvider = ({ children, }: IEventTimelineProviderProps) => { - const [state, setState] = useLocalStorageState( - 'event-timeline:v1', - defaultState, - ); + const [state, setState] = + useLocalStorageState( + 'event-timeline:v1', + defaultState, + ); + const [highlighted, setHighlighted] = useState(false); - const setField = ( + const setField = ( key: K, - value: EventTimelineState[K], + value: EventTimelinePersistentState[K], ) => { setState((prevState) => ({ ...prevState, [key]: value })); }; + const setOpen = ( + open: boolean, + { highlight = false }: OpenOptions = {}, + ) => { + setField('open', open); + setHighlighted(highlight); + if (highlight) { + setTimeout(() => setHighlighted(false), 2000); + } + }; + const contextValue: IEventTimelineContext = { ...state, - setOpen: (open: boolean) => setField('open', open), + highlighted, + setOpen, setTimeSpan: (timeSpan: TimeSpanOption) => setField('timeSpan', timeSpan), setEnvironment: (environment: IEnvironment) => diff --git a/frontend/src/component/layout/MainLayout/MainLayoutEventTimeline.tsx b/frontend/src/component/layout/MainLayout/MainLayoutEventTimeline.tsx index e50ebe81d212..a38af5e274da 100644 --- a/frontend/src/component/layout/MainLayout/MainLayoutEventTimeline.tsx +++ b/frontend/src/component/layout/MainLayout/MainLayoutEventTimeline.tsx @@ -1,4 +1,4 @@ -import { Box, styled } from '@mui/material'; +import { alpha, Box, styled } from '@mui/material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { EventTimeline } from 'component/events/EventTimeline/EventTimeline'; import { useEventTimelineContext } from 'component/events/EventTimeline/EventTimelineContext'; @@ -6,12 +6,35 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { useUiFlag } from 'hooks/useUiFlag'; import { useEffect, useState } from 'react'; -const StyledEventTimelineSlider = styled(Box)(({ theme }) => ({ +const StyledEventTimelineSlider = styled(Box, { + shouldForwardProp: (prop) => prop !== 'variant', +})<{ highlighted?: boolean }>(({ theme, highlighted }) => ({ backgroundColor: theme.palette.background.paper, height: '120px', overflow: 'hidden', boxShadow: theme.boxShadows.popup, borderLeft: `1px solid ${theme.palette.background.application}`, + animation: highlighted ? 'highlight 1s infinite' : 'none', + willChange: 'transform, box-shadow, opacity', + '@keyframes highlight': { + '0%': { + zIndex: theme.zIndex.tooltip, + transform: 'scale(1)', + opacity: 1, + }, + '50%': { + zIndex: theme.zIndex.tooltip, + 'box-shadow': `0 0 30px ${alpha(theme.palette.primary.main, 0.8)}`, + transform: 'scale(1.02)', + opacity: 0.8, + }, + '100%': { + zIndex: theme.zIndex.tooltip, + 'box-shadow': theme.boxShadows.popup, + transform: 'scale(1)', + opacity: 1, + }, + }, })); const StyledEventTimelineWrapper = styled(Box)(({ theme }) => ({ @@ -20,7 +43,7 @@ const StyledEventTimelineWrapper = styled(Box)(({ theme }) => ({ export const MainLayoutEventTimeline = () => { const { isOss } = useUiConfig(); - const { open: showTimeline } = useEventTimelineContext(); + const { open: showTimeline, highlighted } = useEventTimelineContext(); const eventTimelineEnabled = useUiFlag('eventTimeline') && !isOss(); const [isInitialLoad, setIsInitialLoad] = useState(true); @@ -32,6 +55,7 @@ export const MainLayoutEventTimeline = () => { return ( ({ 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 { setOpen: showTimeline } = 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: () => { + showTimeline(true, { highlight: 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} + +); From 21c2999b185402696d9238ca8729a3eb367754e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Thu, 3 Oct 2024 14:47:49 +0100 Subject: [PATCH 2/5] fix: move event timeline provider next to remaining providers, so it's more global --- .../src/component/layout/MainLayout/MainLayout.tsx | 5 ++--- frontend/src/index.tsx | 11 +++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/frontend/src/component/layout/MainLayout/MainLayout.tsx b/frontend/src/component/layout/MainLayout/MainLayout.tsx index 88460c8ebbe3..76c4074580ec 100644 --- a/frontend/src/component/layout/MainLayout/MainLayout.tsx +++ b/frontend/src/component/layout/MainLayout/MainLayout.tsx @@ -18,7 +18,6 @@ import { ThemeMode } from 'component/common/ThemeMode/ThemeMode'; import { NavigationSidebar } from './NavigationSidebar/NavigationSidebar'; import { useUiFlag } from 'hooks/useUiFlag'; import { MainLayoutEventTimeline } from './MainLayoutEventTimeline'; -import { EventTimelineProvider } from 'component/events/EventTimeline/EventTimelineProvider'; interface IMainLayoutProps { children: ReactNode; @@ -126,7 +125,7 @@ export const MainLayout = forwardRef( const isSmallScreen = useMediaQuery(theme.breakpoints.down('lg')); return ( - + <> (