mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +02:00
chore: event timeline (#8176)
https://linear.app/unleash/issue/2-2657/implement-a-first-iteration-of-an-horizontal-event-timeline This implements the very first iteration of our event timeline. This is behind a feature flag, which when enabled shows the new timeline at the top of our event log page. It is missing some features, like: - Placement: It should show up as an option in the header, not in the event log; - Tooltip: It should show proper tooltips for all the events that we're displaying; - Grouping: It should group together events that occurred in a short span of time; - Signals: It should show signals along with the events; Here's how it currently looks like, with some example events, in order from left to right: - A flag was disabled more than 30 min ago; - A flag was then enabled; - A segment was updated (didn't have an icon for segments, so I picked one); - A strategy was updated; - A flag was created;   (Time passed since I took the first screenshot, so you can see the events "moved" to the left slightly in the dark theme screenshot) I have some concerns about the low contrast of `neutral` variant events, especially in dark mode. Maybe we should consider using `error` instead, for red? Or maybe add a border to our event circles? I specifically changed my environment to be "development" for the screenshots. The default selection is the first enabled environment that is `type=production`, which in my case is "production". Here are our filters: - Time Span  - Environment  Here are a few more screenshots, with the different time spans (zooming out, since we're increasing the time span):       Again, when zooming out, some events should be grouped together, but that's a task for later.
This commit is contained in:
parent
70e95e66a8
commit
205b59ddee
@ -1,4 +1,4 @@
|
|||||||
import { Switch, FormControlLabel, useMediaQuery } from '@mui/material';
|
import { Switch, FormControlLabel, useMediaQuery, Box } from '@mui/material';
|
||||||
import EventJson from 'component/events/EventJson/EventJson';
|
import EventJson from 'component/events/EventJson/EventJson';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
@ -15,6 +15,7 @@ import { useEventLogSearch } from './useEventLogSearch';
|
|||||||
import { StickyPaginationBar } from 'component/common/Table/StickyPaginationBar/StickyPaginationBar';
|
import { StickyPaginationBar } from 'component/common/Table/StickyPaginationBar/StickyPaginationBar';
|
||||||
import { EventActions } from './EventActions';
|
import { EventActions } from './EventActions';
|
||||||
import useLoading from 'hooks/useLoading';
|
import useLoading from 'hooks/useLoading';
|
||||||
|
import { EventTimeline } from '../EventTimeline/EventTimeline';
|
||||||
|
|
||||||
interface IEventLogProps {
|
interface IEventLogProps {
|
||||||
title: string;
|
title: string;
|
||||||
@ -50,7 +51,8 @@ const Placeholder = styled('li')({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const EventLog = ({ title, project, feature }: IEventLogProps) => {
|
export const EventLog = ({ title, project, feature }: IEventLogProps) => {
|
||||||
const { isEnterprise } = useUiConfig();
|
const { isOss, isEnterprise } = useUiConfig();
|
||||||
|
const eventTimeline = useUiFlag('eventTimeline') && !isOss();
|
||||||
const showFilters = useUiFlag('newEventSearch') && isEnterprise();
|
const showFilters = useUiFlag('newEventSearch') && isEnterprise();
|
||||||
const {
|
const {
|
||||||
events,
|
events,
|
||||||
@ -131,55 +133,72 @@ export const EventLog = ({ title, project, feature }: IEventLogProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<>
|
||||||
bodyClass={'no-padding'}
|
<ConditionallyRender
|
||||||
header={
|
condition={eventTimeline}
|
||||||
<PageHeader
|
show={
|
||||||
title={`${title} (${total})`}
|
<Box
|
||||||
actions={
|
sx={(theme) => ({
|
||||||
<>
|
borderRadius: theme.shape.borderRadius,
|
||||||
{showDataSwitch}
|
padding: theme.spacing(2),
|
||||||
<EventActions events={events} />
|
marginBottom: theme.spacing(2),
|
||||||
{!isSmallScreen && searchInputField}
|
backgroundColor: theme.palette.background.paper,
|
||||||
</>
|
})}
|
||||||
}
|
>
|
||||||
>
|
<EventTimeline />
|
||||||
{isSmallScreen && searchInputField}
|
</Box>
|
||||||
</PageHeader>
|
}
|
||||||
}
|
/>
|
||||||
>
|
<PageContent
|
||||||
<EventResultWrapper ref={ref} withFilters={showFilters}>
|
bodyClass={'no-padding'}
|
||||||
|
header={
|
||||||
|
<PageHeader
|
||||||
|
title={`${title} (${total})`}
|
||||||
|
actions={
|
||||||
|
<>
|
||||||
|
{showDataSwitch}
|
||||||
|
<EventActions events={events} />
|
||||||
|
{!isSmallScreen && searchInputField}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isSmallScreen && searchInputField}
|
||||||
|
</PageHeader>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<EventResultWrapper ref={ref} withFilters={showFilters}>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={showFilters}
|
||||||
|
show={
|
||||||
|
<StyledFilters
|
||||||
|
logType={
|
||||||
|
project
|
||||||
|
? 'project'
|
||||||
|
: feature
|
||||||
|
? 'flag'
|
||||||
|
: 'global'
|
||||||
|
}
|
||||||
|
state={filterState}
|
||||||
|
onChange={setTableState}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{resultComponent()}
|
||||||
|
</EventResultWrapper>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={showFilters}
|
condition={total > 25}
|
||||||
show={
|
show={
|
||||||
<StyledFilters
|
<StickyPaginationBar
|
||||||
logType={
|
totalItems={total}
|
||||||
project
|
pageSize={pagination.pageSize}
|
||||||
? 'project'
|
pageIndex={pagination.currentPage}
|
||||||
: feature
|
fetchPrevPage={pagination.prevPage}
|
||||||
? 'flag'
|
fetchNextPage={pagination.nextPage}
|
||||||
: 'global'
|
setPageLimit={pagination.setPageLimit}
|
||||||
}
|
|
||||||
state={filterState}
|
|
||||||
onChange={setTableState}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{resultComponent()}
|
</PageContent>
|
||||||
</EventResultWrapper>
|
</>
|
||||||
<ConditionallyRender
|
|
||||||
condition={total > 25}
|
|
||||||
show={
|
|
||||||
<StickyPaginationBar
|
|
||||||
totalItems={total}
|
|
||||||
pageSize={pagination.pageSize}
|
|
||||||
pageIndex={pagination.currentPage}
|
|
||||||
fetchPrevPage={pagination.prevPage}
|
|
||||||
fetchNextPage={pagination.nextPage}
|
|
||||||
setPageLimit={pagination.setPageLimit}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
151
frontend/src/component/events/EventTimeline/EventTimeline.tsx
Normal file
151
frontend/src/component/events/EventTimeline/EventTimeline.tsx
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { styled } from '@mui/material';
|
||||||
|
import type { EventSchemaType } from 'openapi';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { startOfDay, sub } from 'date-fns';
|
||||||
|
import type { IEnvironment } from 'interfaces/environments';
|
||||||
|
import { useEventSearch } from 'hooks/api/getters/useEventSearch/useEventSearch';
|
||||||
|
import { EventTimelineEvent } from './EventTimelineEvent/EventTimelineEvent';
|
||||||
|
import {
|
||||||
|
EventTimelineHeader,
|
||||||
|
type TimeSpanOption,
|
||||||
|
timeSpanOptions,
|
||||||
|
} from './EventTimelineHeader/EventTimelineHeader';
|
||||||
|
|
||||||
|
const StyledRow = styled('div')({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledTimelineContainer = styled('div')(({ theme }) => ({
|
||||||
|
position: 'relative',
|
||||||
|
height: theme.spacing(1),
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: theme.spacing(1.5, 0),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledTimeline = styled('div')(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.divider,
|
||||||
|
height: theme.spacing(0.5),
|
||||||
|
width: '100%',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledMiddleMarkerContainer = styled('div')({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position: 'relative',
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledMarker = styled('div')(({ theme }) => ({
|
||||||
|
position: 'absolute',
|
||||||
|
height: theme.spacing(1),
|
||||||
|
width: theme.spacing(0.25),
|
||||||
|
backgroundColor: theme.palette.text.secondary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledMiddleMarker = styled(StyledMarker)(({ theme }) => ({
|
||||||
|
top: theme.spacing(-2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledMarkerLabel = styled('div')(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.smallerBody,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledStart = styled(StyledMarker)({
|
||||||
|
left: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const StyledEnd = styled(StyledMarker)({
|
||||||
|
right: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const RELEVANT_EVENT_TYPES: EventSchemaType[] = [
|
||||||
|
'strategy-reactivated',
|
||||||
|
'strategy-updated',
|
||||||
|
'segment-updated',
|
||||||
|
'segment-deleted',
|
||||||
|
'feature-created',
|
||||||
|
'feature-updated',
|
||||||
|
'feature-variants-updated',
|
||||||
|
'feature-archived',
|
||||||
|
'feature-revived',
|
||||||
|
'feature-strategy-update',
|
||||||
|
'feature-strategy-add',
|
||||||
|
'feature-strategy-remove',
|
||||||
|
'feature-environment-enabled',
|
||||||
|
'feature-environment-disabled',
|
||||||
|
];
|
||||||
|
|
||||||
|
const toISODateString = (date: Date) => date.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
export const EventTimeline = () => {
|
||||||
|
const [timeSpan, setTimeSpan] = useState<TimeSpanOption>(
|
||||||
|
timeSpanOptions[0],
|
||||||
|
);
|
||||||
|
const [environment, setEnvironment] = useState<IEnvironment | undefined>();
|
||||||
|
|
||||||
|
const endDate = new Date();
|
||||||
|
const startDate = sub(endDate, timeSpan.value);
|
||||||
|
|
||||||
|
const { events } = useEventSearch(
|
||||||
|
{
|
||||||
|
from: `IS:${toISODateString(startOfDay(startDate))}`,
|
||||||
|
to: `IS:${toISODateString(endDate)}`,
|
||||||
|
type: `IS_ANY_OF:${RELEVANT_EVENT_TYPES.join(',')}`,
|
||||||
|
},
|
||||||
|
{ refreshInterval: 10 * 1000 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredEvents = events.filter(
|
||||||
|
(event) =>
|
||||||
|
new Date(event.createdAt).getTime() >= startDate.getTime() &&
|
||||||
|
new Date(event.createdAt).getTime() <= endDate.getTime() &&
|
||||||
|
RELEVANT_EVENT_TYPES.includes(event.type) &&
|
||||||
|
(!event.environment ||
|
||||||
|
!environment ||
|
||||||
|
event.environment === environment.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sortedEvents = [...filteredEvents].reverse();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledRow>
|
||||||
|
<EventTimelineHeader
|
||||||
|
totalEvents={sortedEvents.length}
|
||||||
|
timeSpan={timeSpan}
|
||||||
|
setTimeSpan={setTimeSpan}
|
||||||
|
environment={environment}
|
||||||
|
setEnvironment={setEnvironment}
|
||||||
|
/>
|
||||||
|
</StyledRow>
|
||||||
|
<StyledTimelineContainer>
|
||||||
|
<StyledTimeline />
|
||||||
|
<StyledStart />
|
||||||
|
{sortedEvents.map((event) => (
|
||||||
|
<EventTimelineEvent
|
||||||
|
key={event.id}
|
||||||
|
event={event}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,108 @@
|
|||||||
|
import type { EventSchema, EventSchemaType } from 'openapi';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
|
||||||
|
import { EventTimelineEventTooltip } from './EventTimelineEventTooltip/EventTimelineEventTooltip';
|
||||||
|
import ToggleOnIcon from '@mui/icons-material/ToggleOn';
|
||||||
|
import ToggleOffIcon from '@mui/icons-material/ToggleOff';
|
||||||
|
import FlagOutlinedIcon from '@mui/icons-material/FlagOutlined';
|
||||||
|
import ExtensionOutlinedIcon from '@mui/icons-material/ExtensionOutlined';
|
||||||
|
import SegmentsIcon from '@mui/icons-material/DonutLargeOutlined';
|
||||||
|
import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
|
||||||
|
|
||||||
|
type DefaultEventVariant = 'secondary';
|
||||||
|
type CustomEventVariant = 'success' | 'neutral';
|
||||||
|
type EventVariant = DefaultEventVariant | CustomEventVariant;
|
||||||
|
|
||||||
|
const StyledEvent = styled('div', {
|
||||||
|
shouldForwardProp: (prop) => prop !== 'position',
|
||||||
|
})<{ position: string }>(({ position }) => ({
|
||||||
|
position: 'absolute',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
left: position,
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
zIndex: 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledEventCircle = styled('div', {
|
||||||
|
shouldForwardProp: (prop) => prop !== 'variant',
|
||||||
|
})<{ variant: EventVariant }>(({ theme, variant }) => ({
|
||||||
|
height: theme.spacing(3),
|
||||||
|
width: theme.spacing(3),
|
||||||
|
borderRadius: '50%',
|
||||||
|
backgroundColor: theme.palette[variant].light,
|
||||||
|
border: `1px solid ${theme.palette[variant].main}`,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
transition: 'transform 0.2s',
|
||||||
|
'& svg': {
|
||||||
|
color: theme.palette[variant].main,
|
||||||
|
height: theme.spacing(2.5),
|
||||||
|
width: theme.spacing(2.5),
|
||||||
|
},
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'scale(1.5)',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getEventIcon = (type: EventSchemaType) => {
|
||||||
|
if (type === 'feature-environment-enabled') {
|
||||||
|
return <ToggleOnIcon />;
|
||||||
|
}
|
||||||
|
if (type === 'feature-environment-disabled') {
|
||||||
|
return <ToggleOffIcon />;
|
||||||
|
}
|
||||||
|
if (type.startsWith('strategy-') || type.startsWith('feature-strategy-')) {
|
||||||
|
return <ExtensionOutlinedIcon />;
|
||||||
|
}
|
||||||
|
if (type.startsWith('feature-')) {
|
||||||
|
return <FlagOutlinedIcon />;
|
||||||
|
}
|
||||||
|
if (type.startsWith('segment-')) {
|
||||||
|
return <SegmentsIcon />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <QuestionMarkIcon />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const customEventVariants: Partial<
|
||||||
|
Record<EventSchemaType, CustomEventVariant>
|
||||||
|
> = {
|
||||||
|
'feature-environment-enabled': 'success',
|
||||||
|
'feature-environment-disabled': 'neutral',
|
||||||
|
'feature-archived': 'neutral',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IEventTimelineEventProps {
|
||||||
|
event: EventSchema;
|
||||||
|
startDate: Date;
|
||||||
|
endDate: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EventTimelineEvent = ({
|
||||||
|
event,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
}: IEventTimelineEventProps) => {
|
||||||
|
const timelineDuration = endDate.getTime() - startDate.getTime();
|
||||||
|
const eventTime = new Date(event.createdAt).getTime();
|
||||||
|
|
||||||
|
const position = `${((eventTime - startDate.getTime()) / timelineDuration) * 100}%`;
|
||||||
|
|
||||||
|
const variant = customEventVariants[event.type] || 'secondary';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledEvent position={position}>
|
||||||
|
<HtmlTooltip
|
||||||
|
title={<EventTimelineEventTooltip event={event} />}
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<StyledEventCircle variant={variant}>
|
||||||
|
{getEventIcon(event.type)}
|
||||||
|
</StyledEventCircle>
|
||||||
|
</HtmlTooltip>
|
||||||
|
</StyledEvent>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,50 @@
|
|||||||
|
import { useLocationSettings } from 'hooks/useLocationSettings';
|
||||||
|
import type { EventSchema } from 'openapi';
|
||||||
|
import { formatDateYMDHMS } from 'utils/formatDate';
|
||||||
|
|
||||||
|
interface IEventTimelineEventTooltipProps {
|
||||||
|
event: EventSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EventTimelineEventTooltip = ({
|
||||||
|
event,
|
||||||
|
}: IEventTimelineEventTooltipProps) => {
|
||||||
|
const { locationSettings } = useLocationSettings();
|
||||||
|
const eventDateTime = formatDateYMDHMS(
|
||||||
|
event.createdAt,
|
||||||
|
locationSettings?.locale,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (event.type === 'feature-environment-enabled') {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<small>{eventDateTime}</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>{eventDateTime}</small>
|
||||||
|
<p>
|
||||||
|
{event.createdBy} disabled {event.featureName} for the{' '}
|
||||||
|
{event.environment} environment in project {event.project}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>{eventDateTime}</div>
|
||||||
|
<div>{event.createdBy}</div>
|
||||||
|
<div>{event.type}</div>
|
||||||
|
<div>{event.featureName}</div>
|
||||||
|
<div>{event.environment}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,167 @@
|
|||||||
|
import { MenuItem, styled, TextField } 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';
|
||||||
|
|
||||||
|
const StyledCol = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledFilter = styled(TextField)(({ theme }) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
'& > div': {
|
||||||
|
background: 'transparent',
|
||||||
|
'& > .MuiSelect-select': {
|
||||||
|
padding: theme.spacing(0.5, 4, 0.5, 1),
|
||||||
|
background: 'transparent',
|
||||||
|
},
|
||||||
|
'& > fieldset': { borderColor: 'transparent' },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export type TimeSpanOption = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
value: Duration;
|
||||||
|
markers: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const timeSpanOptions: TimeSpanOption[] = [
|
||||||
|
{
|
||||||
|
key: '30m',
|
||||||
|
label: 'last 30 min',
|
||||||
|
value: { minutes: 30 },
|
||||||
|
markers: ['30 min ago'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '1h',
|
||||||
|
label: 'last hour',
|
||||||
|
value: { hours: 1 },
|
||||||
|
markers: ['1 hour ago', '30 min ago'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '3h',
|
||||||
|
label: 'last 3 hours',
|
||||||
|
value: { hours: 3 },
|
||||||
|
markers: ['3 hours ago', '2 hours ago', '1 hour ago'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '12h',
|
||||||
|
label: 'last 12 hours',
|
||||||
|
value: { hours: 12 },
|
||||||
|
markers: ['12 hours ago', '9 hours ago', '6 hours ago', '3 hours ago'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '24h',
|
||||||
|
label: 'last 24 hours',
|
||||||
|
value: { hours: 24 },
|
||||||
|
markers: [
|
||||||
|
'24 hours ago',
|
||||||
|
'18 hours ago',
|
||||||
|
'12 hours ago',
|
||||||
|
'6 hours ago',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '48h',
|
||||||
|
label: 'last 48 hours',
|
||||||
|
value: { hours: 48 },
|
||||||
|
markers: [
|
||||||
|
'48 hours ago',
|
||||||
|
'36 hours ago',
|
||||||
|
'24 hours ago',
|
||||||
|
'12 hours ago',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface IEventTimelineHeaderProps {
|
||||||
|
totalEvents: number;
|
||||||
|
timeSpan: TimeSpanOption;
|
||||||
|
setTimeSpan: (timeSpan: TimeSpanOption) => void;
|
||||||
|
environment: IEnvironment | undefined;
|
||||||
|
setEnvironment: (environment: IEnvironment) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EventTimelineHeader = ({
|
||||||
|
totalEvents,
|
||||||
|
timeSpan,
|
||||||
|
setTimeSpan,
|
||||||
|
environment,
|
||||||
|
setEnvironment,
|
||||||
|
}: IEventTimelineHeaderProps) => {
|
||||||
|
const { environments } = useEnvironments();
|
||||||
|
|
||||||
|
const activeEnvironments = useMemo(
|
||||||
|
() => environments.filter(({ enabled }) => enabled),
|
||||||
|
[environments],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeEnvironments.length > 0) {
|
||||||
|
const defaultEnvironment =
|
||||||
|
activeEnvironments.find(({ type }) => type === 'production') ||
|
||||||
|
activeEnvironments[0];
|
||||||
|
setEnvironment(defaultEnvironment);
|
||||||
|
}
|
||||||
|
}, [activeEnvironments]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledCol>
|
||||||
|
<span>
|
||||||
|
{totalEvents} event
|
||||||
|
{totalEvents === 1 ? '' : 's'}
|
||||||
|
</span>
|
||||||
|
<StyledFilter
|
||||||
|
select
|
||||||
|
size='small'
|
||||||
|
variant='outlined'
|
||||||
|
value={timeSpan.key}
|
||||||
|
onChange={(e) =>
|
||||||
|
setTimeSpan(
|
||||||
|
timeSpanOptions.find(
|
||||||
|
({ key }) => key === e.target.value,
|
||||||
|
) || timeSpanOptions[0],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{timeSpanOptions.map(({ key, label }) => (
|
||||||
|
<MenuItem key={key} value={key}>
|
||||||
|
{label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</StyledFilter>
|
||||||
|
</StyledCol>
|
||||||
|
<StyledCol>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(environment)}
|
||||||
|
show={() => (
|
||||||
|
<StyledFilter
|
||||||
|
select
|
||||||
|
size='small'
|
||||||
|
variant='outlined'
|
||||||
|
value={environment!.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setEnvironment(
|
||||||
|
environments.find(
|
||||||
|
({ name }) => name === e.target.value,
|
||||||
|
) || environments[0],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{environments.map(({ name }) => (
|
||||||
|
<MenuItem key={name} value={name}>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</StyledFilter>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StyledCol>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user