@checkstack/healthcheck-frontend 0.3.0 → 0.4.1

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.
@@ -1,10 +1,11 @@
1
- import React, { useEffect, useState } from "react";
1
+ import React, { useState } from "react";
2
2
  import {
3
+ usePluginClient,
3
4
  useApi,
4
5
  type SlotContext,
5
6
  accessApiRef,
6
7
  } from "@checkstack/frontend-api";
7
- import { healthCheckApiRef, HealthCheckConfiguration } from "../api";
8
+ import { HealthCheckApi } from "@checkstack/healthcheck-common";
8
9
  import {
9
10
  Button,
10
11
  Dialog,
@@ -52,17 +53,19 @@ export const SystemHealthCheckAssignment: React.FC<Props> = ({
52
53
  systemId,
53
54
  systemName: _systemName,
54
55
  }) => {
55
- const api = useApi(healthCheckApiRef);
56
+ const healthCheckClient = usePluginClient(HealthCheckApi);
56
57
  const accessApi = useApi(accessApiRef);
57
58
  const { allowed: canManage } = accessApi.useAccess(
58
59
  healthCheckAccess.configuration.manage
59
60
  );
60
- const [configs, setConfigs] = useState<HealthCheckConfiguration[]>([]);
61
- const [associations, setAssociations] = useState<AssociationState[]>([]);
62
- const [loading, setLoading] = useState(true);
63
- const [saving, setSaving] = useState(false);
61
+ const toast = useToast();
62
+
63
+ // UI state
64
64
  const [isOpen, setIsOpen] = useState(false);
65
65
  const [selectedPanel, setSelectedPanel] = useState<SelectedPanel>();
66
+ const [localThresholds, setLocalThresholds] = useState<
67
+ Record<string, StateThresholds>
68
+ >({});
66
69
  const [retentionData, setRetentionData] = useState<
67
70
  Record<
68
71
  string,
@@ -71,82 +74,119 @@ export const SystemHealthCheckAssignment: React.FC<Props> = ({
71
74
  hourlyRetentionDays: number;
72
75
  dailyRetentionDays: number;
73
76
  isCustom: boolean;
74
- loading: boolean;
75
77
  }
76
78
  >
77
79
  >({});
78
- const toast = useToast();
79
80
 
80
- const loadData = async () => {
81
- setLoading(true);
82
- try {
83
- const [{ configurations: allConfigs }, systemAssociations] =
84
- await Promise.all([
85
- api.getConfigurations(),
86
- api.getSystemAssociations({ systemId }),
87
- ]);
88
- setConfigs(allConfigs);
89
- setAssociations(systemAssociations);
90
- } catch (error) {
91
- const message =
92
- error instanceof Error ? error.message : "Failed to load data";
93
- toast.error(message);
94
- } finally {
95
- setLoading(false);
96
- }
97
- };
81
+ // Query: Fetch configurations
82
+ const { data: configurationsData, isLoading: configsLoading } =
83
+ healthCheckClient.getConfigurations.useQuery({}, { enabled: isOpen });
84
+
85
+ // Query: Fetch associations
86
+ const {
87
+ data: associations = [],
88
+ isLoading: associationsLoading,
89
+ refetch: refetchAssociations,
90
+ } = healthCheckClient.getSystemAssociations.useQuery(
91
+ { systemId },
92
+ { enabled: true }
93
+ );
98
94
 
99
- // Load association count on mount (for button badge)
100
- useEffect(() => {
101
- api.getSystemAssociations({ systemId }).then(setAssociations);
102
- }, [api, systemId]);
95
+ // Query: Fetch retention config for selected panel
96
+ const selectedConfigId =
97
+ selectedPanel?.panel === "retention" ? selectedPanel.configId : undefined;
98
+ const { data: retentionConfigData } =
99
+ healthCheckClient.getRetentionConfig.useQuery(
100
+ { systemId, configurationId: selectedConfigId ?? "" },
101
+ { enabled: !!selectedConfigId && !retentionData[selectedConfigId] }
102
+ );
103
103
 
104
- // Load full data when dialog opens
105
- useEffect(() => {
106
- if (isOpen) {
107
- loadData();
104
+ // Update retention data when fetched
105
+ React.useEffect(() => {
106
+ if (
107
+ retentionConfigData &&
108
+ selectedConfigId &&
109
+ !retentionData[selectedConfigId]
110
+ ) {
111
+ setRetentionData((prev) => ({
112
+ ...prev,
113
+ [selectedConfigId]: {
114
+ rawRetentionDays:
115
+ retentionConfigData.retentionConfig?.rawRetentionDays ??
116
+ DEFAULT_RETENTION_CONFIG.rawRetentionDays,
117
+ hourlyRetentionDays:
118
+ retentionConfigData.retentionConfig?.hourlyRetentionDays ??
119
+ DEFAULT_RETENTION_CONFIG.hourlyRetentionDays,
120
+ dailyRetentionDays:
121
+ retentionConfigData.retentionConfig?.dailyRetentionDays ??
122
+ DEFAULT_RETENTION_CONFIG.dailyRetentionDays,
123
+ isCustom: !!retentionConfigData.retentionConfig,
124
+ },
125
+ }));
108
126
  }
109
- }, [systemId, isOpen]);
127
+ }, [retentionConfigData, selectedConfigId, retentionData]);
128
+
129
+ // Mutation: Associate system
130
+ const associateMutation = healthCheckClient.associateSystem.useMutation({
131
+ onSuccess: () => {
132
+ void refetchAssociations();
133
+ },
134
+ onError: (error) => {
135
+ toast.error(error instanceof Error ? error.message : "Failed to update");
136
+ },
137
+ });
138
+
139
+ // Mutation: Disassociate system
140
+ const disassociateMutation = healthCheckClient.disassociateSystem.useMutation(
141
+ {
142
+ onSuccess: () => {
143
+ void refetchAssociations();
144
+ },
145
+ onError: (error) => {
146
+ toast.error(
147
+ error instanceof Error ? error.message : "Failed to update"
148
+ );
149
+ },
150
+ }
151
+ );
152
+
153
+ // Mutation: Update retention config
154
+ const updateRetentionMutation =
155
+ healthCheckClient.updateRetentionConfig.useMutation({
156
+ onSuccess: () => {
157
+ toast.success("Retention settings saved");
158
+ setSelectedPanel(undefined);
159
+ },
160
+ onError: (error) => {
161
+ toast.error(error instanceof Error ? error.message : "Failed to save");
162
+ },
163
+ });
164
+
165
+ const configs = configurationsData?.configurations ?? [];
166
+ const loading = configsLoading || associationsLoading;
167
+ const saving =
168
+ associateMutation.isPending ||
169
+ disassociateMutation.isPending ||
170
+ updateRetentionMutation.isPending;
110
171
 
111
- const handleToggleAssignment = async (
172
+ const handleToggleAssignment = (
112
173
  configId: string,
113
174
  isCurrentlyAssigned: boolean
114
175
  ) => {
115
176
  const config = configs.find((c) => c.id === configId);
116
177
  if (!config) return;
117
178
 
118
- setSaving(true);
119
- try {
120
- if (isCurrentlyAssigned) {
121
- await api.disassociateSystem({ systemId, configId });
122
- setAssociations((prev) =>
123
- prev.filter((a) => a.configurationId !== configId)
124
- );
125
- } else {
126
- await api.associateSystem({
127
- systemId,
128
- body: {
129
- configurationId: configId,
130
- enabled: true,
131
- stateThresholds: DEFAULT_STATE_THRESHOLDS,
132
- },
133
- });
134
- setAssociations((prev) => [
135
- ...prev,
136
- {
137
- configurationId: configId,
138
- configurationName: config.name,
139
- enabled: true,
140
- stateThresholds: DEFAULT_STATE_THRESHOLDS,
141
- },
142
- ]);
143
- }
144
- } catch (error) {
145
- const message =
146
- error instanceof Error ? error.message : "Failed to update";
147
- toast.error(message);
148
- } finally {
149
- setSaving(false);
179
+ if (isCurrentlyAssigned) {
180
+ disassociateMutation.mutate({ systemId, configId });
181
+ } else {
182
+ associateMutation.mutate({
183
+ systemId,
184
+ body: {
185
+ configurationId: configId,
186
+ enabled: true,
187
+ stateThresholds: DEFAULT_STATE_THRESHOLDS,
188
+ },
189
+ });
150
190
  }
151
191
  };
152
192
 
@@ -154,43 +194,103 @@ export const SystemHealthCheckAssignment: React.FC<Props> = ({
154
194
  configId: string,
155
195
  thresholds: StateThresholds
156
196
  ) => {
157
- setAssociations((prev) =>
158
- prev.map((a) =>
159
- a.configurationId === configId
160
- ? { ...a, stateThresholds: thresholds }
161
- : a
162
- )
163
- );
197
+ setLocalThresholds((prev) => ({
198
+ ...prev,
199
+ [configId]: thresholds,
200
+ }));
164
201
  };
165
202
 
166
- const handleSaveThresholds = async (configId: string) => {
203
+ const handleSaveThresholds = (configId: string) => {
167
204
  const assoc = associations.find((a) => a.configurationId === configId);
205
+ const thresholds = localThresholds[configId] ?? assoc?.stateThresholds;
168
206
  if (!assoc) return;
169
207
 
170
- setSaving(true);
171
- try {
172
- await api.associateSystem({
208
+ associateMutation.mutate(
209
+ {
173
210
  systemId,
174
211
  body: {
175
212
  configurationId: configId,
176
213
  enabled: assoc.enabled,
177
- stateThresholds: assoc.stateThresholds,
214
+ stateThresholds: thresholds,
178
215
  },
179
- });
180
- toast.success("Thresholds saved");
181
- setSelectedPanel(undefined);
182
- } catch (error) {
183
- const message = error instanceof Error ? error.message : "Failed to save";
184
- toast.error(message);
185
- } finally {
186
- setSaving(false);
187
- }
216
+ },
217
+ {
218
+ onSuccess: () => {
219
+ toast.success("Thresholds saved");
220
+ setSelectedPanel(undefined);
221
+ setLocalThresholds((prev) => {
222
+ const next = { ...prev };
223
+ delete next[configId];
224
+ return next;
225
+ });
226
+ },
227
+ }
228
+ );
229
+ };
230
+
231
+ const handleSaveRetention = (configId: string) => {
232
+ const data = retentionData[configId];
233
+ if (!data) return;
234
+
235
+ updateRetentionMutation.mutate({
236
+ systemId,
237
+ configurationId: configId,
238
+ retentionConfig: {
239
+ rawRetentionDays: data.rawRetentionDays,
240
+ hourlyRetentionDays: data.hourlyRetentionDays,
241
+ dailyRetentionDays: data.dailyRetentionDays,
242
+ },
243
+ });
244
+ };
245
+
246
+ const handleResetRetention = (configId: string) => {
247
+ updateRetentionMutation.mutate(
248
+ {
249
+ systemId,
250
+ configurationId: configId,
251
+ // eslint-disable-next-line unicorn/no-null -- RPC contract uses nullable()
252
+ retentionConfig: null,
253
+ },
254
+ {
255
+ onSuccess: () => {
256
+ setRetentionData((prev) => ({
257
+ ...prev,
258
+ [configId]: {
259
+ rawRetentionDays: DEFAULT_RETENTION_CONFIG.rawRetentionDays,
260
+ hourlyRetentionDays: DEFAULT_RETENTION_CONFIG.hourlyRetentionDays,
261
+ dailyRetentionDays: DEFAULT_RETENTION_CONFIG.dailyRetentionDays,
262
+ isCustom: false,
263
+ },
264
+ }));
265
+ toast.success("Reset to defaults");
266
+ },
267
+ }
268
+ );
269
+ };
270
+
271
+ const updateRetentionField = (
272
+ configId: string,
273
+ field: string,
274
+ value: number
275
+ ) => {
276
+ setRetentionData((prev) => ({
277
+ ...prev,
278
+ [configId]: { ...prev[configId], [field]: value, isCustom: true },
279
+ }));
188
280
  };
189
281
 
190
282
  const assignedIds = associations.map((a) => a.configurationId);
191
283
 
284
+ const getEffectiveThresholds = (assoc: AssociationState): StateThresholds => {
285
+ return (
286
+ localThresholds[assoc.configurationId] ??
287
+ assoc.stateThresholds ??
288
+ DEFAULT_STATE_THRESHOLDS
289
+ );
290
+ };
291
+
192
292
  const renderThresholdEditor = (assoc: AssociationState) => {
193
- const thresholds = assoc.stateThresholds || DEFAULT_STATE_THRESHOLDS;
293
+ const thresholds = getEffectiveThresholds(assoc);
194
294
 
195
295
  return (
196
296
  <div className="mt-4 space-y-4">
@@ -455,129 +555,10 @@ export const SystemHealthCheckAssignment: React.FC<Props> = ({
455
555
  );
456
556
  };
457
557
 
458
- // Load retention data when retention panel is expanded
459
- const loadRetentionConfig = async (configId: string) => {
460
- if (retentionData[configId]) return; // Already loaded
461
-
462
- setRetentionData((prev) => ({
463
- ...prev,
464
- [configId]: {
465
- rawRetentionDays: DEFAULT_RETENTION_CONFIG.rawRetentionDays,
466
- hourlyRetentionDays: DEFAULT_RETENTION_CONFIG.hourlyRetentionDays,
467
- dailyRetentionDays: DEFAULT_RETENTION_CONFIG.dailyRetentionDays,
468
- isCustom: false,
469
- loading: true,
470
- },
471
- }));
472
-
473
- try {
474
- const response = await api.getRetentionConfig({
475
- systemId,
476
- configurationId: configId,
477
- });
478
- setRetentionData((prev) => ({
479
- ...prev,
480
- [configId]: {
481
- rawRetentionDays:
482
- response.retentionConfig?.rawRetentionDays ??
483
- DEFAULT_RETENTION_CONFIG.rawRetentionDays,
484
- hourlyRetentionDays:
485
- response.retentionConfig?.hourlyRetentionDays ??
486
- DEFAULT_RETENTION_CONFIG.hourlyRetentionDays,
487
- dailyRetentionDays:
488
- response.retentionConfig?.dailyRetentionDays ??
489
- DEFAULT_RETENTION_CONFIG.dailyRetentionDays,
490
- isCustom: !!response.retentionConfig,
491
- loading: false,
492
- },
493
- }));
494
- } catch {
495
- setRetentionData((prev) => ({
496
- ...prev,
497
- [configId]: { ...prev[configId], loading: false },
498
- }));
499
- }
500
- };
501
-
502
- const handleSaveRetention = async (configId: string) => {
503
- const data = retentionData[configId];
504
- if (!data) return;
505
-
506
- setSaving(true);
507
- try {
508
- await api.updateRetentionConfig({
509
- systemId,
510
- configurationId: configId,
511
- retentionConfig: {
512
- rawRetentionDays: data.rawRetentionDays,
513
- hourlyRetentionDays: data.hourlyRetentionDays,
514
- dailyRetentionDays: data.dailyRetentionDays,
515
- },
516
- });
517
- toast.success("Retention settings saved");
518
- setSelectedPanel(undefined);
519
- } catch (error) {
520
- const message = error instanceof Error ? error.message : "Failed to save";
521
- toast.error(message);
522
- } finally {
523
- setSaving(false);
524
- }
525
- };
526
-
527
- const handleResetRetention = async (configId: string) => {
528
- setSaving(true);
529
- try {
530
- await api.updateRetentionConfig({
531
- systemId,
532
- configurationId: configId,
533
- // eslint-disable-next-line unicorn/no-null -- RPC contract uses nullable()
534
- retentionConfig: null,
535
- });
536
- setRetentionData((prev) => ({
537
- ...prev,
538
- [configId]: {
539
- rawRetentionDays: DEFAULT_RETENTION_CONFIG.rawRetentionDays,
540
- hourlyRetentionDays: DEFAULT_RETENTION_CONFIG.hourlyRetentionDays,
541
- dailyRetentionDays: DEFAULT_RETENTION_CONFIG.dailyRetentionDays,
542
- isCustom: false,
543
- loading: false,
544
- },
545
- }));
546
- toast.success("Reset to defaults");
547
- } catch (error) {
548
- const message =
549
- error instanceof Error ? error.message : "Failed to reset";
550
- toast.error(message);
551
- } finally {
552
- setSaving(false);
553
- }
554
- };
555
-
556
- const updateRetentionField = (
557
- configId: string,
558
- field: string,
559
- value: number
560
- ) => {
561
- setRetentionData((prev) => ({
562
- ...prev,
563
- [configId]: { ...prev[configId], [field]: value, isCustom: true },
564
- }));
565
- };
566
-
567
558
  const renderRetentionEditor = (configId: string) => {
568
559
  const data = retentionData[configId];
569
560
 
570
- // Trigger load if not loaded
571
561
  if (!data) {
572
- loadRetentionConfig(configId);
573
- return (
574
- <div className="mt-4 flex justify-center py-4">
575
- <LoadingSpinner />
576
- </div>
577
- );
578
- }
579
-
580
- if (data.loading) {
581
562
  return (
582
563
  <div className="mt-4 flex justify-center py-4">
583
564
  <LoadingSpinner />
@@ -1,13 +1,11 @@
1
- import { useState, useEffect, useCallback } from "react";
2
- import { useApi } from "@checkstack/frontend-api";
3
- import { healthCheckApiRef } from "../api";
4
- import { CollectorDto } from "@checkstack/healthcheck-common";
1
+ import { usePluginClient } from "@checkstack/frontend-api";
2
+ import { CollectorDto, HealthCheckApi } from "@checkstack/healthcheck-common";
5
3
 
6
4
  interface UseCollectorsResult {
7
5
  collectors: CollectorDto[];
8
6
  loading: boolean;
9
7
  error: Error | undefined;
10
- refetch: () => Promise<void>;
8
+ refetch: () => void;
11
9
  }
12
10
 
13
11
  /**
@@ -15,38 +13,27 @@ interface UseCollectorsResult {
15
13
  * @param strategyId - The strategy ID to fetch collectors for
16
14
  */
17
15
  export function useCollectors(strategyId: string): UseCollectorsResult {
18
- const api = useApi(healthCheckApiRef);
19
- const [collectors, setCollectors] = useState<CollectorDto[]>([]);
20
- const [loading, setLoading] = useState(false);
21
- const [error, setError] = useState<Error>();
16
+ const healthCheckClient = usePluginClient(HealthCheckApi);
22
17
 
23
- const refetch = useCallback(async () => {
24
- if (!strategyId) {
25
- setCollectors([]);
26
- return;
27
- }
18
+ const {
19
+ data,
20
+ isLoading: loading,
21
+ error: queryError,
22
+ refetch,
23
+ } = healthCheckClient.getCollectors.useQuery(
24
+ { strategyId },
25
+ { enabled: !!strategyId }
26
+ );
28
27
 
29
- setLoading(true);
30
- setError(undefined);
31
- try {
32
- const result = await api.getCollectors({ strategyId });
33
- setCollectors(result);
34
- } catch (error_) {
35
- setError(
36
- error_ instanceof Error
37
- ? error_
38
- : new Error("Failed to fetch collectors")
39
- );
40
- } finally {
41
- setLoading(false);
42
- }
43
- }, [api, strategyId]);
28
+ const collectors = data ?? [];
29
+ const error = queryError instanceof Error ? queryError : undefined;
44
30
 
45
- useEffect(() => {
46
- refetch();
47
- }, [refetch]);
48
-
49
- return { collectors, loading, error, refetch };
31
+ return {
32
+ collectors,
33
+ loading,
34
+ error,
35
+ refetch: () => void refetch(),
36
+ };
50
37
  }
51
38
 
52
39
  /**