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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import { Markdown } from 'component/common/Markdown/Markdown'; import { Markdown } from 'component/common/Markdown/Markdown';
import type { HTMLAttributes } from 'react';
import { useLocationSettings } from 'hooks/useLocationSettings'; import { useLocationSettings } from 'hooks/useLocationSettings';
import { import {
formatDateHMS, formatDateHMS,
@ -35,20 +36,26 @@ const StyledDate = styled('div')(({ theme }) => ({
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
})); }));
const StyledTooltipItemList = styled('div')(({ theme }) => ({
marginTop: theme.spacing(1),
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
}));
const StyledTooltipItem = styled('div')(({ theme }) => ({ const StyledTooltipItem = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
gap: theme.spacing(1), gap: theme.spacing(1),
marginBottom: theme.spacing(1),
})); }));
const StyledEventTimelineEventCircle = styled(EventTimelineEventCircle)( const StyledEventTimelineEventCircle = styled(EventTimelineEventCircle)(
({ theme }) => ({ ({ theme }) => ({
marginTop: theme.spacing(0.5), marginTop: theme.spacing(0.125),
height: theme.spacing(2.5), height: theme.spacing(2.5),
width: theme.spacing(2.5), width: theme.spacing(2.5),
transition: 'none', transition: 'none',
'& > svg': { '& > svg': {
height: theme.spacing(2), height: theme.spacing(1.75),
}, },
'&:hover': { '&:hover': {
transform: 'none', transform: 'none',
@ -56,6 +63,8 @@ const StyledEventTimelineEventCircle = styled(EventTimelineEventCircle)(
}), }),
); );
const BoldToNormal = ({ children }: HTMLAttributes<HTMLElement>) => children;
interface IEventTimelineEventTooltipProps { interface IEventTimelineEventTooltipProps {
group: TimelineEventGroup; group: TimelineEventGroup;
} }
@ -78,7 +87,9 @@ export const EventTimelineEventTooltip = ({
<StyledTooltipTitle>{event.label}</StyledTooltipTitle> <StyledTooltipTitle>{event.label}</StyledTooltipTitle>
<StyledDateTime>{eventDateTime}</StyledDateTime> <StyledDateTime>{eventDateTime}</StyledDateTime>
</StyledTooltipHeader> </StyledTooltipHeader>
<Markdown>{event.summary}</Markdown> <Markdown components={{ strong: BoldToNormal }}>
{event.summary}
</Markdown>
</> </>
); );
} }
@ -97,20 +108,24 @@ export const EventTimelineEventTooltip = ({
</StyledTooltipTitle> </StyledTooltipTitle>
<StyledDate>{eventDate}</StyledDate> <StyledDate>{eventDate}</StyledDate>
</StyledTooltipHeader> </StyledTooltipHeader>
{group.map((event) => ( <StyledTooltipItemList>
<StyledTooltipItem key={event.id}> {group.map((event) => (
<StyledEventTimelineEventCircle group={[event]} /> <StyledTooltipItem key={event.id}>
<div> <StyledEventTimelineEventCircle group={[event]} />
<StyledDate> <div>
{formatDateHMS( <StyledDate>
event.timestamp, {formatDateHMS(
locationSettings?.locale, event.timestamp,
)} locationSettings?.locale,
</StyledDate> )}
<Markdown>{event.summary}</Markdown> </StyledDate>
</div> <Markdown components={{ strong: BoldToNormal }}>
</StyledTooltipItem> {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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments';
import type { IEnvironment } from 'interfaces/environments'; import type { IEnvironment } from 'interfaces/environments';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { type TimeSpanOption, timeSpanOptions } from '../useEventTimeline'; import { type TimeSpanOption, timeSpanOptions } from '../useEventTimeline';
import CloseIcon from '@mui/icons-material/Close';
const StyledCol = styled('div')(({ theme }) => ({ const StyledCol = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -12,9 +19,9 @@ const StyledCol = styled('div')(({ theme }) => ({
})); }));
const StyledFilter = styled(TextField)(({ theme }) => ({ const StyledFilter = styled(TextField)(({ theme }) => ({
color: theme.palette.text.secondary,
'& > div': { '& > div': {
background: 'transparent', background: 'transparent',
color: theme.palette.text.secondary,
'& > .MuiSelect-select': { '& > .MuiSelect-select': {
padding: theme.spacing(0.5, 4, 0.5, 1), padding: theme.spacing(0.5, 4, 0.5, 1),
background: 'transparent', background: 'transparent',
@ -23,12 +30,17 @@ const StyledFilter = styled(TextField)(({ theme }) => ({
}, },
})); }));
const StyledTimelineEventsCount = styled('span')(({ theme }) => ({
marginTop: theme.spacing(0.25),
}));
interface IEventTimelineHeaderProps { interface IEventTimelineHeaderProps {
totalEvents: number; totalEvents: number;
timeSpan: TimeSpanOption; timeSpan: TimeSpanOption;
setTimeSpan: (timeSpan: TimeSpanOption) => void; setTimeSpan: (timeSpan: TimeSpanOption) => void;
environment: IEnvironment | undefined; environment: IEnvironment | undefined;
setEnvironment: (environment: IEnvironment) => void; setEnvironment: (environment: IEnvironment) => void;
setOpen: (open: boolean) => void;
} }
export const EventTimelineHeader = ({ export const EventTimelineHeader = ({
@ -37,6 +49,7 @@ export const EventTimelineHeader = ({
setTimeSpan, setTimeSpan,
environment, environment,
setEnvironment, setEnvironment,
setOpen,
}: IEventTimelineHeaderProps) => { }: IEventTimelineHeaderProps) => {
const { environments } = useEnvironments(); const { environments } = useEnvironments();
@ -57,10 +70,10 @@ export const EventTimelineHeader = ({
return ( return (
<> <>
<StyledCol> <StyledCol>
<span> <StyledTimelineEventsCount>
{totalEvents} event {totalEvents} event
{totalEvents === 1 ? '' : 's'} {totalEvents === 1 ? '' : 's'}
</span> </StyledTimelineEventsCount>
<StyledFilter <StyledFilter
select select
size='small' size='small'
@ -106,6 +119,15 @@ export const EventTimelineHeader = ({
</StyledFilter> </StyledFilter>
)} )}
/> />
<Tooltip title='Hide timeline' arrow>
<IconButton
aria-label='close'
size='small'
onClick={() => setOpen(false)}
>
<CloseIcon />
</IconButton>
</Tooltip>
</StyledCol> </StyledCol>
</> </>
); );

View File

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

View File

@ -1,20 +1,38 @@
import { Box, styled } from '@mui/material'; import { Box, styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { EventTimeline } from 'component/events/EventTimeline/EventTimeline'; 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'; import { useEffect, useState } from 'react';
interface IMainLayoutEventTimelineProps { const StyledEventTimelineSlider = styled(Box)(({ theme }) => ({
open: boolean;
}
const StyledEventTimelineWrapper = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
height: '105px', height: '105px',
overflow: 'hidden', 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 = ({ export const MainLayoutEventTimeline = ({
open, open,
timeSpan,
environment,
setTimeSpan,
setEnvironment,
setOpen,
}: IMainLayoutEventTimelineProps) => { }: IMainLayoutEventTimelineProps) => {
const [isInitialLoad, setIsInitialLoad] = useState(true); const [isInitialLoad, setIsInitialLoad] = useState(true);
@ -23,7 +41,7 @@ export const MainLayoutEventTimeline = ({
}, []); }, []);
return ( return (
<StyledEventTimelineWrapper <StyledEventTimelineSlider
sx={{ sx={{
transition: isInitialLoad transition: isInitialLoad
? 'none' ? 'none'
@ -31,17 +49,20 @@ export const MainLayoutEventTimeline = ({
maxHeight: open ? '105px' : '0', maxHeight: open ? '105px' : '0',
}} }}
> >
<Box <StyledEventTimelineWrapper>
sx={(theme) => ({
padding: theme.spacing(2),
backgroundColor: theme.palette.background.paper,
})}
>
<ConditionallyRender <ConditionallyRender
condition={open} 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 InviteLinkButton from './InviteLink/InviteLinkButton/InviteLinkButton';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
import { CommandBar } from 'component/commandBar/CommandBar'; 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 }) => ({ const HeaderComponent = styled(AppBar)(({ theme }) => ({
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
@ -204,7 +204,7 @@ const Header = ({ showTimeline, setShowTimeline }: IHeaderProps) => {
} }
size='large' size='large'
> >
<TimelineIcon /> <LinearScaleIcon />
</StyledIconButton> </StyledIconButton>
</Tooltip> </Tooltip>
} }

View File

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