@checkstack/healthcheck-frontend 0.2.0 → 0.4.0

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,8 +1,12 @@
1
- import { useEffect, useState, useMemo, useCallback } from "react";
2
- import { useApi, permissionApiRef } from "@checkstack/frontend-api";
3
- import { healthCheckApiRef } from "../api";
1
+ import { useMemo } from "react";
4
2
  import {
5
- permissions,
3
+ usePluginClient,
4
+ accessApiRef,
5
+ useApi,
6
+ } from "@checkstack/frontend-api";
7
+ import { HealthCheckApi } from "../api";
8
+ import {
9
+ healthCheckAccess,
6
10
  DEFAULT_RETENTION_CONFIG,
7
11
  type RetentionConfig,
8
12
  HEALTH_CHECK_RUN_COMPLETED,
@@ -36,10 +40,10 @@ interface UseHealthCheckDataResult {
36
40
  isAggregated: boolean;
37
41
  /** The resolved retention config */
38
42
  retentionConfig: RetentionConfig;
39
- /** Whether user has permission to view detailed data */
40
- hasPermission: boolean;
41
- /** Whether permission is still loading */
42
- permissionLoading: boolean;
43
+ /** Whether user has access to view detailed data */
44
+ hasAccess: boolean;
45
+ /** Whether access is still loading */
46
+ accessLoading: boolean;
43
47
  }
44
48
 
45
49
  /**
@@ -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, hasPermission } = useHealthCheckData({
56
- * systemId,
57
- * configurationId,
58
- * strategyId,
59
- * dateRange: { startDate, endDate },
60
- * });
61
- *
62
- * if (!hasPermission) return <NoPermissionMessage />;
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);
78
- const permissionApi = useApi(permissionApiRef);
79
-
80
- // Permission state
81
- const { allowed: hasPermission, loading: permissionLoading } =
82
- permissionApi.usePermission(permissions.healthCheckDetailsRead.id);
65
+ const healthCheckClient = usePluginClient(HealthCheckApi);
66
+ const accessApi = useApi(accessApiRef);
83
67
 
84
- // Retention config state
85
- const [retentionConfig, setRetentionConfig] = useState<RetentionConfig>(
86
- DEFAULT_RETENTION_CONFIG
68
+ // Access state
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,127 +78,107 @@ 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 (!hasPermission || permissionLoading || retentionLoading || isAggregated)
164
- return;
165
- fetchRawData(true);
166
- }, [
167
- fetchRawData,
168
- hasPermission,
169
- permissionLoading,
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 }) => {
176
- // Only refresh if we're in raw mode (not aggregated) and have permission
142
+ // Only refresh if we're in raw mode (not aggregated) and have access
177
143
  if (
178
144
  changedId === systemId &&
179
- hasPermission &&
180
- !permissionLoading &&
145
+ hasAccess &&
146
+ !accessLoading &&
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
- !hasPermission ||
192
- permissionLoading ||
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 permission
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
- hasPermission,
221
- permissionLoading,
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
- if (!hasPermission || permissionLoading || retentionLoading) {
181
+ if (!hasAccess || accessLoading || retentionLoading) {
231
182
  return undefined;
232
183
  }
233
184
 
@@ -249,8 +200,8 @@ export function useHealthCheckData({
249
200
  runs: rawRuns,
250
201
  };
251
202
  }, [
252
- hasPermission,
253
- permissionLoading,
203
+ hasAccess,
204
+ accessLoading,
254
205
  retentionLoading,
255
206
  isAggregated,
256
207
  systemId,
@@ -261,7 +212,7 @@ export function useHealthCheckData({
261
212
  ]);
262
213
 
263
214
  const loading =
264
- permissionLoading ||
215
+ accessLoading ||
265
216
  retentionLoading ||
266
217
  (isAggregated ? aggregatedLoading : rawLoading);
267
218
 
@@ -270,7 +221,7 @@ export function useHealthCheckData({
270
221
  loading,
271
222
  isAggregated,
272
223
  retentionConfig,
273
- hasPermission,
274
- permissionLoading,
224
+ hasAccess,
225
+ accessLoading,
275
226
  };
276
227
  }
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";
@@ -13,7 +10,7 @@ import { HealthCheckMenuItems } from "./components/HealthCheckMenuItems";
13
10
  import { HealthCheckSystemOverview } from "./components/HealthCheckSystemOverview";
14
11
  import { SystemHealthCheckAssignment } from "./components/SystemHealthCheckAssignment";
15
12
  import { SystemHealthBadge } from "./components/SystemHealthBadge";
16
- import { permissions } from "@checkstack/healthcheck-common";
13
+ import { healthCheckAccess } from "@checkstack/healthcheck-common";
17
14
  import { autoChartExtension } from "./auto-charts";
18
15
 
19
16
  import {
@@ -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
 
@@ -49,33 +45,23 @@ export default createFrontendPlugin({
49
45
  route: healthcheckRoutes.routes.config,
50
46
  element: <HealthCheckConfigPage />,
51
47
  title: "Health Checks",
52
- permission: permissions.healthCheckManage,
48
+ accessRule: healthCheckAccess.configuration.manage,
53
49
  },
54
50
  {
55
51
  route: healthcheckRoutes.routes.history,
56
52
  element: <HealthCheckHistoryPage />,
57
53
  title: "Health Check History",
58
- permission: permissions.healthCheckRead,
54
+ accessRule: healthCheckAccess.configuration.read,
59
55
  },
60
56
  {
61
57
  route: healthcheckRoutes.routes.historyDetail,
62
58
  element: <HealthCheckHistoryDetailPage />,
63
59
  title: "Health Check Detail",
64
- permission: permissions.healthCheckDetailsRead,
65
- },
66
- ],
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
- },
60
+ accessRule: healthCheckAccess.details,
77
61
  },
78
62
  ],
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
- permissionApiRef,
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
+ healthCheckAccess,
14
15
  } from "@checkstack/healthcheck-common";
15
16
  import { HealthCheckList } from "../components/HealthCheckList";
16
17
  import { HealthCheckEditor } from "../components/HealthCheckEditor";
17
- import { Button, ConfirmationModal, PageLayout } from "@checkstack/ui";
18
+ import {
19
+ Button,
20
+ ConfirmationModal,
21
+ PageLayout,
22
+ useToast,
23
+ } from "@checkstack/ui";
18
24
  import { Plus, History } from "lucide-react";
19
25
  import { Link } from "react-router-dom";
20
26
  import { resolveRoute } from "@checkstack/common";
21
27
 
22
28
  const HealthCheckConfigPageContent = () => {
23
- const api = useApi(healthCheckApiRef);
24
- const permissionApi = useApi(permissionApiRef);
29
+ const healthCheckClient = usePluginClient(HealthCheckApi);
30
+ const accessApi = useApi(accessApiRef);
31
+ const toast = useToast();
25
32
  const [searchParams, setSearchParams] = useSearchParams();
26
- const { allowed: canRead, loading: permissionLoading } =
27
- permissionApi.useResourcePermission("healthcheck", "read");
28
- const { allowed: canManage } = permissionApi.useResourcePermission(
29
- "healthcheck",
30
- "manage"
33
+ const { allowed: canRead, loading: accessLoading } = accessApi.useAccess(
34
+ healthCheckAccess.configuration.read
35
+ );
36
+ const { allowed: canManage } = accessApi.useAccess(
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 = () => {
@@ -113,7 +137,7 @@ const HealthCheckConfigPageContent = () => {
113
137
  <PageLayout
114
138
  title="Health Checks"
115
139
  subtitle="Manage health check configurations"
116
- loading={permissionLoading}
140
+ loading={accessLoading}
117
141
  allowed={canRead}
118
142
  actions={
119
143
  <div className="flex gap-2">
@@ -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,15 +1,20 @@
1
1
  import { useState } from "react";
2
2
  import {
3
- useApi,
4
3
  wrapInSuspense,
5
- permissionApiRef,
4
+ accessApiRef,
5
+ useApi,
6
+ usePluginClient,
6
7
  } from "@checkstack/frontend-api";
7
- import { healthCheckApiRef } from "../api";
8
- import { healthcheckRoutes } from "@checkstack/healthcheck-common";
8
+ import {
9
+ healthcheckRoutes,
10
+ healthCheckAccess,
11
+ HealthCheckApi,
12
+ } from "@checkstack/healthcheck-common";
9
13
  import { resolveRoute } from "@checkstack/common";
10
14
  import {
11
15
  PageLayout,
12
16
  usePagination,
17
+ usePaginationSync,
13
18
  Card,
14
19
  CardHeader,
15
20
  CardTitle,
@@ -31,33 +36,32 @@ const HealthCheckHistoryDetailPageContent = () => {
31
36
  configurationId: string;
32
37
  }>();
33
38
 
34
- const api = useApi(healthCheckApiRef);
35
- const permissionApi = useApi(permissionApiRef);
36
- const { allowed: canManage, loading: permissionLoading } =
37
- permissionApi.useResourcePermission("healthcheck", "manage");
39
+ const healthCheckClient = usePluginClient(HealthCheckApi);
40
+ const accessApi = useApi(accessApiRef);
41
+ const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
42
+ healthCheckAccess.configuration.manage
43
+ );
38
44
 
39
45
  const [dateRange, setDateRange] = useState<DateRange>(getDefaultDateRange);
40
46
 
41
- const {
42
- items: runs,
43
- loading,
44
- pagination,
45
- } = usePagination({
46
- fetchFn: (params) =>
47
- api.getDetailedHistory({
48
- systemId,
49
- configurationId,
50
- startDate: params.startDate,
51
- endDate: params.endDate,
52
- limit: params.limit,
53
- offset: params.offset,
54
- }),
55
- getItems: (response) => response.runs as HealthCheckRunDetailed[],
56
- getTotal: (response) => response.total,
57
- defaultLimit: 20,
58
- 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,
59
58
  });
60
59
 
60
+ // Sync total from response
61
+ usePaginationSync(pagination, data?.total);
62
+
63
+ const runs = (data?.runs ?? []) as HealthCheckRunDetailed[];
64
+
61
65
  return (
62
66
  <PageLayout
63
67
  title="Health Check Run History"
@@ -65,7 +69,7 @@ const HealthCheckHistoryDetailPageContent = () => {
65
69
  0,
66
70
  8
67
71
  )}...`}
68
- loading={permissionLoading}
72
+ loading={accessLoading}
69
73
  allowed={canManage}
70
74
  actions={
71
75
  <BackLink to={resolveRoute(healthcheckRoutes.routes.history)}>
@@ -85,7 +89,7 @@ const HealthCheckHistoryDetailPageContent = () => {
85
89
  />
86
90
  <HealthCheckRunsTable
87
91
  runs={runs}
88
- loading={loading}
92
+ loading={isLoading}
89
93
  emptyMessage="No health check runs found for this configuration."
90
94
  pagination={pagination}
91
95
  />