diff --git a/packages/app/src/LogSidePanel.tsx b/packages/app/src/LogSidePanel.tsx index 09a88bfbc..cb26be979 100644 --- a/packages/app/src/LogSidePanel.tsx +++ b/packages/app/src/LogSidePanel.tsx @@ -21,13 +21,19 @@ import Timestamp from 'timestamp-nano'; import { useQueryParam } from 'use-query-params'; import { ActionIcon, + Box, + Card, Group, Menu, + ScrollArea, SegmentedControl, + SimpleGrid, + Stack, TextInput, } from '@mantine/core'; import HyperJson, { GetLineActions, LineAction } from './components/HyperJson'; +import { KubeTimeline } from './components/KubeComponents'; import { Table } from './components/Table'; import api from './api'; import { @@ -2293,7 +2299,6 @@ const ExceptionSubpanel = ({ ); }; -import { Card, SimpleGrid, Stack } from '@mantine/core'; import { convertDateRangeToGranularityString, Granularity } from './ChartUtils'; @@ -2465,12 +2470,40 @@ const MetricsSubpanel = ({ logData }: { logData?: any }) => { return ( {podUid && ( - +
+ + + + Pod Timeline + + + + + This Event
, + timestamp: new Date(timestamp).toISOString(), + }} + /> + + + + + )} {nodeName && ( Details for {podName} - + @@ -180,19 +181,18 @@ export default function PodDetailsSidePanel() { - Latest Kubernetes Events + Latest Pod Events - - + {}} - onPropertySearchClick={() => {}} - /> + > + + + + diff --git a/packages/app/src/ServiceDashboardPage.tsx b/packages/app/src/ServiceDashboardPage.tsx index 47a87f90e..413039009 100644 --- a/packages/app/src/ServiceDashboardPage.tsx +++ b/packages/app/src/ServiceDashboardPage.tsx @@ -11,6 +11,7 @@ import { Flex, Grid, Group, + ScrollArea, Select, Skeleton, Table, @@ -155,74 +156,80 @@ const InfraPodsStatusTable = ({ Pods - {isError ? ( -
- Unable to load pod metrics -
- ) : ( - - - - - - {/* */} - - - - - - {isLoading ? ( - - {Array.from({ length: 4 }).map((_, index) => ( - - - - - - - - ))} - - ) : ( - - {data?.data?.map((row: any) => ( - - - - - {/* */} + + {isError ? ( +
+ Unable to load pod metrics +
+ ) : ( +
NameRestartsAgeCPU AvgMem AvgStatus
- - - - - - - - - -
{row.group}{row['series_0.data']}{formatDistanceStrict(row['series_1.data'] * 1000, 0)}
+ + + + + {/* */} + + + + + + {isLoading ? ( + + {Array.from({ length: 4 }).map((_, index) => ( + + + - - ))} - - )} -
NameRestartsAgeCPU AvgMem AvgStatus
+ + + + - {formatNumber( - row['series_2.data'], - K8S_CPU_PERCENTAGE_NUMBER_FORMAT, - )} + - {formatNumber( - row['series_3.data'], - K8S_MEM_NUMBER_FORMAT, - )} + - +
- )} + ))} + + ) : ( + + {data?.data?.map((row: any) => ( + + + {row.group} + {row['series_0.data']} + {/* {formatDistanceStrict(row['series_1.data'] * 1000, 0)} */} + + {formatNumber( + row['series_2.data'], + K8S_CPU_PERCENTAGE_NUMBER_FORMAT, + )} + + + {formatNumber( + row['series_3.data'], + K8S_MEM_NUMBER_FORMAT, + )} + + + + + + + ))} + + )} + + )} +
); diff --git a/packages/app/src/components/KubeComponents.tsx b/packages/app/src/components/KubeComponents.tsx new file mode 100644 index 000000000..a0d12a142 --- /dev/null +++ b/packages/app/src/components/KubeComponents.tsx @@ -0,0 +1,155 @@ +import * as React from 'react'; +import { format, sub } from 'date-fns'; +import { Badge, Group, Text, Timeline } from '@mantine/core'; + +import api from '../api'; + +type KubeEvent = { + id: string; + timestamp: string; + severity_text?: string; + 'object.reason'?: string; + 'object.note'?: string; + 'object.type'?: string; +}; + +type AnchorEvent = { + timestamp: string; + label: React.ReactNode; +}; + +const FORMAT = 'MMM d HH:mm:ss'; + +const renderKubeEvent = (event: KubeEvent) => { + return ( + + + {format(new Date(event.timestamp), FORMAT)} + + + + {event['object.reason']} + + {event['object.type'] && ( + + {event['object.type']} + + )} + + {event['object.note']} + + ); +}; + +export const KubeTimeline = ({ + q, + anchorEvent, + dateRange, +}: { + q: string; + dateRange?: [Date, Date]; + anchorEvent?: AnchorEvent; +}) => { + const startDate = React.useMemo( + () => dateRange?.[0] ?? sub(new Date(), { days: 7 }), + [dateRange], + ); + const endDate = React.useMemo( + () => dateRange?.[1] ?? new Date(), + [dateRange], + ); + + const { data, isLoading } = api.useLogBatch({ + q: `k8s.resource.name:"events" ${q}`, + limit: 50, + startDate, + endDate, + extraFields: [ + 'object.metadata.creationTimestamp', + 'object.reason', + 'object.note', + 'object.type', + 'type', + ], + order: 'desc', + }); + + const allPodEvents: KubeEvent[] = React.useMemo( + () => + (data?.pages?.[0]?.data || []).sort( + (a, b) => + new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), + ), + [data], + ); + + const podEventsBeforeAnchor = React.useMemo(() => { + return anchorEvent + ? allPodEvents.filter(event => { + return new Date(event.timestamp) < new Date(anchorEvent.timestamp); + }) + : []; + }, [allPodEvents, anchorEvent]); + + const podEventsAfterAnchor = React.useMemo(() => { + return anchorEvent + ? allPodEvents.filter(event => { + return new Date(event.timestamp) > new Date(anchorEvent.timestamp); + }) + : []; + }, [allPodEvents, anchorEvent]); + + // Scroll to anchor event if it exists + const anchorRef = React.useCallback(node => { + if (node !== null) { + // setting block to center causes the entire view to scroll + // todo - figure out how to scroll just the timeline and center the anchor event + node.scrollIntoView({ block: 'nearest' }); + } + }, []); + + if (isLoading) { + return ( + + Loading... + + ); + } + + if (allPodEvents.length === 0) { + return ( + + No events + + ); + } + + if (anchorEvent) { + return ( + + {podEventsBeforeAnchor.map(renderKubeEvent)} + + + {format(new Date(anchorEvent.timestamp), FORMAT)} + + + + {anchorEvent.label} + + + + {podEventsAfterAnchor.map(renderKubeEvent)} + + ); + } else { + return ( + + {allPodEvents.map(renderKubeEvent)} + + ); + } +};