From 98982cfc4a6a54a0bbfb3488950c667b9bf4de7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Tue, 3 Sep 2024 16:58:57 +0100 Subject: [PATCH] chore: timeline spike --- .../common/EventTimeline/EventTimeline.tsx | 216 ++++++++++++++++++ .../project/ProjectList/ProjectList.tsx | 168 +++++++------- 2 files changed, 306 insertions(+), 78 deletions(-) create mode 100644 frontend/src/component/common/EventTimeline/EventTimeline.tsx diff --git a/frontend/src/component/common/EventTimeline/EventTimeline.tsx b/frontend/src/component/common/EventTimeline/EventTimeline.tsx new file mode 100644 index 0000000000..3091f9b5d9 --- /dev/null +++ b/frontend/src/component/common/EventTimeline/EventTimeline.tsx @@ -0,0 +1,216 @@ +import { styled, useTheme } from '@mui/material'; +import { useEventSearch } from 'hooks/api/getters/useEventSearch/useEventSearch'; +import type { EventSchema, EventSchemaType } from 'openapi'; +import { ArcherContainer, ArcherElement } from 'react-archer'; +import ToggleOnIcon from '@mui/icons-material/ToggleOn'; +import ToggleOffIcon from '@mui/icons-material/ToggleOff'; +import { HtmlTooltip } from '../HtmlTooltip/HtmlTooltip'; +import { formatDateYMDHMS } from 'utils/formatDate'; +import { useLocationSettings } from 'hooks/useLocationSettings'; +import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender'; +import { startOfDay } from 'date-fns'; + +const StyledArcherContainer = styled(ArcherContainer)({ + width: '100%', + height: '100%', +}); + +const StyledTimelineContainer = styled('div')(({ theme }) => ({ + position: 'relative', + height: theme.spacing(1), + width: '100%', + display: 'flex', + alignItems: 'center', +})); + +const StyledEventContainer = styled('div')({ + position: 'absolute', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', +}); + +const StyledNonEvent = styled('div')(({ theme }) => ({ + height: theme.spacing(0.25), + width: theme.spacing(0.25), + backgroundColor: theme.palette.secondary.border, +})); + +const StyledEvent = styled(StyledEventContainer, { + shouldForwardProp: (prop) => prop !== 'position', +})<{ position: string }>(({ theme, position }) => ({ + left: position, + transform: 'translateX(-100%)', + padding: theme.spacing(0, 0.25), + zIndex: 1, +})); + +const StyledEventCircle = styled('div')(({ theme }) => ({ + height: theme.spacing(2.25), + width: theme.spacing(2.25), + borderRadius: '50%', + backgroundColor: theme.palette.primary.main, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + transition: 'transform 0.2s', + '& svg': { + color: theme.palette.primary.contrastText, + height: theme.spacing(2), + width: theme.spacing(2), + }, + '&:hover': { + transform: 'scale(1.5)', + }, +})); + +const StyledStart = styled(StyledEventContainer)(({ theme }) => ({ + height: theme.spacing(0.25), + width: theme.spacing(0.25), + left: 0, +})); + +const StyledEnd = styled(StyledEventContainer)(({ theme }) => ({ + height: theme.spacing(0.25), + width: theme.spacing(0.25), + right: 0, +})); + +const getEventIcon = (type: EventSchemaType) => { + switch (type) { + case 'feature-environment-enabled': + return ; + case 'feature-environment-disabled': + return ; + default: + return null; + } +}; + +const getEventTooltip = (event: EventSchema, locale: string) => { + if (event.type === 'feature-environment-enabled') { + return ( +
+ {formatDateYMDHMS(event.createdAt, locale)} +

+ {event.createdBy} enabled {event.featureName} for the{' '} + {event.environment} environment in project {event.project} +

+
+ ); + } + if (event.type === 'feature-environment-disabled') { + return ( +
+ {formatDateYMDHMS(event.createdAt, locale)} +

+ {event.createdBy} disabled {event.featureName} for the{' '} + {event.environment} environment in project {event.project} +

+
+ ); + } + + return ( +
+
{formatDateYMDHMS(event.createdAt, locale)}
+
{event.createdBy}
+
{event.type}
+
{event.featureName}
+
{event.environment}
+
+ ); +}; + +export const EventTimeline: React.FC = () => { + const { locationSettings } = useLocationSettings(); + const theme = useTheme(); + + const endDate = new Date(); + const startDate = startOfDay(endDate); + + const { events } = useEventSearch({ + from: `IS:${startDate.toISOString().split('T')[0]}`, + to: `IS:${endDate.toISOString().split('T')[0]}`, + }); + + const sortedEvents = [...events].reverse(); + + const timelineDuration = endDate.getTime() - startDate.getTime(); + + const calculatePosition = (eventDate: string): string => { + const eventTime = new Date(eventDate).getTime(); + const positionPercentage = + ((eventTime - startDate.getTime()) / timelineDuration) * 100; + return `${positionPercentage}%`; + }; + + return ( + + + + + + show} + /> + + + {sortedEvents.map((event, i) => ( + + + + + {getEventIcon(event.type)} + + + + + ))} + + + + + + + + ); +}; diff --git a/frontend/src/component/project/ProjectList/ProjectList.tsx b/frontend/src/component/project/ProjectList/ProjectList.tsx index 4c3acbb40f..273d9bdae5 100644 --- a/frontend/src/component/project/ProjectList/ProjectList.tsx +++ b/frontend/src/component/project/ProjectList/ProjectList.tsx @@ -18,6 +18,7 @@ import { ProjectCreationButton } from './ProjectCreationButton/ProjectCreationBu import { useGroupedProjects } from './hooks/useGroupedProjects'; import { useProjectsSearchAndSort } from './hooks/useProjectsSearchAndSort'; import { ProjectArchiveLink } from './ProjectArchiveLink/ProjectArchiveLink'; +import { EventTimeline } from 'component/common/EventTimeline/EventTimeline'; const StyledApiError = styled(ApiError)(({ theme }) => ({ maxWidth: '500px', @@ -30,6 +31,12 @@ const StyledContainer = styled('div')(({ theme }) => ({ gap: theme.spacing(6), })); +const StyledEventTimelineContainer = styled('div')(({ theme }) => ({ + marginBottom: theme.spacing(4), + display: 'flex', + justifyContent: 'center', +})); + const NewProjectList = () => { const { projects, loading, error, refetch } = useProjects(); @@ -58,90 +65,95 @@ const NewProjectList = () => { : projects.length; return ( - - - - - - } - /> + <> + + + + + + + + + } + /> - } - /> - - setState({ - create: create ? 'true' : undefined, - }) - } - /> - - } - > - + } + /> + + setState({ + create: create ? 'true' : undefined, + }) + } + /> + } - /> - - } - > - - ( - + + } /> - )} - /> - - - setState({ - sortBy: sortBy as typeof state.sortBy, - }) - } + + } + > + + ( + - } - loading={loading} - projects={groupedProjects.myProjects} + )} /> + + + setState({ + sortBy: sortBy as typeof state.sortBy, + }) + } + /> + } + loading={loading} + projects={groupedProjects.myProjects} + /> - - - - + + + + + ); };