@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,6 +1,10 @@
1
- import { useEffect, useState, useMemo, useCallback } from "react";
2
- import { useApi, accessApiRef } from "@checkstack/frontend-api";
3
- import { healthCheckApiRef } from "../api";
1
+ import { useMemo } from "react";
2
+ import {
3
+ usePluginClient,
4
+ accessApiRef,
5
+ useApi,
6
+ } from "@checkstack/frontend-api";
7
+ import { HealthCheckApi } from "../api";
4
8
  import {
5
9
  healthCheckAccess,
6
10
  DEFAULT_RETENTION_CONFIG,
@@ -49,22 +53,6 @@ interface UseHealthCheckDataResult {
49
53
  * - The configured rawRetentionDays for the assignment
50
54
  *
51
55
  * Returns a ready-to-use context for HealthCheckDiagramSlot.
52
- *
53
- * @example
54
- * ```tsx
55
- * const { context, loading, hasAccess } = useHealthCheckData({
56
- * systemId,
57
- * configurationId,
58
- * strategyId,
59
- * dateRange: { startDate, endDate },
60
- * });
61
- *
62
- * if (!hasAccess) return <NoAccessMessage />;
63
- * if (loading) return <LoadingSpinner />;
64
- * if (!context) return null;
65
- *
66
- * return <ExtensionSlot slot={HealthCheckDiagramSlot} context={context} />;
67
- * ```
68
56
  */
69
57
  export function useHealthCheckData({
70
58
  systemId,
@@ -74,30 +62,13 @@ export function useHealthCheckData({
74
62
  limit = 100,
75
63
  offset = 0,
76
64
  }: UseHealthCheckDataProps): UseHealthCheckDataResult {
77
- const api = useApi(healthCheckApiRef);
65
+ const healthCheckClient = usePluginClient(HealthCheckApi);
78
66
  const accessApi = useApi(accessApiRef);
79
67
 
80
68
  // Access state
81
- const { allowed: hasAccess, loading: accessLoading } =
82
- accessApi.useAccess(healthCheckAccess.details);
83
-
84
- // Retention config state
85
- const [retentionConfig, setRetentionConfig] = useState<RetentionConfig>(
86
- DEFAULT_RETENTION_CONFIG
69
+ const { allowed: hasAccess, loading: accessLoading } = accessApi.useAccess(
70
+ healthCheckAccess.details
87
71
  );
88
- const [retentionLoading, setRetentionLoading] = useState(true);
89
-
90
- // Raw data state
91
- const [rawRuns, setRawRuns] = useState<
92
- TypedHealthCheckRun<Record<string, unknown>>[]
93
- >([]);
94
- const [rawLoading, setRawLoading] = useState(false);
95
-
96
- // Aggregated data state
97
- const [aggregatedBuckets, setAggregatedBuckets] = useState<
98
- TypedAggregatedBucket<Record<string, unknown>>[]
99
- >([]);
100
- const [aggregatedLoading, setAggregatedLoading] = useState(false);
101
72
 
102
73
  // Calculate date range in days
103
74
  const dateRangeDays = useMemo(() => {
@@ -107,69 +78,64 @@ export function useHealthCheckData({
107
78
  );
108
79
  }, [dateRange.startDate, dateRange.endDate]);
109
80
 
81
+ // Query: Fetch retention config
82
+ const { data: retentionData, isLoading: retentionLoading } =
83
+ healthCheckClient.getRetentionConfig.useQuery(
84
+ { systemId, configurationId },
85
+ { enabled: !!systemId && !!configurationId && hasAccess }
86
+ );
87
+
88
+ const retentionConfig =
89
+ retentionData?.retentionConfig ?? DEFAULT_RETENTION_CONFIG;
90
+
110
91
  // Determine if we should use aggregated data
111
92
  const isAggregated = dateRangeDays > retentionConfig.rawRetentionDays;
112
93
 
113
- // Fetch retention config on mount
114
- useEffect(() => {
115
- setRetentionLoading(true);
116
- api
117
- .getRetentionConfig({ systemId, configurationId })
118
- .then((response) =>
119
- setRetentionConfig(response.retentionConfig ?? DEFAULT_RETENTION_CONFIG)
120
- )
121
- .catch(() => {
122
- // Fall back to default on error
123
- setRetentionConfig(DEFAULT_RETENTION_CONFIG);
124
- })
125
- .finally(() => setRetentionLoading(false));
126
- }, [api, systemId, configurationId]);
127
-
128
- // Fetch raw data function - extracted for reuse by signal handler
129
- const fetchRawData = useCallback(
130
- (showLoading = true) => {
131
- if (showLoading) {
132
- setRawLoading(true);
133
- }
134
- api
135
- .getDetailedHistory({
136
- systemId,
137
- configurationId,
138
- startDate: dateRange.startDate,
139
- // Don't pass endDate for live updates - backend defaults to 'now'
140
- limit,
141
- offset,
142
- })
143
- .then((response) => {
144
- setRawRuns(
145
- response.runs.map((r) => ({
146
- id: r.id,
147
- configurationId,
148
- systemId,
149
- status: r.status,
150
- timestamp: r.timestamp,
151
- latencyMs: r.latencyMs,
152
- result: r.result,
153
- }))
154
- );
155
- })
156
- .finally(() => setRawLoading(false));
94
+ // Query: Fetch raw data (when in raw mode)
95
+ const {
96
+ data: rawData,
97
+ isLoading: rawLoading,
98
+ refetch: refetchRawData,
99
+ } = healthCheckClient.getDetailedHistory.useQuery(
100
+ {
101
+ systemId,
102
+ configurationId,
103
+ startDate: dateRange.startDate,
104
+ limit,
105
+ offset,
157
106
  },
158
- [api, systemId, configurationId, dateRange.startDate, limit, offset]
107
+ {
108
+ enabled:
109
+ !!systemId &&
110
+ !!configurationId &&
111
+ hasAccess &&
112
+ !accessLoading &&
113
+ !retentionLoading &&
114
+ !isAggregated,
115
+ }
159
116
  );
160
117
 
161
- // Fetch raw data when in raw mode
162
- useEffect(() => {
163
- if (!hasAccess || accessLoading || retentionLoading || isAggregated)
164
- return;
165
- fetchRawData(true);
166
- }, [
167
- fetchRawData,
168
- hasAccess,
169
- accessLoading,
170
- retentionLoading,
171
- isAggregated,
172
- ]);
118
+ // Query: Fetch aggregated data (when in aggregated mode)
119
+ const bucketSize = dateRangeDays > 30 ? "daily" : "hourly";
120
+ const { data: aggregatedData, isLoading: aggregatedLoading } =
121
+ healthCheckClient.getDetailedAggregatedHistory.useQuery(
122
+ {
123
+ systemId,
124
+ configurationId,
125
+ startDate: dateRange.startDate,
126
+ endDate: dateRange.endDate,
127
+ bucketSize,
128
+ },
129
+ {
130
+ enabled:
131
+ !!systemId &&
132
+ !!configurationId &&
133
+ hasAccess &&
134
+ !accessLoading &&
135
+ !retentionLoading &&
136
+ isAggregated,
137
+ }
138
+ );
173
139
 
174
140
  // Listen for realtime health check updates to refresh data silently
175
141
  useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
@@ -181,50 +147,35 @@ export function useHealthCheckData({
181
147
  !retentionLoading &&
182
148
  !isAggregated
183
149
  ) {
184
- fetchRawData(false);
150
+ void refetchRawData();
185
151
  }
186
152
  });
187
153
 
188
- // Fetch aggregated data when in aggregated mode
189
- useEffect(() => {
190
- if (
191
- !hasAccess ||
192
- accessLoading ||
193
- retentionLoading ||
194
- !isAggregated
195
- )
196
- return;
197
-
198
- setAggregatedLoading(true);
199
- // Use daily buckets for ranges > 30 days, hourly otherwise
200
- const bucketSize = dateRangeDays > 30 ? "daily" : "hourly";
201
- // Use detailed endpoint to get aggregatedResult since we have access
202
- api
203
- .getDetailedAggregatedHistory({
204
- systemId,
205
- configurationId,
206
- startDate: dateRange.startDate,
207
- endDate: dateRange.endDate,
208
- bucketSize,
209
- })
210
- .then((response) => {
211
- setAggregatedBuckets(
212
- response.buckets as TypedAggregatedBucket<Record<string, unknown>>[]
213
- );
214
- })
215
- .finally(() => setAggregatedLoading(false));
216
- }, [
217
- api,
218
- systemId,
219
- configurationId,
220
- hasAccess,
221
- accessLoading,
222
- retentionLoading,
223
- isAggregated,
224
- dateRangeDays,
225
- dateRange.startDate,
226
- dateRange.endDate,
227
- ]);
154
+ // Transform raw runs to typed format
155
+ const rawRuns = useMemo((): TypedHealthCheckRun<
156
+ Record<string, unknown>
157
+ >[] => {
158
+ if (!rawData?.runs) return [];
159
+ return rawData.runs.map((r) => ({
160
+ id: r.id,
161
+ configurationId,
162
+ systemId,
163
+ status: r.status,
164
+ timestamp: r.timestamp,
165
+ latencyMs: r.latencyMs,
166
+ result: r.result as Record<string, unknown>,
167
+ }));
168
+ }, [rawData, configurationId, systemId]);
169
+
170
+ // Transform aggregated buckets
171
+ const aggregatedBuckets = useMemo((): TypedAggregatedBucket<
172
+ Record<string, unknown>
173
+ >[] => {
174
+ if (!aggregatedData?.buckets) return [];
175
+ return aggregatedData.buckets as TypedAggregatedBucket<
176
+ Record<string, unknown>
177
+ >[];
178
+ }, [aggregatedData]);
228
179
 
229
180
  const context = useMemo((): HealthCheckDiagramSlotContext | undefined => {
230
181
  if (!hasAccess || accessLoading || retentionLoading) {
package/src/index.tsx CHANGED
@@ -1,11 +1,8 @@
1
1
  import {
2
2
  createFrontendPlugin,
3
3
  createSlotExtension,
4
- rpcApiRef,
5
- type ApiRef,
6
4
  UserMenuItemsSlot,
7
5
  } from "@checkstack/frontend-api";
8
- import { healthCheckApiRef, type HealthCheckApiClient } from "./api";
9
6
  import { HealthCheckConfigPage } from "./pages/HealthCheckConfigPage";
10
7
  import { HealthCheckHistoryPage } from "./pages/HealthCheckHistoryPage";
11
8
  import { HealthCheckHistoryDetailPage } from "./pages/HealthCheckHistoryDetailPage";
@@ -23,7 +20,6 @@ import {
23
20
  } from "@checkstack/catalog-common";
24
21
  import {
25
22
  healthcheckRoutes,
26
- HealthCheckApi,
27
23
  pluginMetadata,
28
24
  } from "@checkstack/healthcheck-common";
29
25
 
@@ -64,18 +60,8 @@ export default createFrontendPlugin({
64
60
  accessRule: healthCheckAccess.details,
65
61
  },
66
62
  ],
67
- apis: [
68
- {
69
- ref: healthCheckApiRef,
70
- factory: (deps: {
71
- get: <T>(ref: ApiRef<T>) => T;
72
- }): HealthCheckApiClient => {
73
- const rpcApi = deps.get(rpcApiRef);
74
- // HealthCheckApiClient is just the RPC contract - return it directly
75
- return rpcApi.forPlugin(HealthCheckApi);
76
- },
77
- },
78
- ],
63
+ // No APIs needed - components use usePluginClient() directly
64
+ apis: [],
79
65
  extensions: [
80
66
  createSlotExtension(UserMenuItemsSlot, {
81
67
  id: "healthcheck.user-menu.items",
@@ -1,39 +1,42 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import { useSearchParams } from "react-router-dom";
3
3
  import {
4
- useApi,
4
+ usePluginClient,
5
5
  wrapInSuspense,
6
6
  accessApiRef,
7
+ useApi,
7
8
  } from "@checkstack/frontend-api";
8
- import { healthCheckApiRef } from "../api";
9
+ import { HealthCheckApi } from "../api";
9
10
  import {
10
11
  HealthCheckConfiguration,
11
- HealthCheckStrategyDto,
12
12
  CreateHealthCheckConfiguration,
13
13
  healthcheckRoutes,
14
14
  healthCheckAccess,
15
15
  } from "@checkstack/healthcheck-common";
16
16
  import { HealthCheckList } from "../components/HealthCheckList";
17
17
  import { HealthCheckEditor } from "../components/HealthCheckEditor";
18
- import { Button, ConfirmationModal, PageLayout } from "@checkstack/ui";
18
+ import {
19
+ Button,
20
+ ConfirmationModal,
21
+ PageLayout,
22
+ useToast,
23
+ } from "@checkstack/ui";
19
24
  import { Plus, History } from "lucide-react";
20
25
  import { Link } from "react-router-dom";
21
26
  import { resolveRoute } from "@checkstack/common";
22
27
 
23
28
  const HealthCheckConfigPageContent = () => {
24
- const api = useApi(healthCheckApiRef);
29
+ const healthCheckClient = usePluginClient(HealthCheckApi);
25
30
  const accessApi = useApi(accessApiRef);
31
+ const toast = useToast();
26
32
  const [searchParams, setSearchParams] = useSearchParams();
27
- const { allowed: canRead, loading: accessLoading } =
28
- accessApi.useAccess(healthCheckAccess.configuration.read);
33
+ const { allowed: canRead, loading: accessLoading } = accessApi.useAccess(
34
+ healthCheckAccess.configuration.read
35
+ );
29
36
  const { allowed: canManage } = accessApi.useAccess(
30
37
  healthCheckAccess.configuration.manage
31
38
  );
32
39
 
33
- const [configurations, setConfigurations] = useState<
34
- HealthCheckConfiguration[]
35
- >([]);
36
- const [strategies, setStrategies] = useState<HealthCheckStrategyDto[]>([]);
37
40
  const [isEditorOpen, setIsEditorOpen] = useState(false);
38
41
  const [editingConfig, setEditingConfig] = useState<
39
42
  HealthCheckConfiguration | undefined
@@ -42,20 +45,17 @@ const HealthCheckConfigPageContent = () => {
42
45
  // Delete modal state
43
46
  const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
44
47
  const [idToDelete, setIdToDelete] = useState<string | undefined>();
45
- const [isDeleting, setIsDeleting] = useState(false);
46
-
47
- const fetchData = async () => {
48
- const [{ configurations: configs }, strats] = await Promise.all([
49
- api.getConfigurations(),
50
- api.getStrategies(),
51
- ]);
52
- setConfigurations(configs);
53
- setStrategies(strats);
54
- };
55
48
 
56
- useEffect(() => {
57
- fetchData();
58
- }, [api]);
49
+ // Fetch configurations with useQuery
50
+ const { data: configurationsData, refetch: refetchConfigurations } =
51
+ healthCheckClient.getConfigurations.useQuery({});
52
+
53
+ // Fetch strategies with useQuery
54
+ const { data: strategies = [] } = healthCheckClient.getStrategies.useQuery(
55
+ {}
56
+ );
57
+
58
+ const configurations = configurationsData?.configurations ?? [];
59
59
 
60
60
  // Handle ?action=create URL parameter (from command palette)
61
61
  useEffect(() => {
@@ -68,6 +68,38 @@ const HealthCheckConfigPageContent = () => {
68
68
  }
69
69
  }, [searchParams, canManage, setSearchParams]);
70
70
 
71
+ // Mutations
72
+ const createMutation = healthCheckClient.createConfiguration.useMutation({
73
+ onSuccess: () => {
74
+ setIsEditorOpen(false);
75
+ void refetchConfigurations();
76
+ },
77
+ onError: (error) => {
78
+ toast.error(error instanceof Error ? error.message : "Failed to create");
79
+ },
80
+ });
81
+
82
+ const updateMutation = healthCheckClient.updateConfiguration.useMutation({
83
+ onSuccess: () => {
84
+ setIsEditorOpen(false);
85
+ void refetchConfigurations();
86
+ },
87
+ onError: (error) => {
88
+ toast.error(error instanceof Error ? error.message : "Failed to update");
89
+ },
90
+ });
91
+
92
+ const deleteMutation = healthCheckClient.deleteConfiguration.useMutation({
93
+ onSuccess: () => {
94
+ setIsDeleteModalOpen(false);
95
+ setIdToDelete(undefined);
96
+ void refetchConfigurations();
97
+ },
98
+ onError: (error) => {
99
+ toast.error(error instanceof Error ? error.message : "Failed to delete");
100
+ },
101
+ });
102
+
71
103
  const handleCreate = () => {
72
104
  setEditingConfig(undefined);
73
105
  setIsEditorOpen(true);
@@ -83,25 +115,17 @@ const HealthCheckConfigPageContent = () => {
83
115
  setIsDeleteModalOpen(true);
84
116
  };
85
117
 
86
- const confirmDelete = async () => {
118
+ const confirmDelete = () => {
87
119
  if (!idToDelete) return;
88
- setIsDeleting(true);
89
- try {
90
- await api.deleteConfiguration(idToDelete);
91
- await fetchData();
92
- } finally {
93
- setIsDeleting(false);
94
- setIsDeleteModalOpen(false);
95
- setIdToDelete(undefined);
96
- }
120
+ deleteMutation.mutate(idToDelete);
97
121
  };
98
122
 
99
123
  const handleSave = async (data: CreateHealthCheckConfiguration) => {
100
- await (editingConfig
101
- ? api.updateConfiguration({ id: editingConfig.id, body: data })
102
- : api.createConfiguration(data));
103
- setIsEditorOpen(false);
104
- await fetchData();
124
+ if (editingConfig) {
125
+ updateMutation.mutate({ id: editingConfig.id, body: data });
126
+ } else {
127
+ createMutation.mutate(data);
128
+ }
105
129
  };
106
130
 
107
131
  const handleEditorClose = () => {
@@ -153,7 +177,7 @@ const HealthCheckConfigPageContent = () => {
153
177
  message="Are you sure you want to delete this health check configuration? This action cannot be undone."
154
178
  confirmText="Delete"
155
179
  variant="danger"
156
- isLoading={isDeleting}
180
+ isLoading={deleteMutation.isPending}
157
181
  />
158
182
  </PageLayout>
159
183
  );
@@ -1,18 +1,20 @@
1
1
  import { useState } from "react";
2
2
  import {
3
- useApi,
4
3
  wrapInSuspense,
5
4
  accessApiRef,
5
+ useApi,
6
+ usePluginClient,
6
7
  } from "@checkstack/frontend-api";
7
- import { healthCheckApiRef } from "../api";
8
8
  import {
9
9
  healthcheckRoutes,
10
10
  healthCheckAccess,
11
+ HealthCheckApi,
11
12
  } from "@checkstack/healthcheck-common";
12
13
  import { resolveRoute } from "@checkstack/common";
13
14
  import {
14
15
  PageLayout,
15
16
  usePagination,
17
+ usePaginationSync,
16
18
  Card,
17
19
  CardHeader,
18
20
  CardTitle,
@@ -34,33 +36,32 @@ const HealthCheckHistoryDetailPageContent = () => {
34
36
  configurationId: string;
35
37
  }>();
36
38
 
37
- const api = useApi(healthCheckApiRef);
39
+ const healthCheckClient = usePluginClient(HealthCheckApi);
38
40
  const accessApi = useApi(accessApiRef);
39
- const { allowed: canManage, loading: accessLoading } =
40
- accessApi.useAccess(healthCheckAccess.configuration.manage);
41
+ const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
42
+ healthCheckAccess.configuration.manage
43
+ );
41
44
 
42
45
  const [dateRange, setDateRange] = useState<DateRange>(getDefaultDateRange);
43
46
 
44
- const {
45
- items: runs,
46
- loading,
47
- pagination,
48
- } = usePagination({
49
- fetchFn: (params) =>
50
- api.getDetailedHistory({
51
- systemId,
52
- configurationId,
53
- startDate: params.startDate,
54
- endDate: params.endDate,
55
- limit: params.limit,
56
- offset: params.offset,
57
- }),
58
- getItems: (response) => response.runs as HealthCheckRunDetailed[],
59
- getTotal: (response) => response.total,
60
- defaultLimit: 20,
61
- extraParams: { startDate: dateRange.startDate, endDate: dateRange.endDate },
47
+ // Pagination state
48
+ const pagination = usePagination({ defaultLimit: 20 });
49
+
50
+ // Fetch data with useQuery
51
+ const { data, isLoading } = healthCheckClient.getDetailedHistory.useQuery({
52
+ systemId,
53
+ configurationId,
54
+ startDate: dateRange.startDate,
55
+ endDate: dateRange.endDate,
56
+ limit: pagination.limit,
57
+ offset: pagination.offset,
62
58
  });
63
59
 
60
+ // Sync total from response
61
+ usePaginationSync(pagination, data?.total);
62
+
63
+ const runs = (data?.runs ?? []) as HealthCheckRunDetailed[];
64
+
64
65
  return (
65
66
  <PageLayout
66
67
  title="Health Check Run History"
@@ -88,7 +89,7 @@ const HealthCheckHistoryDetailPageContent = () => {
88
89
  />
89
90
  <HealthCheckRunsTable
90
91
  runs={runs}
91
- loading={loading}
92
+ loading={isLoading}
92
93
  emptyMessage="No health check runs found for this configuration."
93
94
  pagination={pagination}
94
95
  />
@@ -1,12 +1,13 @@
1
1
  import {
2
- useApi,
3
2
  wrapInSuspense,
4
3
  accessApiRef,
4
+ useApi,
5
+ usePluginClient,
5
6
  } from "@checkstack/frontend-api";
6
- import { healthCheckApiRef } from "../api";
7
7
  import {
8
8
  PageLayout,
9
9
  usePagination,
10
+ usePaginationSync,
10
11
  Card,
11
12
  CardHeader,
12
13
  CardTitle,
@@ -16,29 +17,32 @@ import {
16
17
  HealthCheckRunsTable,
17
18
  type HealthCheckRunDetailed,
18
19
  } from "../components/HealthCheckRunsTable";
19
- import { healthCheckAccess } from "@checkstack/healthcheck-common";
20
+ import {
21
+ healthCheckAccess,
22
+ HealthCheckApi,
23
+ } from "@checkstack/healthcheck-common";
20
24
 
21
25
  const HealthCheckHistoryPageContent = () => {
22
- const api = useApi(healthCheckApiRef);
26
+ const healthCheckClient = usePluginClient(HealthCheckApi);
23
27
  const accessApi = useApi(accessApiRef);
24
- const { allowed: canManage, loading: accessLoading } =
25
- accessApi.useAccess(healthCheckAccess.configuration.manage);
28
+ const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
29
+ healthCheckAccess.configuration.manage
30
+ );
26
31
 
27
- const {
28
- items: runs,
29
- loading,
30
- pagination,
31
- } = usePagination({
32
- fetchFn: (params: { limit: number; offset: number }) =>
33
- api.getDetailedHistory({
34
- limit: params.limit,
35
- offset: params.offset,
36
- }),
37
- getItems: (response) => response.runs as HealthCheckRunDetailed[],
38
- getTotal: (response) => response.total,
39
- defaultLimit: 20,
32
+ // Pagination state
33
+ const pagination = usePagination({ defaultLimit: 20 });
34
+
35
+ // Fetch data with useQuery
36
+ const { data, isLoading } = healthCheckClient.getDetailedHistory.useQuery({
37
+ limit: pagination.limit,
38
+ offset: pagination.offset,
40
39
  });
41
40
 
41
+ // Sync total from response
42
+ usePaginationSync(pagination, data?.total);
43
+
44
+ const runs = (data?.runs ?? []) as HealthCheckRunDetailed[];
45
+
42
46
  return (
43
47
  <PageLayout
44
48
  title="Health Check History"
@@ -53,7 +57,7 @@ const HealthCheckHistoryPageContent = () => {
53
57
  <CardContent>
54
58
  <HealthCheckRunsTable
55
59
  runs={runs}
56
- loading={loading}
60
+ loading={isLoading}
57
61
  showFilterColumns
58
62
  pagination={pagination}
59
63
  />