@topolo/sdk 0.2.6 → 0.2.8

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/src/widgets.ts CHANGED
@@ -1,12 +1,12 @@
1
- export const TOPOLO_WIDGET_ENDPOINT = '/api/widget';
1
+ export const TOPOLO_WIDGET_ENDPOINT = "/api/widget";
2
2
  export const TOPOLO_WIDGET_REFRESH_INTERVAL_MS = 60_000;
3
3
 
4
4
  export type TopoloWidgetType =
5
- | 'stats'
6
- | 'recent-activity'
7
- | 'quick-actions'
8
- | 'notifications'
9
- | 'chart';
5
+ | "stats"
6
+ | "recent-activity"
7
+ | "quick-actions"
8
+ | "notifications"
9
+ | "chart";
10
10
 
11
11
  export interface TopoloWidgetBase {
12
12
  serviceId: string;
@@ -14,9 +14,11 @@ export interface TopoloWidgetBase {
14
14
  serviceIcon?: string;
15
15
  type: TopoloWidgetType;
16
16
  lastUpdated: string;
17
+ snapshot?: TopoloWidgetSnapshotMetadata;
17
18
  }
18
19
 
19
20
  export interface TopoloWidgetMetric {
21
+ key?: string;
20
22
  label: string;
21
23
  value: string | number;
22
24
  change?: number;
@@ -24,8 +26,29 @@ export interface TopoloWidgetMetric {
24
26
  icon?: string;
25
27
  }
26
28
 
29
+ export interface TopoloWidgetSnapshotMetric {
30
+ key: string;
31
+ label: string;
32
+ value: number;
33
+ unit?: string;
34
+ change?: number;
35
+ changeLabel?: string;
36
+ }
37
+
38
+ export interface TopoloWidgetSnapshotMetadata {
39
+ presetKey?: string;
40
+ audience?: string;
41
+ scope?: string;
42
+ scopeLabel?: string;
43
+ generatedAt?: string;
44
+ summary?: string;
45
+ defaultMetricKeys?: string[];
46
+ availableMetrics?: TopoloWidgetSnapshotMetric[];
47
+ highlights?: string[];
48
+ }
49
+
27
50
  export interface TopoloStatsWidget extends TopoloWidgetBase {
28
- type: 'stats';
51
+ type: "stats";
29
52
  stats: TopoloWidgetMetric[];
30
53
  }
31
54
 
@@ -39,7 +62,7 @@ export interface TopoloWidgetActivityItem {
39
62
  }
40
63
 
41
64
  export interface TopoloRecentActivityWidget extends TopoloWidgetBase {
42
- type: 'recent-activity';
65
+ type: "recent-activity";
43
66
  activities: TopoloWidgetActivityItem[];
44
67
  }
45
68
 
@@ -52,7 +75,7 @@ export interface TopoloWidgetActionItem {
52
75
  }
53
76
 
54
77
  export interface TopoloQuickActionsWidget extends TopoloWidgetBase {
55
- type: 'quick-actions';
78
+ type: "quick-actions";
56
79
  actions: TopoloWidgetActionItem[];
57
80
  }
58
81
 
@@ -60,21 +83,21 @@ export interface TopoloWidgetNotificationItem {
60
83
  id: string;
61
84
  title: string;
62
85
  message: string;
63
- type: 'info' | 'warning' | 'error' | 'success';
86
+ type: "info" | "warning" | "error" | "success";
64
87
  timestamp: string;
65
88
  read: boolean;
66
89
  link?: string;
67
90
  }
68
91
 
69
92
  export interface TopoloNotificationsWidget extends TopoloWidgetBase {
70
- type: 'notifications';
93
+ type: "notifications";
71
94
  notifications: TopoloWidgetNotificationItem[];
72
95
  unreadCount: number;
73
96
  }
74
97
 
75
98
  export interface TopoloChartWidget extends TopoloWidgetBase {
76
- type: 'chart';
77
- chartType: 'line' | 'bar' | 'pie' | 'donut';
99
+ type: "chart";
100
+ chartType: "line" | "bar" | "pie" | "donut";
78
101
  title: string;
79
102
  data: {
80
103
  labels: string[];
@@ -91,9 +114,9 @@ export type TopoloWidget =
91
114
 
92
115
  type TopoloWidgetWithOptionalMetadata<T extends TopoloWidget> = Omit<
93
116
  T,
94
- 'serviceId' | 'serviceName' | 'lastUpdated'
117
+ "serviceId" | "serviceName" | "lastUpdated"
95
118
  > &
96
- Partial<Pick<TopoloWidgetBase, 'serviceId' | 'serviceName' | 'lastUpdated'>>;
119
+ Partial<Pick<TopoloWidgetBase, "serviceId" | "serviceName" | "lastUpdated">>;
97
120
 
98
121
  export type TopoloWidgetDraft = TopoloWidget extends infer Widget
99
122
  ? Widget extends TopoloWidget
@@ -129,33 +152,33 @@ export interface TopoloWidgetConfig {
129
152
  }
130
153
 
131
154
  export function defineTopoloStatsWidget(
132
- widget: Omit<TopoloStatsWidget, 'type'>
155
+ widget: Omit<TopoloStatsWidget, "type">,
133
156
  ): TopoloStatsWidget {
134
- return { ...widget, type: 'stats' };
157
+ return { ...widget, type: "stats" };
135
158
  }
136
159
 
137
160
  export function defineTopoloRecentActivityWidget(
138
- widget: Omit<TopoloRecentActivityWidget, 'type'>
161
+ widget: Omit<TopoloRecentActivityWidget, "type">,
139
162
  ): TopoloRecentActivityWidget {
140
- return { ...widget, type: 'recent-activity' };
163
+ return { ...widget, type: "recent-activity" };
141
164
  }
142
165
 
143
166
  export function defineTopoloQuickActionsWidget(
144
- widget: Omit<TopoloQuickActionsWidget, 'type'>
167
+ widget: Omit<TopoloQuickActionsWidget, "type">,
145
168
  ): TopoloQuickActionsWidget {
146
- return { ...widget, type: 'quick-actions' };
169
+ return { ...widget, type: "quick-actions" };
147
170
  }
148
171
 
149
172
  export function defineTopoloNotificationsWidget(
150
- widget: Omit<TopoloNotificationsWidget, 'type'>
173
+ widget: Omit<TopoloNotificationsWidget, "type">,
151
174
  ): TopoloNotificationsWidget {
152
- return { ...widget, type: 'notifications' };
175
+ return { ...widget, type: "notifications" };
153
176
  }
154
177
 
155
178
  export function defineTopoloChartWidget(
156
- widget: Omit<TopoloChartWidget, 'type'>
179
+ widget: Omit<TopoloChartWidget, "type">,
157
180
  ): TopoloChartWidget {
158
- return { ...widget, type: 'chart' };
181
+ return { ...widget, type: "chart" };
159
182
  }
160
183
 
161
184
  export function createTopoloWidgetResponse({
@@ -168,7 +191,7 @@ export function createTopoloWidgetResponse({
168
191
 
169
192
  return {
170
193
  success: true,
171
- widgets: widgets.map(widget => ({
194
+ widgets: widgets.map((widget) => ({
172
195
  ...widget,
173
196
  serviceId: widget.serviceId?.trim() || serviceId,
174
197
  serviceName: widget.serviceName?.trim() || serviceName,
@@ -177,7 +200,9 @@ export function createTopoloWidgetResponse({
177
200
  };
178
201
  }
179
202
 
180
- export function createTopoloWidgetError(error: string): TopoloWidgetApiResponse {
203
+ export function createTopoloWidgetError(
204
+ error: string,
205
+ ): TopoloWidgetApiResponse {
181
206
  return {
182
207
  success: false,
183
208
  widgets: [],
@@ -186,20 +211,20 @@ export function createTopoloWidgetError(error: string): TopoloWidgetApiResponse
186
211
  }
187
212
 
188
213
  export function validateTopoloWidgetResponse(
189
- response: unknown
214
+ response: unknown,
190
215
  ): TopoloWidgetValidationResult {
191
216
  const errors: string[] = [];
192
217
 
193
218
  if (!isRecord(response)) {
194
- return { ok: false, errors: ['response must be an object'] };
219
+ return { ok: false, errors: ["response must be an object"] };
195
220
  }
196
221
 
197
- if (typeof response.success !== 'boolean') {
198
- errors.push('success must be a boolean');
222
+ if (typeof response.success !== "boolean") {
223
+ errors.push("success must be a boolean");
199
224
  }
200
225
 
201
226
  if (!Array.isArray(response.widgets)) {
202
- errors.push('widgets must be an array');
227
+ errors.push("widgets must be an array");
203
228
  return { ok: errors.length === 0, errors };
204
229
  }
205
230
 
@@ -210,9 +235,9 @@ export function validateTopoloWidgetResponse(
210
235
  if (
211
236
  response.success === false &&
212
237
  response.error !== undefined &&
213
- typeof response.error !== 'string'
238
+ typeof response.error !== "string"
214
239
  ) {
215
- errors.push('error must be a string when provided');
240
+ errors.push("error must be a string when provided");
216
241
  }
217
242
 
218
243
  return { ok: errors.length === 0, errors };
@@ -227,24 +252,25 @@ function validateWidget(widget: unknown, path: string, errors: string[]) {
227
252
  validateNonEmptyString(widget.serviceId, `${path}.serviceId`, errors);
228
253
  validateNonEmptyString(widget.serviceName, `${path}.serviceName`, errors);
229
254
  validateNonEmptyString(widget.lastUpdated, `${path}.lastUpdated`, errors);
255
+ validateSnapshotMetadata(widget.snapshot, `${path}.snapshot`, errors);
230
256
 
231
257
  switch (widget.type) {
232
- case 'stats':
258
+ case "stats":
233
259
  validateArray(widget.stats, `${path}.stats`, errors);
234
260
  break;
235
- case 'recent-activity':
261
+ case "recent-activity":
236
262
  validateArray(widget.activities, `${path}.activities`, errors);
237
263
  break;
238
- case 'quick-actions':
264
+ case "quick-actions":
239
265
  validateArray(widget.actions, `${path}.actions`, errors);
240
266
  break;
241
- case 'notifications':
267
+ case "notifications":
242
268
  validateArray(widget.notifications, `${path}.notifications`, errors);
243
- if (typeof widget.unreadCount !== 'number') {
269
+ if (typeof widget.unreadCount !== "number") {
244
270
  errors.push(`${path}.unreadCount must be a number`);
245
271
  }
246
272
  break;
247
- case 'chart':
273
+ case "chart":
248
274
  validateNonEmptyString(widget.chartType, `${path}.chartType`, errors);
249
275
  validateNonEmptyString(widget.title, `${path}.title`, errors);
250
276
  if (!isRecord(widget.data)) {
@@ -259,12 +285,58 @@ function validateWidget(widget: unknown, path: string, errors: string[]) {
259
285
  }
260
286
  }
261
287
 
288
+ function validateSnapshotMetadata(
289
+ snapshot: unknown,
290
+ path: string,
291
+ errors: string[],
292
+ ) {
293
+ if (snapshot === undefined) {
294
+ return;
295
+ }
296
+ if (!isRecord(snapshot)) {
297
+ errors.push(`${path} must be an object when provided`);
298
+ return;
299
+ }
300
+ if (
301
+ snapshot.defaultMetricKeys !== undefined &&
302
+ !Array.isArray(snapshot.defaultMetricKeys)
303
+ ) {
304
+ errors.push(`${path}.defaultMetricKeys must be an array when provided`);
305
+ }
306
+ if (
307
+ snapshot.highlights !== undefined &&
308
+ !Array.isArray(snapshot.highlights)
309
+ ) {
310
+ errors.push(`${path}.highlights must be an array when provided`);
311
+ }
312
+ if (snapshot.availableMetrics === undefined) {
313
+ return;
314
+ }
315
+ if (!Array.isArray(snapshot.availableMetrics)) {
316
+ errors.push(`${path}.availableMetrics must be an array when provided`);
317
+ return;
318
+ }
319
+
320
+ snapshot.availableMetrics.forEach((metric, index) => {
321
+ const metricPath = `${path}.availableMetrics[${index}]`;
322
+ if (!isRecord(metric)) {
323
+ errors.push(`${metricPath} must be an object`);
324
+ return;
325
+ }
326
+ validateNonEmptyString(metric.key, `${metricPath}.key`, errors);
327
+ validateNonEmptyString(metric.label, `${metricPath}.label`, errors);
328
+ if (typeof metric.value !== "number" || Number.isNaN(metric.value)) {
329
+ errors.push(`${metricPath}.value must be a number`);
330
+ }
331
+ });
332
+ }
333
+
262
334
  function validateNonEmptyString(
263
335
  value: unknown,
264
336
  path: string,
265
- errors: string[]
337
+ errors: string[],
266
338
  ) {
267
- if (typeof value !== 'string' || value.trim().length === 0) {
339
+ if (typeof value !== "string" || value.trim().length === 0) {
268
340
  errors.push(`${path} must be a non-empty string`);
269
341
  }
270
342
  }
@@ -276,7 +348,7 @@ function validateArray(value: unknown, path: string, errors: string[]) {
276
348
  }
277
349
 
278
350
  function isRecord(value: unknown): value is Record<string, unknown> {
279
- return typeof value === 'object' && value !== null && !Array.isArray(value);
351
+ return typeof value === "object" && value !== null && !Array.isArray(value);
280
352
  }
281
353
 
282
354
  function toIsoTimestamp(input: Date | string | undefined): string {