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:
parent
82f9783fe6
commit
98982cfc4a
216
frontend/src/component/common/EventTimeline/EventTimeline.tsx
Normal file
216
frontend/src/component/common/EventTimeline/EventTimeline.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user