Skip to content
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

Merged
merged 2 commits into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 39 additions & 24 deletions packages/app/src/KubernetesDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import HDXLineChart from './HDXLineChart';
import { withAppNav } from './layout';
import { LogTableWithSidePanel } from './LogTableWithSidePanel';
import MetricTagValueSelect from './MetricTagValueSelect';
import NodeDetailsSidePanel from './NodeDetailsSidePanel';
import PodDetailsSidePanel from './PodDetailsSidePanel';
import HdxSearchInput from './SearchInput';
import SearchTimeRangePicker from './SearchTimeRangePicker';
Expand Down Expand Up @@ -410,6 +411,12 @@ const NodesTable = ({
seriesReturnType: 'column',
});

const getLink = React.useCallback((nodeName: string) => {
const searchParams = new URLSearchParams(window.location.search);
searchParams.set('nodeName', `${nodeName}`);
return window.location.pathname + '?' + searchParams.toString();
}, []);

const nodesList = React.useMemo(() => {
if (!data) {
return [];
Expand Down Expand Up @@ -472,30 +479,37 @@ const NodesTable = ({
) : (
<tbody>
{nodesList.map(node => (
<tr key={node.name}>
<td>{node.name || 'N/A'}</td>
<td>
{node.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>
)}
</td>
<td>
{formatNumber(
node.cpuAvg,
K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
)}
</td>
<td>
{formatNumber(node.memAvg, K8S_MEM_NUMBER_FORMAT)}
</td>
<td>{node.uptime ? formatUptime(node.uptime) : '–'}</td>
</tr>
<Link key={node.name} href={getLink(node.name)}>
Copy link
Contributor

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?

Copy link
Contributor Author

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

<tr className="cursor-pointer">
<td>{node.name || 'N/A'}</td>
<td>
{node.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>
)}
</td>
<td>
{formatNumber(
node.cpuAvg,
K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
)}
</td>
<td>
{formatNumber(node.memAvg, K8S_MEM_NUMBER_FORMAT)}
</td>
<td>{node.uptime ? formatUptime(node.uptime) : '–'}</td>
</tr>
</Link>
))}
</tbody>
)}
Expand Down Expand Up @@ -593,6 +607,7 @@ export default function KubernetesDashboardPage() {
<title>Kubernetes Dashboard</title>
</Head>
<PodDetailsSidePanel />
<NodeDetailsSidePanel />
<div className="d-flex flex-column">
<Group
px="md"
Expand Down
286 changes: 286 additions & 0 deletions packages/app/src/NodeDetailsSidePanel.tsx
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} />
Copy link
Contributor

Choose a reason for hiding this comment

The 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).

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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>
);
}
Loading
Loading