1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-04 00:18:01 +01:00

refactor: implement an event timeline context and provider (#8321)

https://linear.app/unleash/issue/2-2730/refactor-the-event-timeline-state-management-to-a-context-and-provider

This PR refactors the state management for the **Event Timeline**
component by introducing a context and provider to improve accessibility
of state across the component tree.
This commit is contained in:
Nuno Góis 2024-10-01 16:21:31 +01:00 committed by GitHub
parent 2d8bc3268f
commit 5dae654022
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 88 additions and 118 deletions

View File

@ -4,11 +4,11 @@ import { startOfDay, sub } from 'date-fns';
import { useEventSearch } from 'hooks/api/getters/useEventSearch/useEventSearch'; import { useEventSearch } from 'hooks/api/getters/useEventSearch/useEventSearch';
import { EventTimelineEventGroup } from './EventTimelineEventGroup/EventTimelineEventGroup'; import { EventTimelineEventGroup } from './EventTimelineEventGroup/EventTimelineEventGroup';
import { EventTimelineHeader } from './EventTimelineHeader/EventTimelineHeader'; import { EventTimelineHeader } from './EventTimelineHeader/EventTimelineHeader';
import type { TimeSpanOption } from './useEventTimeline';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useSignalQuery } from 'hooks/api/getters/useSignalQuery/useSignalQuery'; import { useSignalQuery } from 'hooks/api/getters/useSignalQuery/useSignalQuery';
import type { ISignalQuerySignal } from 'interfaces/signal'; import type { ISignalQuerySignal } from 'interfaces/signal';
import type { IEnvironment } from 'interfaces/environments'; import type { IEnvironment } from 'interfaces/environments';
import { useEventTimelineContext } from './EventTimelineContext';
export type TimelineEventType = 'signal' | EventSchemaType; export type TimelineEventType = 'signal' | EventSchemaType;
@ -157,21 +157,8 @@ const getTimelineEvent = (
} }
}; };
interface IEventTimelineProps { export const EventTimeline = () => {
timeSpan: TimeSpanOption; const { timeSpan, environment } = useEventTimelineContext();
environment: IEnvironment | undefined;
setTimeSpan: (timeSpan: TimeSpanOption) => void;
setEnvironment: (environment: IEnvironment) => void;
setOpen: (open: boolean) => void;
}
export const EventTimeline = ({
timeSpan,
environment,
setTimeSpan,
setEnvironment,
setOpen,
}: IEventTimelineProps) => {
const endDate = new Date(); const endDate = new Date();
const startDate = sub(endDate, timeSpan.value); const startDate = sub(endDate, timeSpan.value);
const endTime = endDate.getTime(); const endTime = endDate.getTime();
@ -246,14 +233,7 @@ export const EventTimeline = ({
return ( return (
<> <>
<StyledRow> <StyledRow>
<EventTimelineHeader <EventTimelineHeader totalEvents={events.length} />
totalEvents={events.length}
timeSpan={timeSpan}
setTimeSpan={setTimeSpan}
environment={environment}
setEnvironment={setEnvironment}
setOpen={setOpen}
/>
</StyledRow> </StyledRow>
<StyledTimelineBody> <StyledTimelineBody>
<StyledTimelineContainer> <StyledTimelineContainer>

View File

@ -0,0 +1,18 @@
import { createContext, useContext } from 'react';
import type { IEventTimelineContext } from './EventTimelineProvider';
export const EventTimelineContext = createContext<
IEventTimelineContext | undefined
>(undefined);
export const useEventTimelineContext = (): IEventTimelineContext => {
const context = useContext(EventTimelineContext);
if (!context) {
throw new Error(
'useEventTimelineContext must be used within a EventTimelineProvider',
);
}
return context;
};

View File

@ -7,10 +7,10 @@ import {
} from '@mui/material'; } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import type { IEnvironment } from 'interfaces/environments';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { type TimeSpanOption, timeSpanOptions } from '../useEventTimeline'; import { timeSpanOptions } from '../EventTimelineProvider';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import { useEventTimelineContext } from '../EventTimelineContext';
const StyledCol = styled('div')(({ theme }) => ({ const StyledCol = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -36,21 +36,13 @@ const StyledTimelineEventsCount = styled('span')(({ theme }) => ({
interface IEventTimelineHeaderProps { interface IEventTimelineHeaderProps {
totalEvents: number; totalEvents: number;
timeSpan: TimeSpanOption;
setTimeSpan: (timeSpan: TimeSpanOption) => void;
environment: IEnvironment | undefined;
setEnvironment: (environment: IEnvironment) => void;
setOpen: (open: boolean) => void;
} }
export const EventTimelineHeader = ({ export const EventTimelineHeader = ({
totalEvents, totalEvents,
timeSpan,
setTimeSpan,
environment,
setEnvironment,
setOpen,
}: IEventTimelineHeaderProps) => { }: IEventTimelineHeaderProps) => {
const { timeSpan, environment, setOpen, setTimeSpan, setEnvironment } =
useEventTimelineContext();
const { environments } = useEnvironments(); const { environments } = useEnvironments();
const activeEnvironments = useMemo( const activeEnvironments = useMemo(

View File

@ -1,13 +1,33 @@
import type { ReactNode } from 'react';
import { EventTimelineContext } from './EventTimelineContext';
import { useLocalStorageState } from 'hooks/useLocalStorageState'; import { useLocalStorageState } from 'hooks/useLocalStorageState';
import type { IEnvironment } from 'interfaces/environments'; import type { IEnvironment } from 'interfaces/environments';
export type TimeSpanOption = { type TimeSpanOption = {
key: string; key: string;
label: string; label: string;
value: Duration; value: Duration;
markers: string[]; markers: string[];
}; };
type EventTimelineState = {
open: boolean;
timeSpan: TimeSpanOption;
environment?: IEnvironment;
signalsAlertSeen?: boolean;
};
type EventTimelineStateSetters = {
setOpen: (open: boolean) => void;
setTimeSpan: (timeSpan: TimeSpanOption) => void;
setEnvironment: (environment: IEnvironment) => void;
setSignalsAlertSeen: (seen: boolean) => void;
};
export interface IEventTimelineContext
extends EventTimelineState,
EventTimelineStateSetters {}
export const timeSpanOptions: TimeSpanOption[] = [ export const timeSpanOptions: TimeSpanOption[] = [
{ {
key: '30m', key: '30m',
@ -57,18 +77,18 @@ export const timeSpanOptions: TimeSpanOption[] = [
}, },
]; ];
type EventTimelineState = {
open: boolean;
timeSpan: TimeSpanOption;
environment?: IEnvironment;
};
const defaultState: EventTimelineState = { const defaultState: EventTimelineState = {
open: false, open: false,
timeSpan: timeSpanOptions[0], timeSpan: timeSpanOptions[0],
}; };
export const useEventTimeline = () => { interface IEventTimelineProviderProps {
children: ReactNode;
}
export const EventTimelineProvider = ({
children,
}: IEventTimelineProviderProps) => {
const [state, setState] = useLocalStorageState<EventTimelineState>( const [state, setState] = useLocalStorageState<EventTimelineState>(
'event-timeline:v1', 'event-timeline:v1',
defaultState, defaultState,
@ -81,12 +101,20 @@ export const useEventTimeline = () => {
setState((prevState) => ({ ...prevState, [key]: value })); setState((prevState) => ({ ...prevState, [key]: value }));
}; };
return { const contextValue: IEventTimelineContext = {
...state, ...state,
setOpen: (open: boolean) => setField('open', open), setOpen: (open: boolean) => setField('open', open),
setTimeSpan: (timeSpan: TimeSpanOption) => setTimeSpan: (timeSpan: TimeSpanOption) =>
setField('timeSpan', timeSpan), setField('timeSpan', timeSpan),
setEnvironment: (environment: IEnvironment) => setEnvironment: (environment: IEnvironment) =>
setField('environment', environment), setField('environment', environment),
setSignalsAlertSeen: (seen: boolean) =>
setField('signalsAlertSeen', seen),
}; };
return (
<EventTimelineContext.Provider value={contextValue}>
{children}
</EventTimelineContext.Provider>
);
}; };

View File

@ -17,8 +17,8 @@ import { DraftBanner } from './DraftBanner/DraftBanner';
import { ThemeMode } from 'component/common/ThemeMode/ThemeMode'; import { ThemeMode } from 'component/common/ThemeMode/ThemeMode';
import { NavigationSidebar } from './NavigationSidebar/NavigationSidebar'; import { NavigationSidebar } from './NavigationSidebar/NavigationSidebar';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
import { useEventTimeline } from 'component/events/EventTimeline/useEventTimeline';
import { MainLayoutEventTimeline } from './MainLayoutEventTimeline'; import { MainLayoutEventTimeline } from './MainLayoutEventTimeline';
import { EventTimelineProvider } from 'component/events/EventTimeline/EventTimelineProvider';
interface IMainLayoutProps { interface IMainLayoutProps {
children: ReactNode; children: ReactNode;
@ -112,20 +112,11 @@ const MainLayoutContentContainer = styled('div')(({ theme }) => ({
export const MainLayout = forwardRef<HTMLDivElement, IMainLayoutProps>( export const MainLayout = forwardRef<HTMLDivElement, IMainLayoutProps>(
({ children }, ref) => { ({ children }, ref) => {
const { uiConfig, isOss } = useUiConfig(); const { uiConfig } = useUiConfig();
const projectId = useOptionalPathParam('projectId'); const projectId = useOptionalPathParam('projectId');
const { isChangeRequestConfiguredInAnyEnv } = useChangeRequestsEnabled( const { isChangeRequestConfiguredInAnyEnv } = useChangeRequestsEnabled(
projectId || '', projectId || '',
); );
const eventTimeline = useUiFlag('eventTimeline') && !isOss();
const {
open: showTimeline,
timeSpan,
environment,
setOpen: setShowTimeline,
setTimeSpan,
setEnvironment,
} = useEventTimeline();
const sidebarNavigationEnabled = useUiFlag('navigationSidebar'); const sidebarNavigationEnabled = useUiFlag('navigationSidebar');
const StyledMainLayoutContent = sidebarNavigationEnabled const StyledMainLayoutContent = sidebarNavigationEnabled
@ -135,22 +126,12 @@ export const MainLayout = forwardRef<HTMLDivElement, IMainLayoutProps>(
const isSmallScreen = useMediaQuery(theme.breakpoints.down('lg')); const isSmallScreen = useMediaQuery(theme.breakpoints.down('lg'));
return ( return (
<> <EventTimelineProvider>
<SkipNavLink /> <SkipNavLink />
<ConditionallyRender <ConditionallyRender
condition={sidebarNavigationEnabled} condition={sidebarNavigationEnabled}
show={ show={<Header />}
<Header elseShow={<OldHeader />}
showTimeline={showTimeline}
setShowTimeline={setShowTimeline}
/>
}
elseShow={
<OldHeader
showTimeline={showTimeline}
setShowTimeline={setShowTimeline}
/>
}
/> />
<SkipNavTarget /> <SkipNavTarget />
@ -185,14 +166,7 @@ export const MainLayout = forwardRef<HTMLDivElement, IMainLayoutProps>(
minWidth: 0, minWidth: 0,
}} }}
> >
<MainLayoutEventTimeline <MainLayoutEventTimeline />
open={eventTimeline && showTimeline}
setOpen={setShowTimeline}
timeSpan={timeSpan}
setTimeSpan={setTimeSpan}
environment={environment}
setEnvironment={setEnvironment}
/>
<StyledMainLayoutContent> <StyledMainLayoutContent>
<MainLayoutContentContainer ref={ref}> <MainLayoutContentContainer ref={ref}>
@ -222,7 +196,7 @@ export const MainLayout = forwardRef<HTMLDivElement, IMainLayoutProps>(
</MainLayoutContentWrapper> </MainLayoutContentWrapper>
<Footer /> <Footer />
</MainLayoutContainer> </MainLayoutContainer>
</> </EventTimelineProvider>
); );
}, },
); );

View File

@ -1,8 +1,9 @@
import { Box, styled } from '@mui/material'; import { Box, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { EventTimeline } from 'component/events/EventTimeline/EventTimeline'; import { EventTimeline } from 'component/events/EventTimeline/EventTimeline';
import type { TimeSpanOption } from 'component/events/EventTimeline/useEventTimeline'; import { useEventTimelineContext } from 'component/events/EventTimeline/EventTimelineContext';
import type { IEnvironment } from 'interfaces/environments'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useUiFlag } from 'hooks/useUiFlag';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
const StyledEventTimelineSlider = styled(Box)(({ theme }) => ({ const StyledEventTimelineSlider = styled(Box)(({ theme }) => ({
@ -17,25 +18,14 @@ const StyledEventTimelineWrapper = styled(Box)(({ theme }) => ({
padding: theme.spacing(1.5, 2), padding: theme.spacing(1.5, 2),
})); }));
interface IMainLayoutEventTimelineProps { export const MainLayoutEventTimeline = () => {
open: boolean; const { isOss } = useUiConfig();
timeSpan: TimeSpanOption; const { open: showTimeline } = useEventTimelineContext();
environment: IEnvironment | undefined; const eventTimelineEnabled = useUiFlag('eventTimeline') && !isOss();
setTimeSpan: (timeSpan: TimeSpanOption) => void;
setEnvironment: (environment: IEnvironment) => void;
setOpen: (open: boolean) => void;
}
export const MainLayoutEventTimeline = ({
open,
timeSpan,
environment,
setTimeSpan,
setEnvironment,
setOpen,
}: IMainLayoutEventTimelineProps) => {
const [isInitialLoad, setIsInitialLoad] = useState(true); const [isInitialLoad, setIsInitialLoad] = useState(true);
const open = showTimeline && eventTimelineEnabled;
useEffect(() => { useEffect(() => {
setIsInitialLoad(false); setIsInitialLoad(false);
}, []); }, []);
@ -52,15 +42,7 @@ export const MainLayoutEventTimeline = ({
<StyledEventTimelineWrapper> <StyledEventTimelineWrapper>
<ConditionallyRender <ConditionallyRender
condition={open} condition={open}
show={ show={<EventTimeline />}
<EventTimeline
timeSpan={timeSpan}
environment={environment}
setTimeSpan={setTimeSpan}
setEnvironment={setEnvironment}
setOpen={setOpen}
/>
}
/> />
</StyledEventTimelineWrapper> </StyledEventTimelineWrapper>
</StyledEventTimelineSlider> </StyledEventTimelineSlider>

View File

@ -34,6 +34,7 @@ import InviteLinkButton from './InviteLink/InviteLinkButton/InviteLinkButton';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
import { CommandBar } from 'component/commandBar/CommandBar'; import { CommandBar } from 'component/commandBar/CommandBar';
import LinearScaleIcon from '@mui/icons-material/LinearScale'; import LinearScaleIcon from '@mui/icons-material/LinearScale';
import { useEventTimelineContext } from 'component/events/EventTimeline/EventTimelineContext';
const HeaderComponent = styled(AppBar)(({ theme }) => ({ const HeaderComponent = styled(AppBar)(({ theme }) => ({
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
@ -97,12 +98,7 @@ const StyledIconButton = styled(IconButton)<{
}, },
})); }));
interface IHeaderProps { const Header = () => {
showTimeline: boolean;
setShowTimeline: (show: boolean) => void;
}
const Header = ({ showTimeline, setShowTimeline }: IHeaderProps) => {
const { onSetThemeMode, themeMode } = useThemeMode(); const { onSetThemeMode, themeMode } = useThemeMode();
const theme = useTheme(); const theme = useTheme();
@ -113,6 +109,8 @@ const Header = ({ showTimeline, setShowTimeline }: IHeaderProps) => {
const toggleDrawer = () => setOpenDrawer((prev) => !prev); const toggleDrawer = () => setOpenDrawer((prev) => !prev);
const celebatoryUnleash = useUiFlag('celebrateUnleash'); const celebatoryUnleash = useUiFlag('celebrateUnleash');
const eventTimeline = useUiFlag('eventTimeline') && !isOss(); const eventTimeline = useUiFlag('eventTimeline') && !isOss();
const { open: showTimeline, setOpen: setShowTimeline } =
useEventTimelineContext();
const routes = getRoutes(); const routes = getRoutes();
const adminRoutes = useAdminRoutes(); const adminRoutes = useAdminRoutes();

View File

@ -37,6 +37,7 @@ import { useAdminRoutes } from 'component/admin/useAdminRoutes';
import InviteLinkButton from './InviteLink/InviteLinkButton/InviteLinkButton'; import InviteLinkButton from './InviteLink/InviteLinkButton/InviteLinkButton';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
import LinearScaleIcon from '@mui/icons-material/LinearScale'; import LinearScaleIcon from '@mui/icons-material/LinearScale';
import { useEventTimelineContext } from 'component/events/EventTimeline/EventTimelineContext';
const HeaderComponent = styled(AppBar)(({ theme }) => ({ const HeaderComponent = styled(AppBar)(({ theme }) => ({
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
@ -131,12 +132,7 @@ const StyledIconButton = styled(IconButton)<{
}, },
})); }));
interface IOldHeaderProps { const OldHeader = () => {
showTimeline: boolean;
setShowTimeline: (show: boolean) => void;
}
const OldHeader = ({ showTimeline, setShowTimeline }: IOldHeaderProps) => {
const { onSetThemeMode, themeMode } = useThemeMode(); const { onSetThemeMode, themeMode } = useThemeMode();
const theme = useTheme(); const theme = useTheme();
const adminId = useId(); const adminId = useId();
@ -153,6 +149,8 @@ const OldHeader = ({ showTimeline, setShowTimeline }: IOldHeaderProps) => {
const onConfigureClose = () => setConfigRef(null); const onConfigureClose = () => setConfigRef(null);
const celebatoryUnleash = useUiFlag('celebrateUnleash'); const celebatoryUnleash = useUiFlag('celebrateUnleash');
const eventTimeline = useUiFlag('eventTimeline') && !isOss(); const eventTimeline = useUiFlag('eventTimeline') && !isOss();
const { open: showTimeline, setOpen: setShowTimeline } =
useEventTimelineContext();
const routes = getRoutes(); const routes = getRoutes();
const adminRoutes = useAdminRoutes(); const adminRoutes = useAdminRoutes();