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

chore: timeline ux alignment (#8283)

https://linear.app/unleash/issue/2-2703/align-with-ux

Timeline UI/UX improvements after sync with UX, including:

- Added some spacing between each event in the grouping tooltip
- Aligned the x events occurred header with filter dropdown
- Improved the strategy icon somewhat so it doesn't look as off center
- New timeline icon
- Improve icon position relative to timestamp on each event in the
grouping tooltip
- Changed text color in dropdowns to a lighter gray
- Removed bold formatting in tooltip
- Adjusted paddings and margins
- Added close button
- Added shadow
- Added left border

There are a few details missing, which will be tackled in separate PRs.


![image](https://github.com/user-attachments/assets/b911696e-1a50-4968-9b73-b01af626d44e)

---------

Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
David Leek 2024-10-01 15:32:54 +02:00 committed by GitHub
parent 4d97f59e62
commit 729acfd318
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 165 additions and 72 deletions

View File

@ -4,7 +4,7 @@ import { startOfDay, sub } from 'date-fns';
import { useEventSearch } from 'hooks/api/getters/useEventSearch/useEventSearch';
import { EventTimelineEventGroup } from './EventTimelineEventGroup/EventTimelineEventGroup';
import { EventTimelineHeader } from './EventTimelineHeader/EventTimelineHeader';
import { useEventTimeline } from './useEventTimeline';
import type { TimeSpanOption } from './useEventTimeline';
import { useMemo } from 'react';
import { useSignalQuery } from 'hooks/api/getters/useSignalQuery/useSignalQuery';
import type { ISignalQuerySignal } from 'interfaces/signal';
@ -30,6 +30,12 @@ const StyledRow = styled('div')({
justifyContent: 'space-between',
});
const StyledTimelineBody = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(1, 0),
}));
const StyledTimelineContainer = styled('div')(({ theme }) => ({
position: 'relative',
height: theme.spacing(1),
@ -151,10 +157,21 @@ const getTimelineEvent = (
}
};
export const EventTimeline = () => {
const { timeSpan, environment, setTimeSpan, setEnvironment } =
useEventTimeline();
interface IEventTimelineProps {
timeSpan: TimeSpanOption;
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 startDate = sub(endDate, timeSpan.value);
const endTime = endDate.getTime();
@ -235,31 +252,34 @@ export const EventTimeline = () => {
setTimeSpan={setTimeSpan}
environment={environment}
setEnvironment={setEnvironment}
setOpen={setOpen}
/>
</StyledRow>
<StyledTimelineContainer>
<StyledTimeline />
<StyledStart />
{groups.map((group) => (
<EventTimelineEventGroup
key={group[0].id}
group={group}
startTime={startTime}
endTime={endTime}
/>
))}
<StyledEnd />
</StyledTimelineContainer>
<StyledRow>
<StyledMarkerLabel>{timeSpan.markers[0]}</StyledMarkerLabel>
{timeSpan.markers.slice(1).map((marker) => (
<StyledMiddleMarkerContainer key={marker}>
<StyledMiddleMarker />
<StyledMarkerLabel>{marker}</StyledMarkerLabel>
</StyledMiddleMarkerContainer>
))}
<StyledMarkerLabel>now</StyledMarkerLabel>
</StyledRow>
<StyledTimelineBody>
<StyledTimelineContainer>
<StyledTimeline />
<StyledStart />
{groups.map((group) => (
<EventTimelineEventGroup
key={group[0].id}
group={group}
startTime={startTime}
endTime={endTime}
/>
))}
<StyledEnd />
</StyledTimelineContainer>
<StyledRow>
<StyledMarkerLabel>{timeSpan.markers[0]}</StyledMarkerLabel>
{timeSpan.markers.slice(1).map((marker) => (
<StyledMiddleMarkerContainer key={marker}>
<StyledMiddleMarker />
<StyledMarkerLabel>{marker}</StyledMarkerLabel>
</StyledMiddleMarkerContainer>
))}
<StyledMarkerLabel>now</StyledMarkerLabel>
</StyledRow>
</StyledTimelineBody>
</>
);
};

View File

@ -47,7 +47,11 @@ const getEventIcon = (type: TimelineEventType) => {
return <ToggleOffIcon />;
}
if (type.startsWith('strategy-') || type.startsWith('feature-strategy-')) {
return <ExtensionOutlinedIcon />;
return (
<ExtensionOutlinedIcon
sx={{ marginTop: '-2px', marginRight: '-2px' }}
/>
);
}
if (type.startsWith('feature-')) {
return <FlagOutlinedIcon />;

View File

@ -36,7 +36,7 @@ export const EventTimelineEventGroup = ({
<StyledEvent position={position}>
<HtmlTooltip
title={<EventTimelineEventTooltip group={group} />}
maxWidth={320}
maxWidth={350}
arrow
>
<Badge

View File

@ -1,5 +1,6 @@
import { styled } from '@mui/material';
import { Markdown } from 'component/common/Markdown/Markdown';
import type { HTMLAttributes } from 'react';
import { useLocationSettings } from 'hooks/useLocationSettings';
import {
formatDateHMS,
@ -35,20 +36,26 @@ const StyledDate = styled('div')(({ theme }) => ({
whiteSpace: 'nowrap',
}));
const StyledTooltipItemList = styled('div')(({ theme }) => ({
marginTop: theme.spacing(1),
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
}));
const StyledTooltipItem = styled('div')(({ theme }) => ({
display: 'flex',
gap: theme.spacing(1),
marginBottom: theme.spacing(1),
}));
const StyledEventTimelineEventCircle = styled(EventTimelineEventCircle)(
({ theme }) => ({
marginTop: theme.spacing(0.5),
marginTop: theme.spacing(0.125),
height: theme.spacing(2.5),
width: theme.spacing(2.5),
transition: 'none',
'& > svg': {
height: theme.spacing(2),
height: theme.spacing(1.75),
},
'&:hover': {
transform: 'none',
@ -56,6 +63,8 @@ const StyledEventTimelineEventCircle = styled(EventTimelineEventCircle)(
}),
);
const BoldToNormal = ({ children }: HTMLAttributes<HTMLElement>) => children;
interface IEventTimelineEventTooltipProps {
group: TimelineEventGroup;
}
@ -78,7 +87,9 @@ export const EventTimelineEventTooltip = ({
<StyledTooltipTitle>{event.label}</StyledTooltipTitle>
<StyledDateTime>{eventDateTime}</StyledDateTime>
</StyledTooltipHeader>
<Markdown>{event.summary}</Markdown>
<Markdown components={{ strong: BoldToNormal }}>
{event.summary}
</Markdown>
</>
);
}
@ -97,20 +108,24 @@ export const EventTimelineEventTooltip = ({
</StyledTooltipTitle>
<StyledDate>{eventDate}</StyledDate>
</StyledTooltipHeader>
{group.map((event) => (
<StyledTooltipItem key={event.id}>
<StyledEventTimelineEventCircle group={[event]} />
<div>
<StyledDate>
{formatDateHMS(
event.timestamp,
locationSettings?.locale,
)}
</StyledDate>
<Markdown>{event.summary}</Markdown>
</div>
</StyledTooltipItem>
))}
<StyledTooltipItemList>
{group.map((event) => (
<StyledTooltipItem key={event.id}>
<StyledEventTimelineEventCircle group={[event]} />
<div>
<StyledDate>
{formatDateHMS(
event.timestamp,
locationSettings?.locale,
)}
</StyledDate>
<Markdown components={{ strong: BoldToNormal }}>
{event.summary}
</Markdown>
</div>
</StyledTooltipItem>
))}
</StyledTooltipItemList>
</>
);
};

View File

@ -1,9 +1,16 @@
import { MenuItem, styled, TextField } from '@mui/material';
import {
IconButton,
MenuItem,
styled,
TextField,
Tooltip,
} from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import type { IEnvironment } from 'interfaces/environments';
import { useEffect, useMemo } from 'react';
import { type TimeSpanOption, timeSpanOptions } from '../useEventTimeline';
import CloseIcon from '@mui/icons-material/Close';
const StyledCol = styled('div')(({ theme }) => ({
display: 'flex',
@ -12,9 +19,9 @@ const StyledCol = styled('div')(({ theme }) => ({
}));
const StyledFilter = styled(TextField)(({ theme }) => ({
color: theme.palette.text.secondary,
'& > div': {
background: 'transparent',
color: theme.palette.text.secondary,
'& > .MuiSelect-select': {
padding: theme.spacing(0.5, 4, 0.5, 1),
background: 'transparent',
@ -23,12 +30,17 @@ const StyledFilter = styled(TextField)(({ theme }) => ({
},
}));
const StyledTimelineEventsCount = styled('span')(({ theme }) => ({
marginTop: theme.spacing(0.25),
}));
interface IEventTimelineHeaderProps {
totalEvents: number;
timeSpan: TimeSpanOption;
setTimeSpan: (timeSpan: TimeSpanOption) => void;
environment: IEnvironment | undefined;
setEnvironment: (environment: IEnvironment) => void;
setOpen: (open: boolean) => void;
}
export const EventTimelineHeader = ({
@ -37,6 +49,7 @@ export const EventTimelineHeader = ({
setTimeSpan,
environment,
setEnvironment,
setOpen,
}: IEventTimelineHeaderProps) => {
const { environments } = useEnvironments();
@ -57,10 +70,10 @@ export const EventTimelineHeader = ({
return (
<>
<StyledCol>
<span>
<StyledTimelineEventsCount>
{totalEvents} event
{totalEvents === 1 ? '' : 's'}
</span>
</StyledTimelineEventsCount>
<StyledFilter
select
size='small'
@ -106,6 +119,15 @@ export const EventTimelineHeader = ({
</StyledFilter>
)}
/>
<Tooltip title='Hide timeline' arrow>
<IconButton
aria-label='close'
size='small'
onClick={() => setOpen(false)}
>
<CloseIcon />
</IconButton>
</Tooltip>
</StyledCol>
</>
);

View File

@ -118,8 +118,14 @@ export const MainLayout = forwardRef<HTMLDivElement, IMainLayoutProps>(
projectId || '',
);
const eventTimeline = useUiFlag('eventTimeline') && !isOss();
const { open: showTimeline, setOpen: setShowTimeline } =
useEventTimeline();
const {
open: showTimeline,
timeSpan,
environment,
setOpen: setShowTimeline,
setTimeSpan,
setEnvironment,
} = useEventTimeline();
const sidebarNavigationEnabled = useUiFlag('navigationSidebar');
const StyledMainLayoutContent = sidebarNavigationEnabled
@ -181,6 +187,11 @@ export const MainLayout = forwardRef<HTMLDivElement, IMainLayoutProps>(
>
<MainLayoutEventTimeline
open={eventTimeline && showTimeline}
setOpen={setShowTimeline}
timeSpan={timeSpan}
setTimeSpan={setTimeSpan}
environment={environment}
setEnvironment={setEnvironment}
/>
<StyledMainLayoutContent>

View File

@ -1,20 +1,38 @@
import { Box, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { EventTimeline } from 'component/events/EventTimeline/EventTimeline';
import type { TimeSpanOption } from 'component/events/EventTimeline/useEventTimeline';
import type { IEnvironment } from 'interfaces/environments';
import { useEffect, useState } from 'react';
interface IMainLayoutEventTimelineProps {
open: boolean;
}
const StyledEventTimelineWrapper = styled(Box)(({ theme }) => ({
const StyledEventTimelineSlider = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
height: '105px',
overflow: 'hidden',
boxShadow: theme.boxShadows.popup,
borderLeft: `1px solid ${theme.palette.divider}`,
}));
const StyledEventTimelineWrapper = styled(Box)(({ theme }) => ({
padding: theme.spacing(1.5, 2),
}));
interface IMainLayoutEventTimelineProps {
open: boolean;
timeSpan: TimeSpanOption;
environment: IEnvironment | undefined;
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);
@ -23,7 +41,7 @@ export const MainLayoutEventTimeline = ({
}, []);
return (
<StyledEventTimelineWrapper
<StyledEventTimelineSlider
sx={{
transition: isInitialLoad
? 'none'
@ -31,17 +49,20 @@ export const MainLayoutEventTimeline = ({
maxHeight: open ? '105px' : '0',
}}
>
<Box
sx={(theme) => ({
padding: theme.spacing(2),
backgroundColor: theme.palette.background.paper,
})}
>
<StyledEventTimelineWrapper>
<ConditionallyRender
condition={open}
show={<EventTimeline />}
show={
<EventTimeline
timeSpan={timeSpan}
environment={environment}
setTimeSpan={setTimeSpan}
setEnvironment={setEnvironment}
setOpen={setOpen}
/>
}
/>
</Box>
</StyledEventTimelineWrapper>
</StyledEventTimelineWrapper>
</StyledEventTimelineSlider>
);
};

View File

@ -33,7 +33,7 @@ import { useAdminRoutes } from 'component/admin/useAdminRoutes';
import InviteLinkButton from './InviteLink/InviteLinkButton/InviteLinkButton';
import { useUiFlag } from 'hooks/useUiFlag';
import { CommandBar } from 'component/commandBar/CommandBar';
import TimelineIcon from '@mui/icons-material/Timeline';
import LinearScaleIcon from '@mui/icons-material/LinearScale';
const HeaderComponent = styled(AppBar)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
@ -204,7 +204,7 @@ const Header = ({ showTimeline, setShowTimeline }: IHeaderProps) => {
}
size='large'
>
<TimelineIcon />
<LinearScaleIcon />
</StyledIconButton>
</Tooltip>
}

View File

@ -36,7 +36,7 @@ import { Notifications } from 'component/common/Notifications/Notifications';
import { useAdminRoutes } from 'component/admin/useAdminRoutes';
import InviteLinkButton from './InviteLink/InviteLinkButton/InviteLinkButton';
import { useUiFlag } from 'hooks/useUiFlag';
import TimelineIcon from '@mui/icons-material/Timeline';
import LinearScaleIcon from '@mui/icons-material/LinearScale';
const HeaderComponent = styled(AppBar)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
@ -269,7 +269,7 @@ const OldHeader = ({ showTimeline, setShowTimeline }: IOldHeaderProps) => {
}
size='large'
>
<TimelineIcon />
<LinearScaleIcon />
</StyledIconButton>
</Tooltip>
}