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 { 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 (
<PageContent
isLoading={loading}
header={
<PageHeader
title={`Projects (${projectCount})`}
actions={
<>
<ConditionallyRender
condition={!isSmallScreen}
show={
<>
<Search
initialValue={state.query || ''}
onChange={setSearchValue}
/>
<PageHeader.Divider />
</>
}
/>
<>
<StyledEventTimelineContainer>
<EventTimeline />
</StyledEventTimelineContainer>
<PageContent
isLoading={loading}
header={
<PageHeader
title={`Projects (${projectCount})`}
actions={
<>
<ConditionallyRender
condition={!isSmallScreen}
show={
<>
<Search
initialValue={state.query || ''}
onChange={setSearchValue}
/>
<PageHeader.Divider />
</>
}
/>
<ConditionallyRender
condition={Boolean(archiveProjectsEnabled)}
show={<ProjectArchiveLink />}
/>
<ProjectCreationButton
isDialogOpen={Boolean(state.create)}
setIsDialogOpen={(create) =>
setState({
create: create ? 'true' : undefined,
})
}
/>
</>
}
>
<ConditionallyRender
condition={isSmallScreen}
show={
<Search
initialValue={state.query || ''}
onChange={setSearchValue}
/>
<ConditionallyRender
condition={Boolean(archiveProjectsEnabled)}
show={<ProjectArchiveLink />}
/>
<ProjectCreationButton
isDialogOpen={Boolean(state.create)}
setIsDialogOpen={(create) =>
setState({
create: create ? 'true' : undefined,
})
}
/>
</>
}
/>
</PageHeader>
}
>
<StyledContainer>
<ConditionallyRender
condition={error}
show={() => (
<StyledApiError
onClick={refetch}
text='Error fetching projects'
>
<ConditionallyRender
condition={isSmallScreen}
show={
<Search
initialValue={state.query || ''}
onChange={setSearchValue}
/>
}
/>
)}
/>
<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,
})
}
</PageHeader>
}
>
<StyledContainer>
<ConditionallyRender
condition={error}
show={() => (
<StyledApiError
onClick={refetch}
text='Error fetching projects'
/>
}
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
sectionTitle='Other projects'
sectionSubtitle='Projects in Unleash that you have access to.'
loading={loading}
projects={groupedProjects.otherProjects}
/>
</SearchHighlightProvider>
</StyledContainer>
</PageContent>
<ProjectGroup
sectionTitle='Other projects'
sectionSubtitle='Projects in Unleash that you have access to.'
loading={loading}
projects={groupedProjects.otherProjects}
/>
</SearchHighlightProvider>
</StyledContainer>
</PageContent>
</>
);
};