Skip to content

Commit 6f77ed2

Browse files
committed
chore: refactor Grid.tsx out of Dashboard/index.tsx
1 parent 23130a4 commit 6f77ed2

File tree

7 files changed

+286
-242
lines changed

7 files changed

+286
-242
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/*
2+
* Copyright 2023 The Kubernetes Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import React from "react"
18+
import { Box, BoxProps, Text, TextProps } from "ink"
19+
20+
import type { Props, State } from "./index.js"
21+
import type { UpdatePayload, Worker } from "./types.js"
22+
23+
type GridProps = {
24+
/** Position of legend w.r.t. the grid UI [default: "below"] */
25+
legendPosition?: "right" | "below"
26+
27+
scale: Props["scale"]
28+
title: NonNullable<Props["grids"][number]>["title"]
29+
states: NonNullable<Props["grids"][number]>["states"]
30+
workers: State["workers"][number]
31+
}
32+
33+
export default class Grid extends React.PureComponent<GridProps> {
34+
/** Chunk `A` into subarrays of at most length `N` */
35+
private chunk<T>(A: T[], N: number): T[][] {
36+
const matrix = Array(N).fill(undefined)
37+
for (let i = 0; i < N; i++) {
38+
matrix[i] = Array(N).fill(undefined)
39+
for (let j = 0; j < N; j++) {
40+
const a = A[i * N + j]
41+
if (a !== undefined) {
42+
matrix[i][j] = a
43+
}
44+
}
45+
}
46+
47+
// leftover?
48+
const lastIdx = N * N
49+
if (lastIdx < A.length) {
50+
const L: T[] = []
51+
matrix.push(L)
52+
for (let j = lastIdx; j < A.length; j++) {
53+
L.push(A[j])
54+
}
55+
}
56+
57+
return matrix
58+
}
59+
60+
// private readonly sizes = ["▁▁", "▃▃", "▅▅", "▆▆", "██", "■■"]
61+
62+
private readonly cell = "█▉"
63+
private cellFor(props: TextProps): TextProps {
64+
return Object.assign({ children: this.cell }, props)
65+
}
66+
67+
private get emptyCell(): TextProps {
68+
// TODO in light terminal themes, white-dim is a better choice
69+
// than gray-dim
70+
return this.cellFor({ color: "gray", dimColor: true })
71+
}
72+
73+
/** @return current `Worker[]` model */
74+
private get workers(): UpdatePayload["workers"] {
75+
return this.props.workers || []
76+
}
77+
78+
/** Zoom/Scale up the grid? */
79+
private get scale() {
80+
return Math.max(this.props.scale || 1, 1)
81+
}
82+
83+
/**
84+
* For visual clarity with a small number of workers, you may set a
85+
* minimum length of grid sides (Note: it will be square, so this is
86+
* just the length of a side in the grid/heat map).
87+
*/
88+
private get minMatrixSize() {
89+
return 6
90+
}
91+
92+
private matrixModel(): Worker[][] {
93+
const N = Math.max(this.minMatrixSize, Math.ceil(Math.sqrt(this.workers.length)))
94+
return this.chunk(this.workers, N)
95+
}
96+
97+
/** Histogram form of `this.workers` */
98+
private histoModel(): number[] {
99+
const keys = this.props.states
100+
const indexer = keys.reduce((M, { state }, idx) => {
101+
M[state] = idx
102+
return M
103+
}, {} as Record<string, number>)
104+
105+
return this.workers.reduce((H, worker) => {
106+
H[indexer[worker.metric]]++
107+
return H
108+
}, Array(keys.length).fill(0))
109+
}
110+
111+
/** @return legend UI */
112+
private legend() {
113+
const H = this.histoModel()
114+
const outerBoxProps: BoxProps = {
115+
marginRight: 1,
116+
justifyContent: "flex-end",
117+
}
118+
const innerBoxProps: BoxProps = {
119+
// flexDirection: "row", // values to the right of labels
120+
flexDirection: "column", // values below labels
121+
alignItems: "flex-end",
122+
}
123+
const valueProps = {}
124+
125+
// arrange into a kind of matrix with at most `k` labels per row
126+
const k = 3 // TODO take into account width of screen
127+
const A = this.chunk(this.props.states, k)
128+
const C = this.chunk(H, k).filter(Boolean)
129+
130+
return (
131+
<Box flexDirection="column">
132+
{A.filter(Boolean).map((AA, ridx) => (
133+
/* legend row */
134+
<Box key={ridx} flexDirection="row" justifyContent="space-between">
135+
{AA.filter(Boolean).map((_, cidx) => (
136+
/* legend entry (i.e. legend column) */
137+
<Box key={_.state} {...outerBoxProps}>
138+
<Box {...innerBoxProps} marginLeft={1}>
139+
{/* legend entry label */}
140+
<Box>
141+
<Text {..._.style} bold>
142+
{_.state}
143+
</Text>
144+
</Box>
145+
146+
{/* legend entry value */}
147+
<Box {...valueProps}>
148+
<Text {..._.style}>{C[ridx][cidx] /*.toString().padStart(maxLen)*/}</Text>
149+
</Box>
150+
</Box>
151+
</Box>
152+
))}
153+
</Box>
154+
))}
155+
</Box>
156+
)
157+
}
158+
159+
/** @return the UI for one row of the grid */
160+
private gridRow(row: Worker[], ridx: number) {
161+
return (
162+
<Box key={ridx}>
163+
{row.map((worker, cidx) => (
164+
<Text key={cidx} {...(!worker ? this.emptyCell : this.cellFor(worker.style))} />
165+
))}
166+
</Box>
167+
)
168+
}
169+
170+
/** @return grid UI */
171+
private grid() {
172+
const M = this.matrixModel()
173+
return (
174+
<Box flexDirection="column">
175+
{M.map((row, ridx) =>
176+
Array(this.scale)
177+
.fill(0)
178+
.map((_, sidx) => this.gridRow(row, this.scale * ridx + sidx))
179+
)}
180+
</Box>
181+
)
182+
}
183+
184+
private title() {
185+
return <Text>{this.props.title}</Text>
186+
}
187+
188+
private get legendPosition() {
189+
return this.props.legendPosition
190+
}
191+
192+
public render() {
193+
const flexDirection = this.legendPosition === "below" ? "column" : "row"
194+
const alignItems = this.legendPosition === "below" ? "center" : "center"
195+
const legendBoxProps = this.legendPosition === "below" ? { marginTop: 1 } : { marginLeft: 2 }
196+
197+
return (
198+
<Box
199+
flexDirection={flexDirection}
200+
alignItems={alignItems}
201+
justifyContent="center"
202+
paddingTop={1}
203+
paddingBottom={1}
204+
>
205+
{/* title and grid */}
206+
<Box flexDirection="column" alignItems="center">
207+
{this.title()}
208+
{this.grid()}
209+
</Box>
210+
211+
{/* legend */}
212+
<Box {...legendBoxProps}>{this.legend()}</Box>
213+
</Box>
214+
)
215+
}
216+
}

0 commit comments

Comments
 (0)