-
Notifications
You must be signed in to change notification settings - Fork 182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Node Details Panel #260
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,286 @@ | ||
import * as React from 'react'; | ||
import Link from 'next/link'; | ||
import Drawer from 'react-modern-drawer'; | ||
import { StringParam, useQueryParam, withDefault } from 'use-query-params'; | ||
import { | ||
Anchor, | ||
Badge, | ||
Card, | ||
Flex, | ||
Grid, | ||
SegmentedControl, | ||
Text, | ||
} from '@mantine/core'; | ||
|
||
import { DrawerBody, DrawerHeader } from './components/DrawerUtils'; | ||
import api from './api'; | ||
import { | ||
convertDateRangeToGranularityString, | ||
K8S_CPU_PERCENTAGE_NUMBER_FORMAT, | ||
K8S_MEM_NUMBER_FORMAT, | ||
} from './ChartUtils'; | ||
import HDXLineChart from './HDXLineChart'; | ||
import { InfraPodsStatusTable } from './KubernetesDashboardPage'; | ||
import { LogTableWithSidePanel } from './LogTableWithSidePanel'; | ||
import { parseTimeQuery, useTimeQuery } from './timeQuery'; | ||
import { formatUptime } from './utils'; | ||
import { useZIndex, ZIndexContext } from './zIndex'; | ||
|
||
import styles from '../styles/LogSidePanel.module.scss'; | ||
|
||
const CHART_HEIGHT = 300; | ||
const defaultTimeRange = parseTimeQuery('Past 1h', false); | ||
|
||
const PodDetailsProperty = React.memo( | ||
({ label, value }: { label: string; value?: React.ReactNode }) => { | ||
if (!value) return null; | ||
return ( | ||
<div className="pe-4"> | ||
<Text size="xs" color="gray.6"> | ||
{label} | ||
</Text> | ||
<Text size="sm" color="gray.3"> | ||
{value} | ||
</Text> | ||
</div> | ||
); | ||
}, | ||
); | ||
|
||
const NodeDetails = ({ | ||
name, | ||
dateRange, | ||
}: { | ||
name: string; | ||
dateRange: [Date, Date]; | ||
}) => { | ||
const where = `k8s.node.name:"${name}"`; | ||
const groupBy = ['k8s.node.name']; | ||
|
||
const { data } = api.useMultiSeriesChart({ | ||
series: [ | ||
{ | ||
table: 'metrics', | ||
field: 'k8s.node.condition_ready - Gauge', | ||
type: 'table', | ||
aggFn: 'last_value', | ||
where, | ||
groupBy, | ||
}, | ||
{ | ||
table: 'metrics', | ||
field: 'k8s.node.uptime - Sum', | ||
type: 'table', | ||
aggFn: 'last_value', | ||
where, | ||
groupBy, | ||
}, | ||
], | ||
endDate: dateRange[1] ?? new Date(), | ||
startDate: dateRange[0] ?? new Date(), | ||
seriesReturnType: 'column', | ||
}); | ||
|
||
const properties = React.useMemo(() => { | ||
const series: Record<string, any> = data?.data?.[0] || {}; | ||
return { | ||
ready: series['series_0.data'], | ||
uptime: series['series_1.data'], | ||
}; | ||
}, [data?.data]); | ||
|
||
return ( | ||
<Grid.Col span={12}> | ||
<div className="p-2 gap-2 d-flex flex-wrap"> | ||
<PodDetailsProperty label="Node" value={name} /> | ||
<PodDetailsProperty | ||
label="Status" | ||
value={ | ||
properties.ready === 1 ? ( | ||
<Badge color="green" fw="normal" tt="none" size="md"> | ||
Ready | ||
</Badge> | ||
) : ( | ||
<Badge color="red" fw="normal" tt="none" size="md"> | ||
Not Ready | ||
</Badge> | ||
) | ||
} | ||
/> | ||
<PodDetailsProperty | ||
label="Uptime" | ||
value={formatUptime(properties.uptime)} | ||
/> | ||
</div> | ||
</Grid.Col> | ||
); | ||
}; | ||
|
||
function NodeLogs({ | ||
where, | ||
dateRange, | ||
}: { | ||
where: string; | ||
dateRange: [Date, Date]; | ||
}) { | ||
const [resultType, setResultType] = React.useState<'all' | 'error'>('all'); | ||
|
||
const _where = where + (resultType === 'error' ? ' level:err' : ''); | ||
|
||
return ( | ||
<Card p="md"> | ||
<Card.Section p="md" py="xs" withBorder> | ||
<Flex justify="space-between" align="center"> | ||
Latest Node Logs & Spans | ||
<Flex gap="xs" align="center"> | ||
<SegmentedControl | ||
size="xs" | ||
value={resultType} | ||
onChange={(value: string) => { | ||
if (value === 'all' || value === 'error') { | ||
setResultType(value); | ||
} | ||
}} | ||
data={[ | ||
{ label: 'All', value: 'all' }, | ||
{ label: 'Errors', value: 'error' }, | ||
]} | ||
/> | ||
<Link href={`/search?q=${encodeURIComponent(_where)}`} passHref> | ||
<Anchor size="xs" color="dimmed"> | ||
Search <i className="bi bi-box-arrow-up-right"></i> | ||
</Anchor> | ||
</Link> | ||
</Flex> | ||
</Flex> | ||
</Card.Section> | ||
<Card.Section p="md" py="sm" h={CHART_HEIGHT}> | ||
<LogTableWithSidePanel | ||
config={{ | ||
dateRange, | ||
where: _where, | ||
}} | ||
isLive={false} | ||
isUTC={false} | ||
setIsUTC={() => {}} | ||
onPropertySearchClick={() => {}} | ||
/> | ||
</Card.Section> | ||
</Card> | ||
); | ||
} | ||
|
||
export default function NodeDetailsSidePanel() { | ||
const [nodeName, setNodeName] = useQueryParam( | ||
'nodeName', | ||
withDefault(StringParam, ''), | ||
{ | ||
updateType: 'replaceIn', | ||
}, | ||
); | ||
|
||
const contextZIndex = useZIndex(); | ||
const drawerZIndex = contextZIndex + 10; | ||
|
||
const where = React.useMemo(() => { | ||
return `k8s.node.name:"${nodeName}"`; | ||
}, [nodeName]); | ||
|
||
const { searchedTimeRange: dateRange } = useTimeQuery({ | ||
isUTC: false, | ||
defaultValue: 'Past 1h', | ||
defaultTimeRange: [ | ||
defaultTimeRange?.[0]?.getTime() ?? -1, | ||
defaultTimeRange?.[1]?.getTime() ?? -1, | ||
], | ||
}); | ||
|
||
const handleClose = React.useCallback(() => { | ||
setNodeName(undefined); | ||
}, [setNodeName]); | ||
|
||
if (!nodeName) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<Drawer | ||
enableOverlay | ||
overlayOpacity={0.1} | ||
duration={0} | ||
open={!!nodeName} | ||
onClose={handleClose} | ||
direction="right" | ||
size={'80vw'} | ||
zIndex={drawerZIndex} | ||
> | ||
<ZIndexContext.Provider value={drawerZIndex}> | ||
<div className={styles.panel}> | ||
<DrawerHeader | ||
header={`Details for ${nodeName}`} | ||
onClose={handleClose} | ||
/> | ||
<DrawerBody> | ||
<Grid> | ||
<NodeDetails name={nodeName} dateRange={dateRange} /> | ||
<Grid.Col span={6}> | ||
<Card p="md"> | ||
<Card.Section p="md" py="xs" withBorder> | ||
CPU Usage by Pod | ||
</Card.Section> | ||
<Card.Section p="md" py="sm" h={CHART_HEIGHT}> | ||
<HDXLineChart | ||
config={{ | ||
dateRange, | ||
granularity: convertDateRangeToGranularityString( | ||
dateRange, | ||
60, | ||
), | ||
groupBy: 'k8s.pod.name', | ||
where, | ||
table: 'metrics', | ||
aggFn: 'avg', | ||
field: 'k8s.pod.cpu.utilization - Gauge', | ||
numberFormat: K8S_CPU_PERCENTAGE_NUMBER_FORMAT, | ||
}} | ||
/> | ||
</Card.Section> | ||
</Card> | ||
</Grid.Col> | ||
<Grid.Col span={6}> | ||
<Card p="md"> | ||
<Card.Section p="md" py="xs" withBorder> | ||
Memory Usage by Pod | ||
</Card.Section> | ||
<Card.Section p="md" py="sm" h={CHART_HEIGHT}> | ||
<HDXLineChart | ||
config={{ | ||
dateRange, | ||
granularity: convertDateRangeToGranularityString( | ||
dateRange, | ||
60, | ||
), | ||
groupBy: 'k8s.pod.name', | ||
where, | ||
table: 'metrics', | ||
aggFn: 'avg', | ||
field: 'k8s.pod.memory.usage - Gauge', | ||
numberFormat: K8S_MEM_NUMBER_FORMAT, | ||
}} | ||
/> | ||
</Card.Section> | ||
</Card> | ||
</Grid.Col> | ||
<Grid.Col span={12}> | ||
<InfraPodsStatusTable dateRange={dateRange} where={where} /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When clicking on the pod table here, the side panel gets obscured behind since it's triggering it in the "wrong" z index context. I wonder if we still need to import side panels like we do with the log side panel (along with the table, instead of just globally on a page). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i added a hack to check if the panel is nested, but going to rework it with a separate state later |
||
</Grid.Col> | ||
<Grid.Col span={12}> | ||
<NodeLogs where={where} dateRange={dateRange} /> | ||
</Grid.Col> | ||
</Grid> | ||
</DrawerBody> | ||
</div> | ||
</ZIndexContext.Provider> | ||
</Drawer> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any chance we can add anchor tags to these so they're more clicky?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not very trivial, let's descope for now :D