Skip to content

Commit 2f94f9b

Browse files
committed
HPCC-35525 Add rudimentary Global Metrics page
Signed-off-by: Gordon Smith <[email protected]>
1 parent 768a29e commit 2f94f9b

File tree

12 files changed

+1844
-1895
lines changed

12 files changed

+1844
-1895
lines changed

esp/src/package-lock.json

Lines changed: 1410 additions & 1837 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

esp/src/package.json

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "1.0.0",
44
"description": "'ECL Watch' Web interface for HPCC Platform.",
55
"scripts": {
6+
"install-playwright-deps": "npx playwright install --with-deps",
67
"clean": "rimraf ./build ./lib ./types ./src/nlsHPCCType.ts",
78
"lint": "eslint ./eclwatch ./src ./src-react",
89
"lint-fix": "eslint --fix eclwatch/**/*.js src/**/*.ts src-react/**/*.ts?",
@@ -49,20 +50,20 @@
4950
"@fluentui/react-icons-mdl2": "1.3.88",
5051
"@fluentui/react-migration-v8-v9": "9.9.7",
5152
"@fluentui/react-timepicker-compat": "0.4.16",
52-
"@hpcc-js/chart": "3.5.2",
53-
"@hpcc-js/codemirror": "3.4.1",
54-
"@hpcc-js/common": "3.3.9",
55-
"@hpcc-js/comms": "3.10.0",
53+
"@hpcc-js/chart": "3.6.1",
54+
"@hpcc-js/codemirror": "3.6.5",
55+
"@hpcc-js/common": "3.6.1",
56+
"@hpcc-js/comms": "3.13.0",
5657
"@hpcc-js/dataflow": "3.0.1",
57-
"@hpcc-js/eclwatch": "3.5.2",
58-
"@hpcc-js/graph": "3.3.12",
59-
"@hpcc-js/html": "3.2.10",
60-
"@hpcc-js/layout": "3.2.10",
61-
"@hpcc-js/map": "3.2.15",
62-
"@hpcc-js/other": "3.2.10",
63-
"@hpcc-js/phosphor": "3.2.10",
64-
"@hpcc-js/timeline": "3.2.2",
65-
"@hpcc-js/util": "3.3.9",
58+
"@hpcc-js/eclwatch": "3.5.5",
59+
"@hpcc-js/graph": "3.6.1",
60+
"@hpcc-js/html": "3.3.5",
61+
"@hpcc-js/layout": "3.4.5",
62+
"@hpcc-js/map": "3.4.5",
63+
"@hpcc-js/other": "3.4.5",
64+
"@hpcc-js/phosphor": "3.4.3",
65+
"@hpcc-js/timeline": "3.3.0",
66+
"@hpcc-js/util": "3.4.4",
6667
"@hpcc-js/wasm-duckdb": "1.9.0",
6768
"@hpcc-js/wasm-graphviz": "1.11.0",
6869
"clipboard": "2.0.11",

esp/src/src-dojo/nls/bs/hpcc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ export = {
544544
LogFilterSelectColumnModeTooltip: "Navedite koje kolone treba uključiti u log fajl",
545545
LogFilterSortByTooltip: "ASC - najstariji prvo, DESC - prvo najnoviji",
546546
LogFilterStartDateTooltip: "Uključite redove log dnevnika od početka datog vremenskog raspona",
547-
LogFiltersUnavailable:"Filteri dnevnika nisu dostupni",
547+
LogFiltersUnavailable: "Filteri dnevnika nisu dostupni",
548548
LogFilterTimeRequired: "Odaberite ili \"Od - do datume\" ili \"Relativni vremenski raspon\"",
549549
LogFilterWildcardFilterTooltip: "Tekst koje se koristi za filtriranje log dnevnika",
550550
LogFormat: "Format log dnevnika",

esp/src/src-dojo/nls/hpcc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ export = {
412412
GetVersion: "Get Version",
413413
GetPart: "Get Part",
414414
GetSoftwareInformation: "Get Software Information",
415+
GlobalMetrics: "Global Metrics",
415416
Graph: "Graph",
416417
Graphs: "Graphs",
417418
GraphControl: "Graph Control",

esp/src/src-dojo/nls/pt-br/hpcc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export = {
7777
AutoRefreshIncrement: "Atualização automática do incremento",
7878
Back: "voltar",
7979
BackupDFUWorkunit: "WU DFU de backup",
80-
BackupECLWorkunit: "Backup workunit ECL",
80+
BackupECLWorkunit: "WU ECL de backup",
8181
BannerColor: "Cor da Faixa",
8282
BannerColorTooltip: "Alterar a cor de fundo da navegação superior",
8383
BannerMessage: "Mensagem da Faixa",
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import * as React from "react";
2+
import { Badge, Field, Label, makeStyles, ProgressBar, Toolbar, ToolbarButton, ToolbarGroup, tokens } from "@fluentui/react-components";
3+
import { ArrowClockwiseRegular, ArrowRightFilled, ArrowResetFilled } from "@fluentui/react-icons";
4+
import { DatePicker } from "@fluentui/react-datepicker-compat";
5+
import { SelectionMode } from "@fluentui/react";
6+
import { timeFormat, timeParse } from "@hpcc-js/common";
7+
import { SMCService, WsSMC } from "@hpcc-js/comms";
8+
import { scopedLogger } from "@hpcc-js/util";
9+
import nlsHPCC from "src/nlsHPCC";
10+
import { formatCost } from "src/Session";
11+
import { HolyGrail } from "../layouts/HolyGrail";
12+
import { SizeMe } from "../layouts/SizeMe";
13+
import { pushParams } from "../util/history";
14+
import { FluentColumns, FluentGrid, useFluentStoreState } from "./controls/Grid";
15+
16+
const logger = scopedLogger("src-react/components/GlobalMetrics.tsx");
17+
18+
const useStyles = makeStyles({
19+
root: {
20+
maxWidth: "200px"
21+
},
22+
toolbar: {
23+
justifyContent: "space-between",
24+
},
25+
label: { marginLeft: tokens.spacingHorizontalM, marginRight: tokens.spacingHorizontalXS }
26+
});
27+
28+
const dateFormatter = timeFormat("%Y-%m-%d");
29+
const dateParser = timeParse("%Y-%m-%d");
30+
const bucketDateFormatter = timeFormat("%Y-%m-%d %H:00");
31+
32+
function formatDuration(value: unknown): string {
33+
if (value === null || value === undefined || value === "") return "";
34+
35+
const pad2 = (n: number) => n.toString().padStart(2, "0");
36+
const pad3 = (n: number) => n.toString().padStart(3, "0");
37+
38+
const asTrimmedString = typeof value === "string" ? value.trim() : undefined;
39+
const isIntegerString = asTrimmedString !== undefined && /^-?\d+$/.test(asTrimmedString);
40+
41+
if (typeof value === "bigint" || isIntegerString) {
42+
const seconds = typeof value === "bigint" ? value : BigInt(asTrimmedString!);
43+
const sign = seconds < 0n ? "-" : "";
44+
const absSeconds = seconds < 0n ? -seconds : seconds;
45+
const totalMs = (absSeconds * 1000n) % 1000n;
46+
const ms = Number(totalMs);
47+
const sec = Number(absSeconds % 60n);
48+
const totalMin = absSeconds / 60n;
49+
const min = Number(totalMin % 60n);
50+
const hours = totalMin / 60n;
51+
52+
return `${sign}${hours.toString()}:${pad2(min)}:${pad2(sec)}.${pad3(ms)}`;
53+
}
54+
55+
const secondsNumber = typeof value === "number" ? value : Number(asTrimmedString ?? value);
56+
if (!Number.isFinite(secondsNumber)) return String(value);
57+
58+
const sign = secondsNumber < 0 ? "-" : "";
59+
const absSeconds = Math.abs(secondsNumber);
60+
const ms = Math.trunc((absSeconds * 1000) % 1000);
61+
const totalSec = Math.trunc(absSeconds);
62+
const sec = totalSec % 60;
63+
const totalMin = Math.trunc(totalSec / 60);
64+
const min = totalMin % 60;
65+
const hours = Math.trunc(totalMin / 60);
66+
67+
return `${sign}${hours}:${pad2(min)}:${pad2(sec)}.${pad3(ms)}`;
68+
}
69+
70+
function compareColumns(a: string, b: string): number {
71+
if (a.indexOf("🏷️") === 0) {
72+
if (b.indexOf("🏷️") === 0) {
73+
return a.localeCompare(b);
74+
} else if (b.indexOf("📊") === 0) {
75+
return -1;
76+
}
77+
} else if (a.indexOf("📊") === 0) {
78+
if (b.indexOf("📊") === 0) {
79+
return a.localeCompare(b);
80+
} else if (b.indexOf("🏷️") === 0) {
81+
return 1;
82+
}
83+
}
84+
return 0;
85+
}
86+
87+
const smc = new SMCService({ baseUrl: "" });
88+
89+
async function getGlobalMetrics(request: Partial<WsSMC.GetGlobalMetrics>) {
90+
return smc.GetNormalisedGlobalMetrics(request).then(response => {
91+
const columnsSet = new Set<string>();
92+
columnsSet.add("Category");
93+
columnsSet.add("Start");
94+
columnsSet.add("End");
95+
const data = response.map(metric => {
96+
const retVal = {
97+
Category: metric.Category,
98+
Start: bucketDateFormatter(metric.Start),
99+
End: bucketDateFormatter(metric.End),
100+
};
101+
for (const dimName in metric.dimensions) {
102+
const name = `🏷️${dimName}`;
103+
columnsSet.add(name);
104+
retVal[name] = metric.dimensions[dimName];
105+
}
106+
for (const statName in metric.stats) {
107+
const name = `📊${statName}`;
108+
columnsSet.add(name);
109+
if ((statName ?? "").indexOf("Time") === 0) {
110+
retVal[name] = formatDuration(metric.stats[statName]);
111+
} else if ((statName ?? "").indexOf("Cost") === 0) {
112+
retVal[name] = formatCost(metric.stats[statName]);
113+
} else {
114+
retVal[name] = metric.stats[statName];
115+
}
116+
}
117+
return retVal;
118+
}) ?? [];
119+
120+
return {
121+
columns: Array.from(columnsSet).sort(compareColumns),
122+
data
123+
};
124+
});
125+
}
126+
127+
export interface GlobalMetricsProps {
128+
from?: string;
129+
to?: string;
130+
}
131+
132+
export const GlobalMetrics: React.FunctionComponent<GlobalMetricsProps> = ({
133+
to = dateFormatter(new Date()),
134+
from = dateFormatter(new Date(new Date().setDate(new Date().getDate() - 7)))
135+
}) => {
136+
const styles = useStyles();
137+
138+
const end = dateParser(to);
139+
const start = dateParser(from);
140+
141+
const [columns, setColumns] = React.useState<string[]>([]);
142+
const [data, setData] = React.useState<{ [key: string]: any }[]>([]);
143+
const [loading, setLoading] = React.useState(false);
144+
145+
const {
146+
setSelection,
147+
setTotal,
148+
refreshTable
149+
} = useFluentStoreState({});
150+
151+
const rows = React.useMemo(() => {
152+
return (data ?? []).map((d) => {
153+
const retVal = {};
154+
columns.forEach(col => {
155+
retVal[col] = d[col] ?? "";
156+
});
157+
return retVal;
158+
});
159+
}, [columns, data]);
160+
161+
const fluentColumns = React.useMemo((): FluentColumns => {
162+
const retVal: FluentColumns = {};
163+
columns.forEach((column) => {
164+
retVal[column] = {
165+
label: column,
166+
sortable: true,
167+
width: 180
168+
};
169+
});
170+
return retVal;
171+
}, [columns]);
172+
173+
const fetchData = React.useCallback(async (start: Date, end: Date) => {
174+
setLoading(true);
175+
setColumns([]);
176+
setData([]);
177+
try {
178+
const adjustedEnd = new Date(end);
179+
adjustedEnd.setDate(adjustedEnd.getDate() + 1);
180+
181+
const { columns, data } = await getGlobalMetrics({
182+
DateTimeRange: {
183+
Start: start.toISOString(),
184+
End: adjustedEnd.toISOString()
185+
}
186+
});
187+
setColumns(columns);
188+
setData(data);
189+
} catch (e) {
190+
logger.error(e);
191+
} finally {
192+
setLoading(false);
193+
}
194+
}, []);
195+
196+
React.useEffect(() => {
197+
fetchData(dateParser(from), dateParser(to));
198+
}, [fetchData, from, to]);
199+
200+
const onFromChange = React.useCallback((date: Date | null | undefined) => {
201+
if (date) {
202+
pushParams({ from: dateFormatter(date) });
203+
}
204+
}, []);
205+
206+
const onToChange = React.useCallback((date: Date | null | undefined) => {
207+
if (date) {
208+
pushParams({ to: dateFormatter(date) });
209+
}
210+
}, []);
211+
212+
return <>
213+
<HolyGrail
214+
header={<Toolbar className={styles.toolbar}>
215+
<ToolbarGroup>
216+
<ToolbarButton icon={<ArrowClockwiseRegular />} onClick={() => { fetchData(dateParser(from), dateParser(to)); }}>{nlsHPCC.Refresh}</ToolbarButton>
217+
<DatePicker value={start} onSelectDate={onFromChange} placeholder={nlsHPCC.FromDate} className={styles.root} />
218+
<ArrowRightFilled />
219+
<DatePicker value={end} onSelectDate={onToChange} showCloseButton={true} placeholder={nlsHPCC.ToDate} className={styles.root} />
220+
<ToolbarButton icon={<ArrowResetFilled title={nlsHPCC.Reset} />} onClick={() => { pushParams({ from: undefined, to: undefined }); }} title={nlsHPCC.Reset}></ToolbarButton>
221+
</ToolbarGroup>
222+
<ToolbarGroup>
223+
<Label style={{ color: tokens.colorBrandForeground1 }} className={styles.label}>{nlsHPCC.Total}:</Label><Badge appearance="tint" color="brand">{data.length}</Badge>
224+
</ToolbarGroup>
225+
</Toolbar>}
226+
main={
227+
<SizeMe>{({ size }) =>
228+
<div style={{ width: "100%", height: "100%", overflow: "hidden" }}>
229+
{loading &&
230+
<Field validationMessage={nlsHPCC.Refresh} validationState="none">
231+
<ProgressBar />
232+
</Field>
233+
}
234+
<FluentGrid
235+
data={rows}
236+
primaryID={"id"}
237+
columns={fluentColumns}
238+
setSelection={setSelection}
239+
setTotal={setTotal}
240+
refresh={refreshTable}
241+
height={`${Math.max(0, size.height - (loading ? 44 : 0))}px`}
242+
selectionMode={SelectionMode.none}
243+
/>
244+
</div>
245+
}</SizeMe>
246+
}
247+
footer={<></>}
248+
footerStyles={{}}
249+
/>
250+
</>;
251+
};

esp/src/src-react/components/Menu.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ const subMenuItems: SubMenuItems = {
203203
{ headerText: nlsHPCC.Services, itemKey: "/topology/services" },
204204
{ headerText: nlsHPCC.Logs, itemKey: "/topology/logs" },
205205
{ headerText: nlsHPCC.WUSummary, itemKey: "/topology/wu-summary" },
206+
{ headerText: nlsHPCC.GlobalMetrics, itemKey: "/topology/global-stats" },
206207
{ headerText: nlsHPCC.Security + " (L)", itemKey: "/topology/security" },
207208
{ headerText: nlsHPCC.DESDL + " (L)", itemKey: "/topology/desdl" },
208209
{ headerText: nlsHPCC.DaliAdmin, itemKey: "/topology/daliadmin" },
@@ -215,6 +216,7 @@ const subMenuItems: SubMenuItems = {
215216
{ headerText: nlsHPCC.ClusterProcesses + " (L)", itemKey: "/operations/processes" },
216217
{ headerText: nlsHPCC.SystemServers + " (L)", itemKey: "/operations/servers" },
217218
{ headerText: nlsHPCC.WUSummary, itemKey: "/operations/wu-summary" },
219+
{ headerText: nlsHPCC.GlobalMetrics, itemKey: "/operations/global-stats" },
218220
{ headerText: nlsHPCC.Security + " (L)", itemKey: "/operations/security" },
219221
{ headerText: nlsHPCC.DESDL + " (L)", itemKey: "/operations/desdl" },
220222
],

esp/src/src-react/hooks/diskUsage.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback, useEffect, useMemo, useState } from "react";
2-
import { MachineService, WsDFUXRefEx, WsMachineEx } from "@hpcc-js/comms";
2+
import { MachineService, WsMachineEx } from "@hpcc-js/comms";
33

44
export interface DirectoryEx {
55
Cluster: string;
@@ -13,11 +13,6 @@ export interface DirectoryEx {
1313
PositiveSkew: string;
1414
}
1515

16-
export interface XREFDirectories {
17-
nodes: WsDFUXRefEx.XRefNode[];
18-
directories: DirectoryEx[];
19-
}
20-
2116
export interface UseAsyncResult<T> {
2217
data?: T;
2318
loading: boolean;

esp/src/src-react/routes.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,12 @@ export const routes: RoutesEx = [
360360
return <_.WUSSummary from={filter?.from} to={filter?.to} />;
361361
})
362362
},
363+
{
364+
path: "/global-stats", action: (ctx) => import("./components/GlobalMetrics").then(_ => {
365+
const filter = parseSearch(ctx.search) as any;
366+
return <_.GlobalMetrics from={filter?.from} to={filter?.to} />;
367+
})
368+
},
363369
{
364370
path: "/security",
365371
action: () => { if (!dojoConfig.isAdmin) { replaceUrl("/topology"); } },
@@ -512,7 +518,12 @@ export const routes: RoutesEx = [
512518
})
513519
},
514520
{
515-
521+
path: "/global-stats", action: (ctx) => import("./components/GlobalMetrics").then(_ => {
522+
const filter = parseSearch(ctx.search) as any;
523+
return <_.GlobalMetrics from={filter?.from} to={filter?.to} />;
524+
})
525+
},
526+
{
516527
path: "/security",
517528
action: () => { if (!dojoConfig.isAdmin) { replaceUrl("/operations"); } },
518529
children: [

esp/src/src/Session.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,12 +129,12 @@ getBuildInfo().then(info => {
129129

130130
export const formatTwoDigits = d3Format(",.2f");
131131
const formatSixDigits = d3Format(",.6f");
132-
export function formatCost(value): string {
132+
export function formatCost(value, unitOffset: number = 1): string {
133133
if (isNaN(value)) {
134134
logger.debug(`formatCost called for a nullish value: ${value}`);
135135
return "";
136136
}
137-
const _number = typeof value === "string" ? Number(value) : value;
137+
const _number = (typeof value === "string" ? Number(value) : value) * unitOffset;
138138
const format = (_number > 0 && _number < 1) ? formatSixDigits : formatTwoDigits;
139139
return `${format(_number)} (${dojoConfig?.currencyCode || "USD"})`;
140140
}

0 commit comments

Comments
 (0)