@topolo/sdk 0.2.6 → 0.2.7

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