@oneuptime/common 10.5.29 → 10.5.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Server/Services/UserNotificationRuleService.ts +3 -3
- package/Types/Dashboard/DashboardTemplates.ts +27 -26
- package/Types/Metrics/MetricQueryConfigData.ts +11 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +38 -0
- package/UI/Utils/Navigation.ts +39 -0
- package/UI/Utils/TableFilterUrlState.ts +92 -0
- package/build/dist/Server/Services/UserNotificationRuleService.js +3 -3
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardTemplates.js +27 -26
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +29 -0
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +30 -0
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/UI/Utils/TableFilterUrlState.js +67 -0
- package/build/dist/UI/Utils/TableFilterUrlState.js.map +1 -0
- package/package.json +1 -1
|
@@ -1838,7 +1838,7 @@ export class Service extends DatabaseService<Model> {
|
|
|
1838
1838
|
to: to,
|
|
1839
1839
|
data: [
|
|
1840
1840
|
{
|
|
1841
|
-
sayMessage: "This is a call from
|
|
1841
|
+
sayMessage: "This is a call from One Uptime",
|
|
1842
1842
|
},
|
|
1843
1843
|
{
|
|
1844
1844
|
sayMessage: "A new alert has been created",
|
|
@@ -1894,7 +1894,7 @@ export class Service extends DatabaseService<Model> {
|
|
|
1894
1894
|
to: to,
|
|
1895
1895
|
data: [
|
|
1896
1896
|
{
|
|
1897
|
-
sayMessage: "This is a call from
|
|
1897
|
+
sayMessage: "This is a call from One Uptime",
|
|
1898
1898
|
},
|
|
1899
1899
|
{
|
|
1900
1900
|
sayMessage: "A new incident has been created",
|
|
@@ -1951,7 +1951,7 @@ export class Service extends DatabaseService<Model> {
|
|
|
1951
1951
|
to: to,
|
|
1952
1952
|
data: [
|
|
1953
1953
|
{
|
|
1954
|
-
sayMessage: "This is a call from
|
|
1954
|
+
sayMessage: "This is a call from One Uptime",
|
|
1955
1955
|
},
|
|
1956
1956
|
{
|
|
1957
1957
|
sayMessage: "A new alert episode has been created",
|
|
@@ -933,12 +933,13 @@ function createKubernetesDashboardConfig(): DashboardViewConfig {
|
|
|
933
933
|
* first-class percent metric we replace it with a Value widget that
|
|
934
934
|
* renders the absolute usage via ValueFormatter (e.g. "8.3 GB").
|
|
935
935
|
*
|
|
936
|
-
* - CPU widgets
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
936
|
+
* - CPU widgets show cores, not a percent. OTel's k8s.*.cpu.utilization
|
|
937
|
+
* is a misnamed cores gauge (cores in use, NOT a [0, 1] ratio), and
|
|
938
|
+
* this templated dashboard's renderer can't divide by per-node
|
|
939
|
+
* allocatable CPU to form a true percentage. So we use the `.usage`
|
|
940
|
+
* metrics and label them in cores ("2.3 cores"). The Kubernetes
|
|
941
|
+
* cluster overview page — which fetches `k8s.node.allocatable_cpu` —
|
|
942
|
+
* is where CPU is shown as a real "% of capacity".
|
|
942
943
|
*/
|
|
943
944
|
const components: Array<DashboardBaseComponent> = [
|
|
944
945
|
// Row 0: Title
|
|
@@ -953,16 +954,16 @@ function createKubernetesDashboardConfig(): DashboardViewConfig {
|
|
|
953
954
|
|
|
954
955
|
/*
|
|
955
956
|
* Row 1: Key cluster metrics — averages render with proper units via
|
|
956
|
-
* ValueFormatter (CPU
|
|
957
|
+
* ValueFormatter (CPU usage → cores, memory.usage → "MB"/"GB").
|
|
957
958
|
* All four are "higher = worse" (closer to capacity = bad).
|
|
958
959
|
*/
|
|
959
960
|
createValueComponent({
|
|
960
|
-
title: "Pod CPU (avg)",
|
|
961
|
+
title: "Pod CPU (cores, avg)",
|
|
961
962
|
top: 1,
|
|
962
963
|
left: 0,
|
|
963
964
|
width: 3,
|
|
964
965
|
metricConfig: {
|
|
965
|
-
metricName: "k8s.pod.cpu.
|
|
966
|
+
metricName: "k8s.pod.cpu.usage",
|
|
966
967
|
aggregationType: MetricsAggregationType.Avg,
|
|
967
968
|
},
|
|
968
969
|
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
@@ -979,12 +980,12 @@ function createKubernetesDashboardConfig(): DashboardViewConfig {
|
|
|
979
980
|
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
980
981
|
}),
|
|
981
982
|
createValueComponent({
|
|
982
|
-
title: "Node CPU (avg)",
|
|
983
|
+
title: "Node CPU (cores, avg)",
|
|
983
984
|
top: 1,
|
|
984
985
|
left: 6,
|
|
985
986
|
width: 3,
|
|
986
987
|
metricConfig: {
|
|
987
|
-
metricName: "k8s.node.cpu.
|
|
988
|
+
metricName: "k8s.node.cpu.usage",
|
|
988
989
|
aggregationType: MetricsAggregationType.Avg,
|
|
989
990
|
},
|
|
990
991
|
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
@@ -1003,16 +1004,16 @@ function createKubernetesDashboardConfig(): DashboardViewConfig {
|
|
|
1003
1004
|
|
|
1004
1005
|
// Row 2-4: Resource usage charts
|
|
1005
1006
|
createChartComponent({
|
|
1006
|
-
title: "CPU
|
|
1007
|
+
title: "Pod CPU Cores Over Time",
|
|
1007
1008
|
chartType: DashboardChartType.Line,
|
|
1008
1009
|
top: 2,
|
|
1009
1010
|
left: 0,
|
|
1010
1011
|
width: 6,
|
|
1011
1012
|
height: 3,
|
|
1012
1013
|
metricConfig: {
|
|
1013
|
-
metricName: "k8s.pod.cpu.
|
|
1014
|
+
metricName: "k8s.pod.cpu.usage",
|
|
1014
1015
|
aggregationType: MetricsAggregationType.Avg,
|
|
1015
|
-
legend: "CPU
|
|
1016
|
+
legend: "CPU Cores",
|
|
1016
1017
|
},
|
|
1017
1018
|
}),
|
|
1018
1019
|
createChartComponent({
|
|
@@ -1072,24 +1073,24 @@ function createKubernetesDashboardConfig(): DashboardViewConfig {
|
|
|
1072
1073
|
}),
|
|
1073
1074
|
|
|
1074
1075
|
/*
|
|
1075
|
-
* Row 11-13: CPU
|
|
1076
|
-
*
|
|
1077
|
-
*
|
|
1076
|
+
* Row 11-13: cluster CPU cores tile and the network throughput
|
|
1077
|
+
* chart. This was a 0-100 CPU gauge, but the cores-valued
|
|
1078
|
+
* `k8s.node.cpu.utilization` pinned it to nonsense (e.g. 711%) and
|
|
1079
|
+
* the templated renderer can't divide by allocatable CPU to make a
|
|
1080
|
+
* real percentage — so we show total cores in use instead. The old
|
|
1081
|
+
* "Memory Utilization" gauge over raw bytes is gone — see
|
|
1082
|
+
* top-of-function comment.
|
|
1078
1083
|
*/
|
|
1079
|
-
|
|
1080
|
-
title: "Cluster CPU
|
|
1084
|
+
createValueComponent({
|
|
1085
|
+
title: "Cluster CPU (cores in use)",
|
|
1081
1086
|
top: 11,
|
|
1082
1087
|
left: 0,
|
|
1083
1088
|
width: 4,
|
|
1084
|
-
height: 3,
|
|
1085
|
-
minValue: 0,
|
|
1086
|
-
maxValue: 100,
|
|
1087
|
-
warningThreshold: 70,
|
|
1088
|
-
criticalThreshold: 90,
|
|
1089
1089
|
metricConfig: {
|
|
1090
|
-
metricName: "k8s.node.cpu.
|
|
1091
|
-
aggregationType: MetricsAggregationType.
|
|
1090
|
+
metricName: "k8s.node.cpu.usage",
|
|
1091
|
+
aggregationType: MetricsAggregationType.Sum,
|
|
1092
1092
|
},
|
|
1093
|
+
trendDirection: DashboardValueTrendDirection.HigherIsWorse,
|
|
1093
1094
|
}),
|
|
1094
1095
|
createChartComponent({
|
|
1095
1096
|
title: "Network I/O",
|
|
@@ -18,6 +18,17 @@ export default interface MetricQueryConfigData {
|
|
|
18
18
|
getSeries?: ((data: AggregatedModel) => ChartSeries) | undefined;
|
|
19
19
|
chartType?: MetricChartType | undefined;
|
|
20
20
|
yAxisValueFormatter?: ((value: number) => string) | undefined;
|
|
21
|
+
/*
|
|
22
|
+
* Optional post-aggregation transform of each datapoint's plotted
|
|
23
|
+
* value. Runs before `transformAsRate`. Receives the raw aggregated
|
|
24
|
+
* value plus the full datapoint (so the transform can read grouped
|
|
25
|
+
* attributes like `resource.k8s.node.name`). Used, e.g., to turn a
|
|
26
|
+
* Kubernetes CPU *cores* value into "% of its node's allocatable CPU"
|
|
27
|
+
* by dividing each point by the node's capacity. Unset = no change.
|
|
28
|
+
*/
|
|
29
|
+
transformValue?:
|
|
30
|
+
| ((value: number, dataPoint: AggregatedModel) => number)
|
|
31
|
+
| undefined;
|
|
21
32
|
warningThreshold?: number | undefined;
|
|
22
33
|
criticalThreshold?: number | undefined;
|
|
23
34
|
/*
|
|
@@ -12,6 +12,7 @@ import Sort from "../../../Types/BaseDatabase/Sort";
|
|
|
12
12
|
import Select from "../../../Types/BaseDatabase/Select";
|
|
13
13
|
import { Logger } from "../../Utils/Logger";
|
|
14
14
|
import Navigation from "../../Utils/Navigation";
|
|
15
|
+
import TableFilterUrlState from "../../Utils/TableFilterUrlState";
|
|
15
16
|
import PermissionUtil from "../../Utils/Permission";
|
|
16
17
|
import ProjectUtil from "../../Utils/Project";
|
|
17
18
|
import User from "../../Utils/User";
|
|
@@ -1862,6 +1863,43 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
1862
1863
|
setQuery({ ...newQuery });
|
|
1863
1864
|
};
|
|
1864
1865
|
|
|
1866
|
+
/*
|
|
1867
|
+
* URL persistence for classic (column) filters, keyed by the table's
|
|
1868
|
+
* `saveFilterProps.tableId`. Mirrors the facet persistence in
|
|
1869
|
+
* `useResourceOwners` so filters survive navigating to a detail page and
|
|
1870
|
+
* back, and so a filtered view is shareable. `hasRestoredUrlFilters` is
|
|
1871
|
+
* state (not a ref) so the persist effect only runs after the restore has
|
|
1872
|
+
* been applied — never clobbering the snapshot with the empty default.
|
|
1873
|
+
*/
|
|
1874
|
+
const [hasRestoredUrlFilters, setHasRestoredUrlFilters] =
|
|
1875
|
+
useState<boolean>(false);
|
|
1876
|
+
|
|
1877
|
+
useEffect(() => {
|
|
1878
|
+
const restored: JSONObject | null = TableFilterUrlState.read(
|
|
1879
|
+
props.saveFilterProps?.tableId,
|
|
1880
|
+
"filter",
|
|
1881
|
+
);
|
|
1882
|
+
if (restored) {
|
|
1883
|
+
/*
|
|
1884
|
+
* Re-run through onFilterChanged so the derived query is rebuilt and the
|
|
1885
|
+
* first fetch uses the restored filters.
|
|
1886
|
+
*/
|
|
1887
|
+
onFilterChanged(restored as unknown as FilterData<TBaseModel>);
|
|
1888
|
+
}
|
|
1889
|
+
setHasRestoredUrlFilters(true);
|
|
1890
|
+
}, []);
|
|
1891
|
+
|
|
1892
|
+
useEffect(() => {
|
|
1893
|
+
if (!hasRestoredUrlFilters) {
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
TableFilterUrlState.write(
|
|
1897
|
+
props.saveFilterProps?.tableId,
|
|
1898
|
+
"filter",
|
|
1899
|
+
filterData as unknown as JSONObject,
|
|
1900
|
+
);
|
|
1901
|
+
}, [hasRestoredUrlFilters, filterData]);
|
|
1902
|
+
|
|
1865
1903
|
type GetDeleteBulkActionFunction = () => BulkActionButtonSchema<TBaseModel>;
|
|
1866
1904
|
|
|
1867
1905
|
const getDeleteBulkAction: GetDeleteBulkActionFunction =
|
package/UI/Utils/Navigation.ts
CHANGED
|
@@ -66,6 +66,45 @@ abstract class Navigation {
|
|
|
66
66
|
return null;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Update (or remove) query-string params on the current URL *in place*,
|
|
71
|
+
* without pushing a new browser-history entry and without triggering a
|
|
72
|
+
* react-router navigation/re-render. Pass `null` (or "") as a value to
|
|
73
|
+
* delete that param.
|
|
74
|
+
*
|
|
75
|
+
* This is used to keep table filter/facet state in the URL so it survives
|
|
76
|
+
* navigating to a detail page and back, and so a filtered view is
|
|
77
|
+
* shareable/bookmarkable. We use `replaceState` (not push) so changing a
|
|
78
|
+
* filter doesn't flood the back-button history.
|
|
79
|
+
*/
|
|
80
|
+
public static setQueryString(params: Dictionary<string | null>): void {
|
|
81
|
+
const urlSearchParams: URLSearchParams = new URLSearchParams(
|
|
82
|
+
window.location.search,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
for (const paramName in params) {
|
|
86
|
+
const value: string | null =
|
|
87
|
+
params[paramName] === undefined
|
|
88
|
+
? null
|
|
89
|
+
: (params[paramName] as string | null);
|
|
90
|
+
|
|
91
|
+
if (value === null || value === "") {
|
|
92
|
+
urlSearchParams.delete(paramName);
|
|
93
|
+
} else {
|
|
94
|
+
urlSearchParams.set(paramName, value);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const queryString: string = urlSearchParams.toString();
|
|
99
|
+
|
|
100
|
+
const newRelativeUrl: string =
|
|
101
|
+
window.location.pathname +
|
|
102
|
+
(queryString ? `?${queryString}` : "") +
|
|
103
|
+
window.location.hash;
|
|
104
|
+
|
|
105
|
+
window.history.replaceState(window.history.state, "", newRelativeUrl);
|
|
106
|
+
}
|
|
107
|
+
|
|
69
108
|
public static getParamByName(
|
|
70
109
|
paramName: string,
|
|
71
110
|
routeTemplate: Route,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import Navigation from "./Navigation";
|
|
2
|
+
import { JSONObject } from "../../Types/JSON";
|
|
3
|
+
import JSONFunctions from "../../Types/JSONFunctions";
|
|
4
|
+
|
|
5
|
+
export type TableFilterUrlStateKind = "facets" | "filter";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Persists a table's filter/facet selections in the URL query string so they:
|
|
9
|
+
* - survive navigating to a detail page and clicking the browser "Back" button
|
|
10
|
+
* (the list page remounts with the params still on the URL), and
|
|
11
|
+
* - are shareable/bookmarkable (the URL fully describes the filtered view).
|
|
12
|
+
*
|
|
13
|
+
* The snapshot is run through {@link JSONFunctions} so OneUptime's typed query
|
|
14
|
+
* values (Search, Includes, InBetween, ...) round-trip as real class instances
|
|
15
|
+
* rather than plain objects. Params are namespaced by `tableId` so two tables on
|
|
16
|
+
* the same route don't clobber each other.
|
|
17
|
+
*/
|
|
18
|
+
export default class TableFilterUrlState {
|
|
19
|
+
private static getParamName(
|
|
20
|
+
tableId: string,
|
|
21
|
+
kind: TableFilterUrlStateKind,
|
|
22
|
+
): string {
|
|
23
|
+
return `${tableId}-${kind}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* Read a previously-persisted snapshot from the URL. Returns null when absent,
|
|
28
|
+
* empty, or unparseable (e.g. a hand-edited param) so callers fall back to
|
|
29
|
+
* their defaults.
|
|
30
|
+
*/
|
|
31
|
+
public static read(
|
|
32
|
+
tableId: string | undefined,
|
|
33
|
+
kind: TableFilterUrlStateKind,
|
|
34
|
+
): JSONObject | null {
|
|
35
|
+
if (!tableId) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const raw: string | null = Navigation.getQueryStringByName(
|
|
41
|
+
this.getParamName(tableId, kind),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (!raw) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const deserialized: JSONObject = JSONFunctions.deserialize(
|
|
49
|
+
JSONFunctions.parseJSONObject(raw),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
if (deserialized && Object.keys(deserialized).length > 0) {
|
|
53
|
+
return deserialized;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/*
|
|
63
|
+
* Persist a snapshot to the URL, or remove the param when the state is empty
|
|
64
|
+
* (`null`, or an object with no keys). Does not add a browser-history entry.
|
|
65
|
+
*/
|
|
66
|
+
public static write(
|
|
67
|
+
tableId: string | undefined,
|
|
68
|
+
kind: TableFilterUrlStateKind,
|
|
69
|
+
state: JSONObject | null,
|
|
70
|
+
): void {
|
|
71
|
+
if (!tableId) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const hasState: boolean = Boolean(state && Object.keys(state).length > 0);
|
|
77
|
+
|
|
78
|
+
const value: string | null = hasState
|
|
79
|
+
? JSON.stringify(JSONFunctions.serialize(state as JSONObject))
|
|
80
|
+
: null;
|
|
81
|
+
|
|
82
|
+
Navigation.setQueryString({
|
|
83
|
+
[this.getParamName(tableId, kind)]: value,
|
|
84
|
+
});
|
|
85
|
+
} catch {
|
|
86
|
+
/*
|
|
87
|
+
* Serialization failure (shouldn't happen for filter data) — skip the
|
|
88
|
+
* URL sync rather than break the table.
|
|
89
|
+
*/
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -1339,7 +1339,7 @@ export class Service extends DatabaseService {
|
|
|
1339
1339
|
to: to,
|
|
1340
1340
|
data: [
|
|
1341
1341
|
{
|
|
1342
|
-
sayMessage: "This is a call from
|
|
1342
|
+
sayMessage: "This is a call from One Uptime",
|
|
1343
1343
|
},
|
|
1344
1344
|
{
|
|
1345
1345
|
sayMessage: "A new alert has been created",
|
|
@@ -1378,7 +1378,7 @@ export class Service extends DatabaseService {
|
|
|
1378
1378
|
to: to,
|
|
1379
1379
|
data: [
|
|
1380
1380
|
{
|
|
1381
|
-
sayMessage: "This is a call from
|
|
1381
|
+
sayMessage: "This is a call from One Uptime",
|
|
1382
1382
|
},
|
|
1383
1383
|
{
|
|
1384
1384
|
sayMessage: "A new incident has been created",
|
|
@@ -1419,7 +1419,7 @@ export class Service extends DatabaseService {
|
|
|
1419
1419
|
to: to,
|
|
1420
1420
|
data: [
|
|
1421
1421
|
{
|
|
1422
|
-
sayMessage: "This is a call from
|
|
1422
|
+
sayMessage: "This is a call from One Uptime",
|
|
1423
1423
|
},
|
|
1424
1424
|
{
|
|
1425
1425
|
sayMessage: "A new alert episode has been created",
|