metrickit 0.1.2
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/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +379 -0
- package/dist/cache-redis.d.ts +10 -0
- package/dist/cache-redis.js +24 -0
- package/dist/cache-utils.d.ts +5 -0
- package/dist/cache.d.ts +18 -0
- package/dist/catalog.d.ts +12 -0
- package/dist/define-metric.d.ts +37 -0
- package/dist/engine.d.ts +200 -0
- package/dist/filters/default-metadata.d.ts +28 -0
- package/dist/filters/index.d.ts +3 -0
- package/dist/filters/parse.d.ts +7 -0
- package/dist/filters/types.d.ts +19 -0
- package/dist/frontend/catalog.d.ts +24 -0
- package/dist/frontend/dashboard.d.ts +12 -0
- package/dist/frontend/format.d.ts +10 -0
- package/dist/frontend/index.d.ts +10 -0
- package/dist/frontend/markers.d.ts +19 -0
- package/dist/frontend/renderers.d.ts +14 -0
- package/dist/frontend/requests.d.ts +17 -0
- package/dist/frontend/stream-state.d.ts +14 -0
- package/dist/frontend/time.d.ts +5 -0
- package/dist/frontend/transport.d.ts +19 -0
- package/dist/frontend/types.d.ts +108 -0
- package/dist/frontend.d.ts +1 -0
- package/dist/frontend.js +752 -0
- package/dist/helpers/clickhouse.d.ts +27 -0
- package/dist/helpers/distribution.d.ts +15 -0
- package/dist/helpers/index.d.ts +6 -0
- package/dist/helpers/metric-type.d.ts +21 -0
- package/dist/helpers/pivot.d.ts +15 -0
- package/dist/helpers/prisma.d.ts +20 -0
- package/dist/helpers/timeseries.d.ts +6 -0
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.js +668 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1322 -0
- package/dist/orpc.d.ts +36 -0
- package/dist/orpc.js +1157 -0
- package/dist/registry.d.ts +269 -0
- package/dist/run-metrics.d.ts +14 -0
- package/dist/schemas/index.d.ts +4 -0
- package/dist/schemas/inputs.d.ts +19 -0
- package/dist/schemas/metric-type.d.ts +7 -0
- package/dist/schemas/output.d.ts +842 -0
- package/dist/schemas/time.d.ts +24 -0
- package/dist/time.d.ts +6 -0
- package/dist/type-guards.d.ts +7 -0
- package/package.json +91 -0
package/dist/frontend.js
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
// src/registry.ts
|
|
2
|
+
import { z as z5 } from "zod";
|
|
3
|
+
|
|
4
|
+
// src/schemas/time.ts
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
var TimeGranularitySchema = z.enum([
|
|
7
|
+
"hour",
|
|
8
|
+
"day",
|
|
9
|
+
"week",
|
|
10
|
+
"month",
|
|
11
|
+
"quarter",
|
|
12
|
+
"year"
|
|
13
|
+
]);
|
|
14
|
+
var TimeRangeSchema = z.object({
|
|
15
|
+
from: z.date().optional(),
|
|
16
|
+
to: z.date().optional()
|
|
17
|
+
});
|
|
18
|
+
var RequiredTimeRangeSchema = z.object({
|
|
19
|
+
from: z.date(),
|
|
20
|
+
to: z.date()
|
|
21
|
+
});
|
|
22
|
+
var CompareSchema = z.object({
|
|
23
|
+
compareToPrevious: z.boolean().optional().default(false)
|
|
24
|
+
});
|
|
25
|
+
// src/schemas/output.ts
|
|
26
|
+
import { z as z2 } from "zod";
|
|
27
|
+
var OutputKindSchema = z2.enum([
|
|
28
|
+
"kpi",
|
|
29
|
+
"timeseries",
|
|
30
|
+
"distribution",
|
|
31
|
+
"table",
|
|
32
|
+
"leaderboard",
|
|
33
|
+
"pivot"
|
|
34
|
+
]);
|
|
35
|
+
var MetricUnitSchema = z2.enum([
|
|
36
|
+
"DKK",
|
|
37
|
+
"EUR",
|
|
38
|
+
"USD",
|
|
39
|
+
"GBP",
|
|
40
|
+
"SEK",
|
|
41
|
+
"NOK",
|
|
42
|
+
"PERCENTAGE"
|
|
43
|
+
]);
|
|
44
|
+
var KpiOutputSchema = z2.object({
|
|
45
|
+
kind: z2.literal("kpi"),
|
|
46
|
+
value: z2.number(),
|
|
47
|
+
label: z2.string().optional(),
|
|
48
|
+
unit: MetricUnitSchema.optional(),
|
|
49
|
+
prefix: z2.string().optional(),
|
|
50
|
+
suffix: z2.string().optional(),
|
|
51
|
+
trend: z2.enum(["up", "down", "flat"]).optional()
|
|
52
|
+
});
|
|
53
|
+
function defineKpiOutput(output) {
|
|
54
|
+
return {
|
|
55
|
+
kind: "kpi",
|
|
56
|
+
...output
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
var TimeSeriesPointSchema = z2.object({
|
|
60
|
+
ts: z2.date(),
|
|
61
|
+
value: z2.number(),
|
|
62
|
+
label: z2.string().optional()
|
|
63
|
+
});
|
|
64
|
+
var TimeSeriesSeriesSchema = z2.object({
|
|
65
|
+
key: z2.string(),
|
|
66
|
+
label: z2.string().optional(),
|
|
67
|
+
points: z2.array(TimeSeriesPointSchema),
|
|
68
|
+
chartType: z2.enum(["line", "bar"]).optional(),
|
|
69
|
+
axis: z2.enum(["left", "right"]).optional(),
|
|
70
|
+
meta: z2.record(z2.string(), z2.unknown()).optional()
|
|
71
|
+
});
|
|
72
|
+
var TimeSeriesOutputSchema = z2.object({
|
|
73
|
+
kind: z2.literal("timeseries"),
|
|
74
|
+
granularity: TimeGranularitySchema,
|
|
75
|
+
series: z2.array(TimeSeriesSeriesSchema)
|
|
76
|
+
});
|
|
77
|
+
function defineTimeSeriesOutput(output) {
|
|
78
|
+
if (Array.isArray(output.series)) {
|
|
79
|
+
return {
|
|
80
|
+
kind: "timeseries",
|
|
81
|
+
...output,
|
|
82
|
+
series: [...output.series]
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
kind: "timeseries",
|
|
87
|
+
...output,
|
|
88
|
+
series: Object.entries(output.series).map(([key, series]) => ({
|
|
89
|
+
key,
|
|
90
|
+
...series
|
|
91
|
+
}))
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
var DistributionSegmentSchema = z2.object({
|
|
95
|
+
key: z2.string(),
|
|
96
|
+
label: z2.string(),
|
|
97
|
+
value: z2.number(),
|
|
98
|
+
percent: z2.number().optional()
|
|
99
|
+
});
|
|
100
|
+
var DistributionChartTypeSchema = z2.enum([
|
|
101
|
+
"bar",
|
|
102
|
+
"donut",
|
|
103
|
+
"pie",
|
|
104
|
+
"funnel"
|
|
105
|
+
]);
|
|
106
|
+
var DistributionOutputSchema = z2.object({
|
|
107
|
+
kind: z2.literal("distribution"),
|
|
108
|
+
total: z2.number(),
|
|
109
|
+
segments: z2.array(DistributionSegmentSchema),
|
|
110
|
+
chartType: DistributionChartTypeSchema.optional()
|
|
111
|
+
});
|
|
112
|
+
function defineDistributionOutput(output) {
|
|
113
|
+
return {
|
|
114
|
+
kind: "distribution",
|
|
115
|
+
...output,
|
|
116
|
+
segments: [...output.segments]
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
var TableColumnSchema = z2.object({
|
|
120
|
+
key: z2.string(),
|
|
121
|
+
label: z2.string(),
|
|
122
|
+
type: z2.enum(["string", "number", "date", "boolean"]).optional(),
|
|
123
|
+
nullable: z2.boolean().optional()
|
|
124
|
+
});
|
|
125
|
+
var TableOutputSchema = z2.object({
|
|
126
|
+
kind: z2.literal("table"),
|
|
127
|
+
columns: z2.array(TableColumnSchema),
|
|
128
|
+
rows: z2.array(z2.record(z2.string(), z2.unknown())),
|
|
129
|
+
total: z2.number().optional()
|
|
130
|
+
});
|
|
131
|
+
function defineTableOutput(output) {
|
|
132
|
+
return {
|
|
133
|
+
kind: "table",
|
|
134
|
+
...output,
|
|
135
|
+
columns: [...output.columns],
|
|
136
|
+
rows: [...output.rows]
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
var PivotDimensionSchema = z2.object({
|
|
140
|
+
key: z2.string(),
|
|
141
|
+
label: z2.string()
|
|
142
|
+
});
|
|
143
|
+
var PivotTotalsSchema = z2.object({
|
|
144
|
+
rowTotals: z2.array(z2.number()).optional(),
|
|
145
|
+
columnTotals: z2.array(z2.number()).optional(),
|
|
146
|
+
grandTotal: z2.number().optional()
|
|
147
|
+
});
|
|
148
|
+
function addGrandTotalMismatchIssue(ctx, totalsName, totals, grandTotal) {
|
|
149
|
+
const totalsSum = totals?.reduce((total, value) => total + value, 0);
|
|
150
|
+
if (totalsSum === undefined)
|
|
151
|
+
return;
|
|
152
|
+
if (Math.abs(totalsSum - grandTotal) <= Number.EPSILON)
|
|
153
|
+
return;
|
|
154
|
+
ctx.addIssue({
|
|
155
|
+
code: z2.ZodIssueCode.custom,
|
|
156
|
+
path: ["totals", "grandTotal"],
|
|
157
|
+
message: `grandTotal must equal the sum of ${totalsName}`
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
function clonePivotTotals(totals) {
|
|
161
|
+
if (!totals)
|
|
162
|
+
return;
|
|
163
|
+
return {
|
|
164
|
+
rowTotals: totals.rowTotals ? [...totals.rowTotals] : undefined,
|
|
165
|
+
columnTotals: totals.columnTotals ? [...totals.columnTotals] : undefined,
|
|
166
|
+
grandTotal: totals.grandTotal
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function clonePivotCellTooltips(cellTooltips) {
|
|
170
|
+
if (!cellTooltips)
|
|
171
|
+
return;
|
|
172
|
+
return cellTooltips.map((row) => [...row]);
|
|
173
|
+
}
|
|
174
|
+
var PivotOutputSchema = z2.object({
|
|
175
|
+
kind: z2.literal("pivot"),
|
|
176
|
+
rowDimension: PivotDimensionSchema,
|
|
177
|
+
columnDimension: PivotDimensionSchema,
|
|
178
|
+
rows: z2.array(z2.string()),
|
|
179
|
+
columns: z2.array(z2.string()),
|
|
180
|
+
values: z2.array(z2.array(z2.number())),
|
|
181
|
+
cellTooltips: z2.array(z2.array(z2.string())).optional(),
|
|
182
|
+
totals: PivotTotalsSchema.optional()
|
|
183
|
+
}).superRefine((pivot, ctx) => {
|
|
184
|
+
const rowCount = pivot.rows.length;
|
|
185
|
+
const columnCount = pivot.columns.length;
|
|
186
|
+
if (pivot.values.length !== rowCount) {
|
|
187
|
+
ctx.addIssue({
|
|
188
|
+
code: z2.ZodIssueCode.custom,
|
|
189
|
+
path: ["values"],
|
|
190
|
+
message: `Pivot values must have ${rowCount} rows`
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
for (const [rowIndex, row] of pivot.values.entries()) {
|
|
194
|
+
if (row.length !== columnCount) {
|
|
195
|
+
ctx.addIssue({
|
|
196
|
+
code: z2.ZodIssueCode.custom,
|
|
197
|
+
path: ["values", rowIndex],
|
|
198
|
+
message: `Pivot row ${rowIndex} must have ${columnCount} columns`
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (pivot.cellTooltips !== undefined) {
|
|
203
|
+
if (pivot.cellTooltips.length !== rowCount) {
|
|
204
|
+
ctx.addIssue({
|
|
205
|
+
code: z2.ZodIssueCode.custom,
|
|
206
|
+
path: ["cellTooltips"],
|
|
207
|
+
message: `cellTooltips must have ${rowCount} rows`
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
for (const [rowIndex, row] of pivot.cellTooltips.entries()) {
|
|
211
|
+
if (row.length !== columnCount) {
|
|
212
|
+
ctx.addIssue({
|
|
213
|
+
code: z2.ZodIssueCode.custom,
|
|
214
|
+
path: ["cellTooltips", rowIndex],
|
|
215
|
+
message: `cellTooltips row ${rowIndex} must have ${columnCount} columns`
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (pivot.totals?.rowTotals && pivot.totals.rowTotals.length !== rowCount) {
|
|
221
|
+
ctx.addIssue({
|
|
222
|
+
code: z2.ZodIssueCode.custom,
|
|
223
|
+
path: ["totals", "rowTotals"],
|
|
224
|
+
message: `rowTotals must have ${rowCount} entries`
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (pivot.totals?.columnTotals && pivot.totals.columnTotals.length !== columnCount) {
|
|
228
|
+
ctx.addIssue({
|
|
229
|
+
code: z2.ZodIssueCode.custom,
|
|
230
|
+
path: ["totals", "columnTotals"],
|
|
231
|
+
message: `columnTotals must have ${columnCount} entries`
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (pivot.totals?.grandTotal !== undefined) {
|
|
235
|
+
addGrandTotalMismatchIssue(ctx, "rowTotals", pivot.totals.rowTotals, pivot.totals.grandTotal);
|
|
236
|
+
addGrandTotalMismatchIssue(ctx, "columnTotals", pivot.totals.columnTotals, pivot.totals.grandTotal);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
function definePivotOutput(output) {
|
|
240
|
+
return {
|
|
241
|
+
kind: "pivot",
|
|
242
|
+
...output,
|
|
243
|
+
rows: [...output.rows],
|
|
244
|
+
columns: [...output.columns],
|
|
245
|
+
values: output.values.map((row) => [...row]),
|
|
246
|
+
cellTooltips: clonePivotCellTooltips(output.cellTooltips),
|
|
247
|
+
totals: clonePivotTotals(output.totals)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
var LeaderboardItemSchema = z2.object({
|
|
251
|
+
rank: z2.number(),
|
|
252
|
+
id: z2.string(),
|
|
253
|
+
label: z2.string(),
|
|
254
|
+
value: z2.number(),
|
|
255
|
+
meta: z2.record(z2.string(), z2.unknown()).optional()
|
|
256
|
+
});
|
|
257
|
+
var LeaderboardOutputSchema = z2.object({
|
|
258
|
+
kind: z2.literal("leaderboard"),
|
|
259
|
+
items: z2.array(LeaderboardItemSchema),
|
|
260
|
+
total: z2.number().optional()
|
|
261
|
+
});
|
|
262
|
+
function defineLeaderboardOutput(output) {
|
|
263
|
+
return {
|
|
264
|
+
kind: "leaderboard",
|
|
265
|
+
...output,
|
|
266
|
+
items: [...output.items]
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function defineMetricOutput(kind, output) {
|
|
270
|
+
const handlers = {
|
|
271
|
+
kpi: () => defineKpiOutput(output),
|
|
272
|
+
timeseries: () => defineTimeSeriesOutput(output),
|
|
273
|
+
distribution: () => defineDistributionOutput(output),
|
|
274
|
+
table: () => defineTableOutput(output),
|
|
275
|
+
leaderboard: () => defineLeaderboardOutput(output),
|
|
276
|
+
pivot: () => definePivotOutput(output)
|
|
277
|
+
};
|
|
278
|
+
return handlers[kind]();
|
|
279
|
+
}
|
|
280
|
+
function validateOutput(kind, output) {
|
|
281
|
+
return OutputSchemaMap[kind].parse(output);
|
|
282
|
+
}
|
|
283
|
+
var MetricOutputSchema = z2.discriminatedUnion("kind", [
|
|
284
|
+
KpiOutputSchema,
|
|
285
|
+
TimeSeriesOutputSchema,
|
|
286
|
+
DistributionOutputSchema,
|
|
287
|
+
TableOutputSchema,
|
|
288
|
+
LeaderboardOutputSchema,
|
|
289
|
+
PivotOutputSchema
|
|
290
|
+
]);
|
|
291
|
+
var OutputSchemaMap = {
|
|
292
|
+
kpi: KpiOutputSchema,
|
|
293
|
+
timeseries: TimeSeriesOutputSchema,
|
|
294
|
+
distribution: DistributionOutputSchema,
|
|
295
|
+
table: TableOutputSchema,
|
|
296
|
+
leaderboard: LeaderboardOutputSchema,
|
|
297
|
+
pivot: PivotOutputSchema
|
|
298
|
+
};
|
|
299
|
+
var MetricExecutionCacheStatusSchema = z2.enum([
|
|
300
|
+
"hit",
|
|
301
|
+
"partialHit",
|
|
302
|
+
"miss",
|
|
303
|
+
"bypassed"
|
|
304
|
+
]);
|
|
305
|
+
var MetricExecutionSchema = z2.object({
|
|
306
|
+
cacheStatus: MetricExecutionCacheStatusSchema,
|
|
307
|
+
durationMs: z2.number().nonnegative(),
|
|
308
|
+
granularity: TimeGranularitySchema.optional()
|
|
309
|
+
});
|
|
310
|
+
var MetricResultSchema = z2.object({
|
|
311
|
+
current: MetricOutputSchema,
|
|
312
|
+
previous: MetricOutputSchema.optional(),
|
|
313
|
+
supportsTimeRange: z2.boolean(),
|
|
314
|
+
execution: MetricExecutionSchema.optional()
|
|
315
|
+
});
|
|
316
|
+
// src/schemas/inputs.ts
|
|
317
|
+
import { z as z3 } from "zod";
|
|
318
|
+
var BaseFiltersSchema = z3.object({
|
|
319
|
+
organizationIds: z3.array(z3.string()).optional()
|
|
320
|
+
}).merge(TimeRangeSchema);
|
|
321
|
+
var TimeSeriesFiltersSchema = z3.object({
|
|
322
|
+
organizationIds: z3.array(z3.string()).optional()
|
|
323
|
+
}).merge(RequiredTimeRangeSchema);
|
|
324
|
+
function extendBaseFilters(shape) {
|
|
325
|
+
const optionalShape = Object.fromEntries(Object.entries(shape).map(([key, schema]) => [
|
|
326
|
+
key,
|
|
327
|
+
schema.optional()
|
|
328
|
+
]));
|
|
329
|
+
return BaseFiltersSchema.extend(optionalShape);
|
|
330
|
+
}
|
|
331
|
+
function extendTimeSeriesFilters(shape) {
|
|
332
|
+
const optionalShape = Object.fromEntries(Object.entries(shape).map(([key, schema]) => [
|
|
333
|
+
key,
|
|
334
|
+
schema.optional()
|
|
335
|
+
]));
|
|
336
|
+
return TimeSeriesFiltersSchema.extend(optionalShape);
|
|
337
|
+
}
|
|
338
|
+
// src/schemas/metric-type.ts
|
|
339
|
+
import { z as z4 } from "zod";
|
|
340
|
+
var MetricTypeSchema = z4.enum(["TOTAL", "AVG", "PER_BUCKET"]);
|
|
341
|
+
// src/define-metric.ts
|
|
342
|
+
function defineMetricWithSchema(kind, outputSchema, def) {
|
|
343
|
+
return {
|
|
344
|
+
kind,
|
|
345
|
+
outputSchema,
|
|
346
|
+
...def
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function defineKpiMetric(def) {
|
|
350
|
+
return defineMetricWithSchema("kpi", KpiOutputSchema, def);
|
|
351
|
+
}
|
|
352
|
+
function defineTimeSeriesMetric(def) {
|
|
353
|
+
return defineMetricWithSchema("timeseries", TimeSeriesOutputSchema, def);
|
|
354
|
+
}
|
|
355
|
+
function defineDistributionMetric(def) {
|
|
356
|
+
return defineMetricWithSchema("distribution", DistributionOutputSchema, def);
|
|
357
|
+
}
|
|
358
|
+
function defineTableMetric(def) {
|
|
359
|
+
return defineMetricWithSchema("table", TableOutputSchema, def);
|
|
360
|
+
}
|
|
361
|
+
function defineLeaderboardMetric(def) {
|
|
362
|
+
return defineMetricWithSchema("leaderboard", LeaderboardOutputSchema, def);
|
|
363
|
+
}
|
|
364
|
+
function definePivotMetric(def) {
|
|
365
|
+
return defineMetricWithSchema("pivot", PivotOutputSchema, def);
|
|
366
|
+
}
|
|
367
|
+
function getOutputSchema(metric) {
|
|
368
|
+
return metric.outputSchema;
|
|
369
|
+
}
|
|
370
|
+
function validateMetricOutput(metric, output2) {
|
|
371
|
+
return getOutputSchema(metric).parse(output2);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/registry.ts
|
|
375
|
+
function createRegistry(metrics, options) {
|
|
376
|
+
const metricKeys = metrics.map((m) => m.key);
|
|
377
|
+
const MetricKeySchema = z5.enum(metricKeys);
|
|
378
|
+
const MetricRequestSchema = z5.object({
|
|
379
|
+
key: MetricKeySchema,
|
|
380
|
+
requestKey: z5.string().optional(),
|
|
381
|
+
filters: z5.record(z5.string(), z5.unknown()).optional()
|
|
382
|
+
});
|
|
383
|
+
const MetricsRequestSchema = z5.object({
|
|
384
|
+
metrics: z5.array(MetricRequestSchema).min(1, "At least one metric required"),
|
|
385
|
+
granularity: TimeGranularitySchema.optional(),
|
|
386
|
+
from: z5.date().optional(),
|
|
387
|
+
to: z5.date().optional(),
|
|
388
|
+
compareToPrevious: z5.boolean().optional().default(false),
|
|
389
|
+
disableCache: z5.boolean().optional().default(false)
|
|
390
|
+
});
|
|
391
|
+
const metricsByKey = Object.fromEntries(metrics.map((metric) => [metric.key, metric]));
|
|
392
|
+
const baseFilterSchema = options?.baseFilterSchema ?? BaseFiltersSchema;
|
|
393
|
+
const kindSchemas = options?.kindSchemas ?? OutputSchemaMap;
|
|
394
|
+
function getMetricByKey(key) {
|
|
395
|
+
return metricsByKey[key];
|
|
396
|
+
}
|
|
397
|
+
function parseMetricRequestInput(metricRequest, options2) {
|
|
398
|
+
const metric = getMetricByKey(metricRequest.key);
|
|
399
|
+
if (!metric) {
|
|
400
|
+
throw new Error(`Unknown metric: ${metricRequest.key}`);
|
|
401
|
+
}
|
|
402
|
+
const baseFilters = baseFilterSchema.parse({
|
|
403
|
+
from: options2?.from,
|
|
404
|
+
to: options2?.to,
|
|
405
|
+
...metricRequest.filters
|
|
406
|
+
});
|
|
407
|
+
return {
|
|
408
|
+
metric,
|
|
409
|
+
requestKey: getMetricResultKey(metricRequest),
|
|
410
|
+
filters: metric.filterSchema.parse(baseFilters)
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
metrics,
|
|
415
|
+
baseFilterSchema,
|
|
416
|
+
kindSchemas,
|
|
417
|
+
metricKeys,
|
|
418
|
+
MetricKeySchema,
|
|
419
|
+
MetricRequestSchema,
|
|
420
|
+
MetricsRequestSchema,
|
|
421
|
+
metricsByKey,
|
|
422
|
+
getMetricByKey,
|
|
423
|
+
parseMetricRequestInput,
|
|
424
|
+
getOutputSchema
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function getMetricResultKey(metricRequest) {
|
|
428
|
+
return metricRequest.requestKey ?? metricRequest.key;
|
|
429
|
+
}
|
|
430
|
+
function getMetric(result, key) {
|
|
431
|
+
return result.metrics[key];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/frontend/requests.ts
|
|
435
|
+
function defineMetricRequest(request) {
|
|
436
|
+
return request;
|
|
437
|
+
}
|
|
438
|
+
function defineMetricsRequest(request) {
|
|
439
|
+
return request;
|
|
440
|
+
}
|
|
441
|
+
function getMetricResult(result, key) {
|
|
442
|
+
return result.metrics[key];
|
|
443
|
+
}
|
|
444
|
+
// src/frontend/stream-state.ts
|
|
445
|
+
function getPendingMetricKeys(request) {
|
|
446
|
+
return request.metrics.map((metric) => getMetricResultKey(metric));
|
|
447
|
+
}
|
|
448
|
+
function createMetricsStreamState(request) {
|
|
449
|
+
return {
|
|
450
|
+
metrics: {},
|
|
451
|
+
errors: {},
|
|
452
|
+
status: "idle",
|
|
453
|
+
done: false,
|
|
454
|
+
receivedKeys: [],
|
|
455
|
+
pendingKeys: getPendingMetricKeys(request)
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function applyMetricResultChunk(state, chunk) {
|
|
459
|
+
const streamChunk = chunk;
|
|
460
|
+
const resultKey = getMetricResultKey(streamChunk);
|
|
461
|
+
const receivedKeys = state.receivedKeys.includes(resultKey) ? state.receivedKeys : [...state.receivedKeys, resultKey];
|
|
462
|
+
const errors = streamChunk.error ? {
|
|
463
|
+
...state.errors,
|
|
464
|
+
[resultKey]: streamChunk.error
|
|
465
|
+
} : state.errors;
|
|
466
|
+
const metrics = streamChunk.result ? {
|
|
467
|
+
...state.metrics,
|
|
468
|
+
[resultKey]: streamChunk.result
|
|
469
|
+
} : state.metrics;
|
|
470
|
+
const nextState = {
|
|
471
|
+
...state,
|
|
472
|
+
status: streamChunk.done ? "complete" : "streaming",
|
|
473
|
+
done: streamChunk.done,
|
|
474
|
+
receivedKeys,
|
|
475
|
+
pendingKeys: state.pendingKeys.filter((key) => key !== resultKey),
|
|
476
|
+
errors,
|
|
477
|
+
metrics
|
|
478
|
+
};
|
|
479
|
+
return nextState;
|
|
480
|
+
}
|
|
481
|
+
// src/frontend/catalog.ts
|
|
482
|
+
function createAvailableMetricMap(metrics) {
|
|
483
|
+
return Object.fromEntries(metrics.map((metric) => [metric.key, metric]));
|
|
484
|
+
}
|
|
485
|
+
function getAvailableMetric(metrics, key) {
|
|
486
|
+
return metrics.find((metric) => metric.key === key);
|
|
487
|
+
}
|
|
488
|
+
function getMetricFilterFields(metric) {
|
|
489
|
+
return metric?.filters ?? [];
|
|
490
|
+
}
|
|
491
|
+
function groupAvailableMetricsByKind(metrics) {
|
|
492
|
+
const groups = {};
|
|
493
|
+
for (const metric of metrics) {
|
|
494
|
+
const kind = metric.kind;
|
|
495
|
+
const group = groups[kind] ?? [];
|
|
496
|
+
groups[kind] = [...group, metric];
|
|
497
|
+
}
|
|
498
|
+
return groups;
|
|
499
|
+
}
|
|
500
|
+
// src/frontend/format.ts
|
|
501
|
+
var CURRENCY_UNITS = ["DKK", "EUR", "USD", "GBP", "SEK", "NOK"];
|
|
502
|
+
var formatterCache = new Map;
|
|
503
|
+
function getFormatter(locale, options = {}) {
|
|
504
|
+
const cacheKey = JSON.stringify([locale, options]);
|
|
505
|
+
const cached = formatterCache.get(cacheKey);
|
|
506
|
+
if (cached) {
|
|
507
|
+
return cached;
|
|
508
|
+
}
|
|
509
|
+
const formatter = new Intl.NumberFormat(locale, options);
|
|
510
|
+
formatterCache.set(cacheKey, formatter);
|
|
511
|
+
return formatter;
|
|
512
|
+
}
|
|
513
|
+
function formatMetricNumber(value, options = {}) {
|
|
514
|
+
return getFormatter(options.locale ?? "da-DK").format(value);
|
|
515
|
+
}
|
|
516
|
+
function formatMetricDecimal(value, options = {}) {
|
|
517
|
+
const safeValue = Number.isFinite(value) ? value : 0;
|
|
518
|
+
return getFormatter(options.locale ?? "da-DK", {
|
|
519
|
+
maximumFractionDigits: options.maximumFractionDigits ?? 2
|
|
520
|
+
}).format(safeValue);
|
|
521
|
+
}
|
|
522
|
+
function formatMetricCurrency(value, currency, options = {}) {
|
|
523
|
+
return getFormatter(options.locale ?? "da-DK", {
|
|
524
|
+
style: "currency",
|
|
525
|
+
currency: currency ?? options.defaultCurrency ?? "DKK",
|
|
526
|
+
maximumFractionDigits: options.maximumFractionDigits ?? 0
|
|
527
|
+
}).format(value);
|
|
528
|
+
}
|
|
529
|
+
function formatKpiValue(value, unit, prefix, suffix, options = {}) {
|
|
530
|
+
if (unit && CURRENCY_UNITS.includes(unit)) {
|
|
531
|
+
return formatMetricCurrency(value, unit, options);
|
|
532
|
+
}
|
|
533
|
+
if (unit === "PERCENTAGE") {
|
|
534
|
+
return `${value.toFixed(options.maximumFractionDigits ?? 1)}%`;
|
|
535
|
+
}
|
|
536
|
+
const formatted = formatMetricNumber(value, options);
|
|
537
|
+
const leading = prefix ?? "";
|
|
538
|
+
const trailing = suffix ? ` ${suffix}` : "";
|
|
539
|
+
return `${leading}${formatted}${trailing}`;
|
|
540
|
+
}
|
|
541
|
+
// src/frontend/time.ts
|
|
542
|
+
function computeRangeLabel(from, to, options = {}) {
|
|
543
|
+
if (!from || !to)
|
|
544
|
+
return "";
|
|
545
|
+
const now = options.now ?? new Date;
|
|
546
|
+
const diffMs = to.getTime() - from.getTime();
|
|
547
|
+
const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
|
|
548
|
+
const toIsNow = Math.abs(now.getTime() - to.getTime()) < 1000 * 60 * 60 * 24;
|
|
549
|
+
if (toIsNow) {
|
|
550
|
+
if (diffDays <= 1)
|
|
551
|
+
return "Last 24 hours";
|
|
552
|
+
if (diffDays <= 7)
|
|
553
|
+
return "Last 7 days";
|
|
554
|
+
if (diffDays <= 14)
|
|
555
|
+
return "Last 14 days";
|
|
556
|
+
if (diffDays <= 30)
|
|
557
|
+
return "Last 30 days";
|
|
558
|
+
if (diffDays <= 60)
|
|
559
|
+
return "Last 60 days";
|
|
560
|
+
if (diffDays <= 90)
|
|
561
|
+
return "Last 90 days";
|
|
562
|
+
if (diffDays <= 180)
|
|
563
|
+
return "Last 6 months";
|
|
564
|
+
if (diffDays <= 365)
|
|
565
|
+
return "Last 12 months";
|
|
566
|
+
return `Last ${Math.round(diffDays / 365)} years`;
|
|
567
|
+
}
|
|
568
|
+
const locale = options.locale ?? "en-US";
|
|
569
|
+
const formatDate = (date) => date.toLocaleDateString(locale, { month: "short", day: "numeric" });
|
|
570
|
+
const fromYear = from.getFullYear();
|
|
571
|
+
const toYear = to.getFullYear();
|
|
572
|
+
if (fromYear === toYear) {
|
|
573
|
+
return `${formatDate(from)} - ${formatDate(to)}, ${fromYear}`;
|
|
574
|
+
}
|
|
575
|
+
return `${formatDate(from)}, ${fromYear} - ${formatDate(to)}, ${toYear}`;
|
|
576
|
+
}
|
|
577
|
+
// src/frontend/markers.ts
|
|
578
|
+
var SEVERITY_COLORS = {
|
|
579
|
+
info: "#3b82f6",
|
|
580
|
+
success: "#22c55e",
|
|
581
|
+
warning: "#f59e0b",
|
|
582
|
+
danger: "#ef4444"
|
|
583
|
+
};
|
|
584
|
+
var LOG_TYPE_CATEGORIES = {
|
|
585
|
+
UNIT_OTHER: "Notes",
|
|
586
|
+
UNIT_USER_CREATED: "Unit Created",
|
|
587
|
+
UNIT_STATUS: "Status Changes",
|
|
588
|
+
UNIT_INIT_PRODUCTION: "Production",
|
|
589
|
+
UNIT_ASSIGNED_TO_ORGANIZATION: "Assignment",
|
|
590
|
+
UNIT_ASSIGNED_TO_WAREHOUSE: "Warehouse",
|
|
591
|
+
UNIT_REMOVED_FROM_WAREHOUSE: "Warehouse",
|
|
592
|
+
UNIT_ASSIGNED_TO_PRODUCTION: "Production",
|
|
593
|
+
UNIT_MAINTENANCE_CREATED: "Maintenance",
|
|
594
|
+
UNIT_MAINTENANCE_DONE: "Maintenance",
|
|
595
|
+
UNIT_DESTROYED: "Destroyed",
|
|
596
|
+
UNIT_CHANGE_IMSI: "IMSI Change",
|
|
597
|
+
UNIT_CHANGED_PLACEMENT_TYPE: "Placement Type"
|
|
598
|
+
};
|
|
599
|
+
function getMarkerTypes(markers) {
|
|
600
|
+
return Array.from(new Set(markers.map((marker) => marker.type)));
|
|
601
|
+
}
|
|
602
|
+
function filterChartMarkers(markers, enabledTypes) {
|
|
603
|
+
return markers.filter((marker) => enabledTypes.has(marker.type));
|
|
604
|
+
}
|
|
605
|
+
function getMarkerCategories(markers, enabledTypes) {
|
|
606
|
+
const counts = new Map;
|
|
607
|
+
for (const marker of markers) {
|
|
608
|
+
const existing = counts.get(marker.type);
|
|
609
|
+
if (existing) {
|
|
610
|
+
existing.count++;
|
|
611
|
+
} else {
|
|
612
|
+
counts.set(marker.type, { label: marker.label, count: 1 });
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return Array.from(counts.entries()).map(([type, { label, count }]) => ({
|
|
616
|
+
type,
|
|
617
|
+
label,
|
|
618
|
+
count,
|
|
619
|
+
enabled: enabledTypes.has(type)
|
|
620
|
+
}));
|
|
621
|
+
}
|
|
622
|
+
function toggleMarkerType(enabledTypes, type) {
|
|
623
|
+
const next = new Set(enabledTypes);
|
|
624
|
+
if (next.has(type)) {
|
|
625
|
+
next.delete(type);
|
|
626
|
+
} else {
|
|
627
|
+
next.add(type);
|
|
628
|
+
}
|
|
629
|
+
return next;
|
|
630
|
+
}
|
|
631
|
+
function toggleAllMarkerTypes(markers, enabled) {
|
|
632
|
+
return enabled ? new Set(getMarkerTypes(markers)) : new Set;
|
|
633
|
+
}
|
|
634
|
+
function mapLogItemsToMarkers(items) {
|
|
635
|
+
return items.filter((item) => item.data != null).map((item) => ({
|
|
636
|
+
id: item.id,
|
|
637
|
+
timestamp: new Date(item.createdAt),
|
|
638
|
+
type: item.data.type,
|
|
639
|
+
label: LOG_TYPE_CATEGORIES[item.data.type] ?? item.data.type,
|
|
640
|
+
title: item.data.title,
|
|
641
|
+
description: item.data.description,
|
|
642
|
+
severity: item.data.severity,
|
|
643
|
+
icon: item.data.icon
|
|
644
|
+
}));
|
|
645
|
+
}
|
|
646
|
+
// src/frontend/dashboard.ts
|
|
647
|
+
function defineMetricsDashboard(config) {
|
|
648
|
+
return config;
|
|
649
|
+
}
|
|
650
|
+
function defineWidget(keyOrConfig, config) {
|
|
651
|
+
if (typeof keyOrConfig === "string") {
|
|
652
|
+
return {
|
|
653
|
+
key: keyOrConfig,
|
|
654
|
+
...config ?? {}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
return keyOrConfig;
|
|
658
|
+
}
|
|
659
|
+
function defineHeadline(title, description) {
|
|
660
|
+
return {
|
|
661
|
+
_type: "headline",
|
|
662
|
+
title,
|
|
663
|
+
description
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function defineSpacer(layout) {
|
|
667
|
+
return {
|
|
668
|
+
_type: "spacer",
|
|
669
|
+
layout
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
function defineCustomSlot(slotId, config) {
|
|
673
|
+
return {
|
|
674
|
+
_type: "slot",
|
|
675
|
+
slotId,
|
|
676
|
+
layout: config?.layout,
|
|
677
|
+
props: config?.props
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
var defaultDashboardControls = {
|
|
681
|
+
showTimeRange: true,
|
|
682
|
+
showGranularity: false,
|
|
683
|
+
showCompare: true,
|
|
684
|
+
defaultCompareToPrevious: false
|
|
685
|
+
};
|
|
686
|
+
// src/frontend/renderers.ts
|
|
687
|
+
function defineRendererRegistry(registry) {
|
|
688
|
+
return registry;
|
|
689
|
+
}
|
|
690
|
+
function hasCustomRenderer(metricKey, registry) {
|
|
691
|
+
return Boolean(registry?.byMetricKey?.[metricKey]);
|
|
692
|
+
}
|
|
693
|
+
function resolveRendererHandle(metricKey, kind, registry) {
|
|
694
|
+
const byMetricKey = registry?.byMetricKey?.[metricKey];
|
|
695
|
+
if (byMetricKey) {
|
|
696
|
+
return {
|
|
697
|
+
rendererId: byMetricKey,
|
|
698
|
+
source: "metric",
|
|
699
|
+
metricKey,
|
|
700
|
+
kind
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const byKind = registry?.byKind?.[kind];
|
|
704
|
+
if (byKind) {
|
|
705
|
+
return {
|
|
706
|
+
rendererId: byKind,
|
|
707
|
+
source: "kind",
|
|
708
|
+
metricKey,
|
|
709
|
+
kind
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
rendererId: `kind:${kind}`,
|
|
714
|
+
source: "fallback",
|
|
715
|
+
metricKey,
|
|
716
|
+
kind
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
export {
|
|
720
|
+
toggleMarkerType,
|
|
721
|
+
toggleAllMarkerTypes,
|
|
722
|
+
resolveRendererHandle,
|
|
723
|
+
mapLogItemsToMarkers,
|
|
724
|
+
hasCustomRenderer,
|
|
725
|
+
groupAvailableMetricsByKind,
|
|
726
|
+
getMetricResultKey as getRequestedMetricKey,
|
|
727
|
+
getPendingMetricKeys,
|
|
728
|
+
getMetricResult,
|
|
729
|
+
getMetricFilterFields,
|
|
730
|
+
getMarkerTypes,
|
|
731
|
+
getMarkerCategories,
|
|
732
|
+
getAvailableMetric,
|
|
733
|
+
formatMetricNumber,
|
|
734
|
+
formatMetricDecimal,
|
|
735
|
+
formatMetricCurrency,
|
|
736
|
+
formatKpiValue,
|
|
737
|
+
filterChartMarkers,
|
|
738
|
+
defineWidget,
|
|
739
|
+
defineSpacer,
|
|
740
|
+
defineRendererRegistry,
|
|
741
|
+
defineMetricsRequest,
|
|
742
|
+
defineMetricsDashboard,
|
|
743
|
+
defineMetricRequest,
|
|
744
|
+
defineHeadline,
|
|
745
|
+
defineCustomSlot,
|
|
746
|
+
defaultDashboardControls,
|
|
747
|
+
createMetricsStreamState,
|
|
748
|
+
createAvailableMetricMap,
|
|
749
|
+
computeRangeLabel,
|
|
750
|
+
applyMetricResultChunk,
|
|
751
|
+
SEVERITY_COLORS
|
|
752
|
+
};
|