1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

chore: timeline spike

This commit is contained in:
Nuno Góis 2024-09-03 16:58:57 +01:00
parent 82f9783fe6
commit 98982cfc4a
No known key found for this signature in database
GPG Key ID: 71ECC689F1091765
2 changed files with 306 additions and 78 deletions

View File

@ -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 <ToggleOnIcon />;
case 'feature-environment-disabled':
return <ToggleOffIcon />;
default:
return null;
}
};
const getEventTooltip = (event: EventSchema, locale: string) => {
if (event.type === 'feature-environment-enabled') {
return (
<div>
<small>{formatDateYMDHMS(event.createdAt, locale)}</small>
<p>
{event.createdBy} enabled {event.featureName} for the{' '}
{event.environment} environment in project {event.project}
</p>
</div>
);
}
if (event.type === 'feature-environment-disabled') {
return (
<div>
<small>{formatDateYMDHMS(event.createdAt, locale)}</small>
<p>
{event.createdBy} disabled {event.featureName} for the{' '}
{event.environment} environment in project {event.project}
</p>
</div>
);
}
return (
<div>
<div>{formatDateYMDHMS(event.createdAt, locale)}</div>
<div>{event.createdBy}</div>
<div>{event.type}</div>
<div>{event.featureName}</div>
<div>{event.environment}</div>
</div>
);
};
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 (
<StyledArcherContainer
strokeColor={theme.palette.text.primary}
endMarker={false}
>
<StyledTimelineContainer>
<ArcherElement
id='start'
relations={[
{
targetId: sortedEvents[0]?.id.toString() ?? 'end',
targetAnchor: 'left',
sourceAnchor: 'right',
style: {
strokeColor: theme.palette.secondary.border,
},
},
]}
>
<StyledStart>
<StyledNonEvent />
<ConditionallyRender
condition={false}
show={<span>show</span>}
/>
</StyledStart>
</ArcherElement>
{sortedEvents.map((event, i) => (
<ArcherElement
key={event.id}
id={event.id.toString()}
relations={[
{
targetId:
sortedEvents[i + 1]?.id.toString() ?? 'end',
targetAnchor: 'left',
sourceAnchor: 'right',
style: {
strokeColor: theme.palette.secondary.border,
},
},
]}
>
<StyledEvent
position={calculatePosition(event.createdAt)}
>
<HtmlTooltip
title={getEventTooltip(
event,
locationSettings.locale,
)}
arrow
>
<StyledEventCircle>
{getEventIcon(event.type)}
</StyledEventCircle>
</HtmlTooltip>
</StyledEvent>
</ArcherElement>
))}
<ArcherElement id='end'>
<StyledEnd>
<StyledNonEvent />
</StyledEnd>
</ArcherElement>
</StyledTimelineContainer>
</StyledArcherContainer>
);
};

View File

@ -18,6 +18,7 @@ import { ProjectCreationButton } from './ProjectCreationButton/ProjectCreationBu
import { useGroupedProjects } from './hooks/useGroupedProjects'; import { useGroupedProjects } from './hooks/useGroupedProjects';
import { useProjectsSearchAndSort } from './hooks/useProjectsSearchAndSort'; import { useProjectsSearchAndSort } from './hooks/useProjectsSearchAndSort';
import { ProjectArchiveLink } from './ProjectArchiveLink/ProjectArchiveLink'; import { ProjectArchiveLink } from './ProjectArchiveLink/ProjectArchiveLink';
import { EventTimeline } from 'component/common/EventTimeline/EventTimeline';
const StyledApiError = styled(ApiError)(({ theme }) => ({ const StyledApiError = styled(ApiError)(({ theme }) => ({
maxWidth: '500px', maxWidth: '500px',
@ -30,6 +31,12 @@ const StyledContainer = styled('div')(({ theme }) => ({
gap: theme.spacing(6), gap: theme.spacing(6),
})); }));
const StyledEventTimelineContainer = styled('div')(({ theme }) => ({
marginBottom: theme.spacing(4),
display: 'flex',
justifyContent: 'center',
}));
const NewProjectList = () => { const NewProjectList = () => {
const { projects, loading, error, refetch } = useProjects(); const { projects, loading, error, refetch } = useProjects();
@ -58,90 +65,95 @@ const NewProjectList = () => {
: projects.length; : projects.length;
return ( return (
<PageContent <>
isLoading={loading} <StyledEventTimelineContainer>
header={ <EventTimeline />
<PageHeader </StyledEventTimelineContainer>
title={`Projects (${projectCount})`} <PageContent
actions={ isLoading={loading}
<> header={
<ConditionallyRender <PageHeader
condition={!isSmallScreen} title={`Projects (${projectCount})`}
show={ actions={
<> <>
<Search <ConditionallyRender
initialValue={state.query || ''} condition={!isSmallScreen}
onChange={setSearchValue} show={
/> <>
<PageHeader.Divider /> <Search
</> initialValue={state.query || ''}
} onChange={setSearchValue}
/> />
<PageHeader.Divider />
</>
}
/>
<ConditionallyRender <ConditionallyRender
condition={Boolean(archiveProjectsEnabled)} condition={Boolean(archiveProjectsEnabled)}
show={<ProjectArchiveLink />} show={<ProjectArchiveLink />}
/> />
<ProjectCreationButton <ProjectCreationButton
isDialogOpen={Boolean(state.create)} isDialogOpen={Boolean(state.create)}
setIsDialogOpen={(create) => setIsDialogOpen={(create) =>
setState({ setState({
create: create ? 'true' : undefined, create: create ? 'true' : undefined,
}) })
} }
/> />
</> </>
}
>
<ConditionallyRender
condition={isSmallScreen}
show={
<Search
initialValue={state.query || ''}
onChange={setSearchValue}
/>
} }
/> >
</PageHeader> <ConditionallyRender
} condition={isSmallScreen}
> show={
<StyledContainer> <Search
<ConditionallyRender initialValue={state.query || ''}
condition={error} onChange={setSearchValue}
show={() => ( />
<StyledApiError }
onClick={refetch}
text='Error fetching projects'
/> />
)} </PageHeader>
/> }
<SearchHighlightProvider value={state.query || ''}> >
<ProjectGroup <StyledContainer>
sectionTitle='My projects' <ConditionallyRender
sectionSubtitle='Favorite projects, projects you own or projects you are a member of.' condition={error}
HeaderActions={ show={() => (
<ProjectsListSort <StyledApiError
sortBy={state.sortBy} onClick={refetch}
setSortBy={(sortBy) => text='Error fetching projects'
setState({
sortBy: sortBy as typeof state.sortBy,
})
}
/> />
} )}
loading={loading}
projects={groupedProjects.myProjects}
/> />
<SearchHighlightProvider value={state.query || ''}>
<ProjectGroup
sectionTitle='My projects'
sectionSubtitle='Favorite projects, projects you own or projects you are a member of.'
HeaderActions={
<ProjectsListSort
sortBy={state.sortBy}
setSortBy={(sortBy) =>
setState({
sortBy: sortBy as typeof state.sortBy,
})
}
/>
}
loading={loading}
projects={groupedProjects.myProjects}
/>
<ProjectGroup <ProjectGroup
sectionTitle='Other projects' sectionTitle='Other projects'
sectionSubtitle='Projects in Unleash that you have access to.' sectionSubtitle='Projects in Unleash that you have access to.'
loading={loading} loading={loading}
projects={groupedProjects.otherProjects} projects={groupedProjects.otherProjects}
/> />
</SearchHighlightProvider> </SearchHighlightProvider>
</StyledContainer> </StyledContainer>
</PageContent> </PageContent>
</>
); );
}; };