@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,83 @@
1
1
  # @checkstack/healthcheck-frontend
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [4eed42d]
8
+ - @checkstack/frontend-api@0.3.0
9
+ - @checkstack/dashboard-frontend@0.3.0
10
+ - @checkstack/auth-frontend@0.3.1
11
+ - @checkstack/catalog-common@1.2.1
12
+ - @checkstack/ui@0.2.2
13
+
14
+ ## 0.4.0
15
+
16
+ ### Minor Changes
17
+
18
+ - 7a23261: ## TanStack Query Integration
19
+
20
+ Migrated all frontend components to use `usePluginClient` hook with TanStack Query integration, replacing the legacy `forPlugin()` pattern.
21
+
22
+ ### New Features
23
+
24
+ - **`usePluginClient` hook**: Provides type-safe access to plugin APIs with `.useQuery()` and `.useMutation()` methods
25
+ - **Automatic request deduplication**: Multiple components requesting the same data share a single network request
26
+ - **Built-in caching**: Configurable stale time and cache duration per query
27
+ - **Loading/error states**: TanStack Query provides `isLoading`, `error`, `isRefetching` states automatically
28
+ - **Background refetching**: Stale data is automatically refreshed when components mount
29
+
30
+ ### Contract Changes
31
+
32
+ All RPC contracts now require `operationType: "query"` or `operationType: "mutation"` metadata:
33
+
34
+ ```typescript
35
+ const getItems = proc()
36
+ .meta({ operationType: "query", access: [access.read] })
37
+ .output(z.array(itemSchema))
38
+ .query();
39
+
40
+ const createItem = proc()
41
+ .meta({ operationType: "mutation", access: [access.manage] })
42
+ .input(createItemSchema)
43
+ .output(itemSchema)
44
+ .mutation();
45
+ ```
46
+
47
+ ### Migration
48
+
49
+ ```typescript
50
+ // Before (forPlugin pattern)
51
+ const api = useApi(myPluginApiRef);
52
+ const [items, setItems] = useState<Item[]>([]);
53
+ useEffect(() => {
54
+ api.getItems().then(setItems);
55
+ }, [api]);
56
+
57
+ // After (usePluginClient pattern)
58
+ const client = usePluginClient(MyPluginApi);
59
+ const { data: items, isLoading } = client.getItems.useQuery({});
60
+ ```
61
+
62
+ ### Bug Fixes
63
+
64
+ - Fixed `rpc.test.ts` test setup for middleware type inference
65
+ - Fixed `SearchDialog` to use `setQuery` instead of deprecated `search` method
66
+ - Fixed null→undefined warnings in notification and queue frontends
67
+
68
+ ### Patch Changes
69
+
70
+ - Updated dependencies [180be38]
71
+ - Updated dependencies [7a23261]
72
+ - @checkstack/dashboard-frontend@0.2.0
73
+ - @checkstack/frontend-api@0.2.0
74
+ - @checkstack/common@0.3.0
75
+ - @checkstack/auth-frontend@0.3.0
76
+ - @checkstack/catalog-common@1.2.0
77
+ - @checkstack/healthcheck-common@0.4.0
78
+ - @checkstack/ui@0.2.1
79
+ - @checkstack/signal-frontend@0.0.7
80
+
3
81
  ## 0.3.0
4
82
 
5
83
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-frontend",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "scripts": {
@@ -12,6 +12,7 @@
12
12
  "@checkstack/auth-frontend": "workspace:*",
13
13
  "@checkstack/catalog-common": "workspace:*",
14
14
  "@checkstack/common": "workspace:*",
15
+ "@checkstack/dashboard-frontend": "workspace:*",
15
16
  "@checkstack/frontend-api": "workspace:*",
16
17
  "@checkstack/healthcheck-common": "workspace:*",
17
18
  "@checkstack/signal-frontend": "workspace:*",
package/src/api.ts CHANGED
@@ -1,7 +1,3 @@
1
- import { createApiRef } from "@checkstack/frontend-api";
2
- import { HealthCheckApi } from "@checkstack/healthcheck-common";
3
- import type { InferClient } from "@checkstack/common";
4
-
5
1
  // Re-export types for convenience
6
2
  export type {
7
3
  HealthCheckConfiguration,
@@ -9,9 +5,5 @@ export type {
9
5
  HealthCheckRun,
10
6
  HealthCheckRunPublic,
11
7
  } from "@checkstack/healthcheck-common";
12
-
13
- // HealthCheckApiClient type inferred from the client definition
14
- export type HealthCheckApiClient = InferClient<typeof HealthCheckApi>;
15
-
16
- export const healthCheckApiRef =
17
- createApiRef<HealthCheckApiClient>("healthcheck-api");
8
+ // Client definition is in @checkstack/healthcheck-common - use with usePluginClient
9
+ export { HealthCheckApi } from "@checkstack/healthcheck-common";
@@ -7,8 +7,8 @@
7
7
  */
8
8
 
9
9
  import { useEffect, useState } from "react";
10
- import { useApi } from "@checkstack/frontend-api";
11
- import { healthCheckApiRef } from "../api";
10
+ import { usePluginClient } from "@checkstack/frontend-api";
11
+ import { HealthCheckApi } from "../api";
12
12
 
13
13
  interface StrategySchemas {
14
14
  resultSchema: Record<string, unknown> | undefined;
@@ -28,59 +28,50 @@ export function useStrategySchemas(strategyId: string): {
28
28
  schemas: StrategySchemas | undefined;
29
29
  loading: boolean;
30
30
  } {
31
- const api = useApi(healthCheckApiRef);
31
+ const healthCheckClient = usePluginClient(HealthCheckApi);
32
32
  const [schemas, setSchemas] = useState<StrategySchemas | undefined>();
33
33
  const [loading, setLoading] = useState(true);
34
34
 
35
+ // Fetch strategies with useQuery
36
+ const { data: strategies } = healthCheckClient.getStrategies.useQuery({});
37
+
38
+ // Fetch collectors with useQuery
39
+ const { data: collectors } = healthCheckClient.getCollectors.useQuery(
40
+ { strategyId },
41
+ { enabled: !!strategyId }
42
+ );
43
+
35
44
  useEffect(() => {
36
- let cancelled = false;
37
-
38
- async function fetchSchemas() {
39
- try {
40
- // Fetch strategy and collectors in parallel
41
- const [strategies, collectors] = await Promise.all([
42
- api.getStrategies(),
43
- api.getCollectors({ strategyId }),
44
- ]);
45
-
46
- const strategy = strategies.find((s) => s.id === strategyId);
47
-
48
- if (!cancelled && strategy) {
49
- // Build collector schemas object for nesting under resultSchema.properties.collectors
50
- const collectorProperties: Record<string, unknown> = {};
51
- for (const collector of collectors) {
52
- // Use full ID so it matches stored data keys like "healthcheck-http.request"
53
- collectorProperties[collector.id] = collector.resultSchema;
54
- }
55
-
56
- // Merge collector schemas into strategy result schema
57
- const mergedResultSchema = mergeCollectorSchemas(
58
- strategy.resultSchema as Record<string, unknown> | undefined,
59
- collectorProperties
60
- );
61
-
62
- setSchemas({
63
- resultSchema: mergedResultSchema,
64
- aggregatedResultSchema:
65
- (strategy.aggregatedResultSchema as Record<string, unknown>) ??
66
- undefined,
67
- });
68
- }
69
- } catch (error) {
70
- console.error("Failed to fetch strategy schemas:", error);
71
- } finally {
72
- if (!cancelled) {
73
- setLoading(false);
74
- }
75
- }
45
+ if (!strategies || !collectors) {
46
+ return;
76
47
  }
77
48
 
78
- fetchSchemas();
49
+ const strategy = strategies.find((s) => s.id === strategyId);
79
50
 
80
- return () => {
81
- cancelled = true;
82
- };
83
- }, [api, strategyId]);
51
+ if (strategy) {
52
+ // Build collector schemas object for nesting under resultSchema.properties.collectors
53
+ const collectorProperties: Record<string, unknown> = {};
54
+ for (const collector of collectors) {
55
+ // Use full ID so it matches stored data keys like "healthcheck-http.request"
56
+ collectorProperties[collector.id] = collector.resultSchema;
57
+ }
58
+
59
+ // Merge collector schemas into strategy result schema
60
+ const mergedResultSchema = mergeCollectorSchemas(
61
+ strategy.resultSchema as Record<string, unknown> | undefined,
62
+ collectorProperties
63
+ );
64
+
65
+ setSchemas({
66
+ resultSchema: mergedResultSchema,
67
+ aggregatedResultSchema:
68
+ (strategy.aggregatedResultSchema as Record<string, unknown>) ??
69
+ undefined,
70
+ });
71
+ }
72
+
73
+ setLoading(false);
74
+ }, [strategies, collectors, strategyId]);
84
75
 
85
76
  return { schemas, loading };
86
77
  }
@@ -1,6 +1,5 @@
1
- import React, { useEffect, useState } from "react";
2
- import { useApi, type SlotContext } from "@checkstack/frontend-api";
3
- import { healthCheckApiRef, HealthCheckRunPublic } from "../api";
1
+ import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
2
+ import { HealthCheckApi } from "../api";
4
3
  import { SystemDetailsSlot } from "@checkstack/catalog-common";
5
4
  import {
6
5
  Table,
@@ -25,18 +24,15 @@ export const HealthCheckHistory: React.FC<SlotProps> = (props) => {
25
24
  const { system, configurationId, limit } = props as Props;
26
25
  const systemId = system?.id;
27
26
 
28
- const healthCheckApi = useApi(healthCheckApiRef);
29
- const [history, setHistory] = useState<HealthCheckRunPublic[]>([]);
30
- const [loading, setLoading] = useState(true);
27
+ const healthCheckClient = usePluginClient(HealthCheckApi);
31
28
 
32
- useEffect(() => {
33
- // If it's used in a context that doesn't provide systemId or configurationId,
34
- // we might want to skip or handle it.
35
- healthCheckApi
36
- .getHistory({ systemId, configurationId, limit })
37
- .then((response) => setHistory(response.runs))
38
- .finally(() => setLoading(false));
39
- }, [healthCheckApi, systemId, configurationId, limit]);
29
+ // Fetch history with useQuery
30
+ const { data, isLoading: loading } = healthCheckClient.getHistory.useQuery(
31
+ { systemId, configurationId, limit },
32
+ { enabled: true }
33
+ );
34
+
35
+ const history = data?.runs ?? [];
40
36
 
41
37
  if (loading) return <LoadingSpinner />;
42
38
 
@@ -1,9 +1,11 @@
1
- import React, { useState } from "react";
2
- import { useApi, type SlotContext } from "@checkstack/frontend-api";
1
+ import React, { useState, useCallback } from "react";
2
+ import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
3
3
  import { useSignal } from "@checkstack/signal-frontend";
4
- import { healthCheckApiRef } from "../api";
5
4
  import { SystemDetailsSlot } from "@checkstack/catalog-common";
6
- import { HEALTH_CHECK_RUN_COMPLETED } from "@checkstack/healthcheck-common";
5
+ import {
6
+ HEALTH_CHECK_RUN_COMPLETED,
7
+ HealthCheckApi,
8
+ } from "@checkstack/healthcheck-common";
7
9
  import {
8
10
  HealthBadge,
9
11
  LoadingSpinner,
@@ -16,6 +18,7 @@ import {
16
18
  Tooltip,
17
19
  Pagination,
18
20
  usePagination,
21
+ usePaginationSync,
19
22
  DateRangeFilter,
20
23
  } from "@checkstack/ui";
21
24
  import { formatDistanceToNow } from "date-fns";
@@ -50,7 +53,7 @@ interface ExpandedRowProps {
50
53
  }
51
54
 
52
55
  const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
53
- const api = useApi(healthCheckApiRef);
56
+ const healthCheckClient = usePluginClient(HealthCheckApi);
54
57
 
55
58
  // Date range state for filtering
56
59
  const [dateRange, setDateRange] = useState<{
@@ -79,42 +82,33 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
79
82
  limit: 1000,
80
83
  });
81
84
 
82
- // Paginated history for the table
85
+ // Pagination state for history table
86
+ const pagination = usePagination({ defaultLimit: 10 });
87
+
88
+ // Fetch paginated history with useQuery
83
89
  const {
84
- items: runs,
85
- loading,
86
- pagination,
87
- } = usePagination({
88
- fetchFn: (params: {
89
- limit: number;
90
- offset: number;
91
- systemId: string;
92
- configurationId: string;
93
- startDate?: Date;
94
- }) =>
95
- api.getHistory({
96
- systemId: params.systemId,
97
- configurationId: params.configurationId,
98
- limit: params.limit,
99
- offset: params.offset,
100
- startDate: params.startDate,
101
- // Don't pass endDate - backend defaults to 'now' so new runs are included
102
- }),
103
- getItems: (response) => response.runs,
104
- getTotal: (response) => response.total,
105
- extraParams: {
106
- systemId,
107
- configurationId: item.configurationId,
108
- startDate: dateRange.startDate,
109
- },
110
- defaultLimit: 10,
90
+ data: historyData,
91
+ isLoading: loading,
92
+ refetch,
93
+ } = healthCheckClient.getHistory.useQuery({
94
+ systemId,
95
+ configurationId: item.configurationId,
96
+ limit: pagination.limit,
97
+ offset: pagination.offset,
98
+ startDate: dateRange.startDate,
99
+ // Don't pass endDate - backend defaults to 'now' so new runs are included
111
100
  });
112
101
 
102
+ // Sync total from response
103
+ usePaginationSync(pagination, historyData?.total);
104
+
105
+ const runs = historyData?.runs ?? [];
106
+
113
107
  // Listen for realtime health check updates to refresh history table
114
108
  // Charts are refreshed automatically by useHealthCheckData
115
109
  useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
116
110
  if (changedId === systemId) {
117
- pagination.silentRefetch();
111
+ void refetch();
118
112
  }
119
113
  });
120
114
 
@@ -248,100 +242,49 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
248
242
 
249
243
  export function HealthCheckSystemOverview(props: SlotProps) {
250
244
  const systemId = props.system.id;
251
- const api = useApi(healthCheckApiRef);
245
+ const healthCheckClient = usePluginClient(HealthCheckApi);
252
246
 
253
- // Fetch health check overview
254
- const [overview, setOverview] = React.useState<HealthCheckOverviewItem[]>([]);
255
- const [initialLoading, setInitialLoading] = React.useState(true);
256
247
  const [expandedRow, setExpandedRow] = React.useState<string | undefined>();
257
248
 
258
- const fetchOverview = React.useCallback(() => {
259
- api.getSystemHealthOverview({ systemId }).then((data) => {
260
- setOverview(
261
- data.checks.map((check) => ({
262
- configurationId: check.configurationId,
263
- strategyId: check.strategyId,
264
- name: check.configurationName,
265
- state: check.status,
266
- intervalSeconds: check.intervalSeconds,
267
- lastRunAt: check.recentRuns[0]?.timestamp
268
- ? new Date(check.recentRuns[0].timestamp)
269
- : undefined,
270
- stateThresholds: check.stateThresholds,
271
- recentStatusHistory: check.recentRuns.map((r) => r.status),
272
- }))
273
- );
274
- setInitialLoading(false);
275
- });
276
- }, [api, systemId]);
277
-
278
- React.useEffect(() => {
279
- fetchOverview();
280
- }, [fetchOverview]);
281
-
282
- // Listen for realtime health check updates - merge into existing state to avoid remounting expanded content
283
- useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
284
- if (changedId === systemId) {
285
- // Fetch fresh data but merge it into existing state to preserve object identity
286
- // for unchanged items, preventing unnecessary re-renders of expanded content
287
- api.getSystemHealthOverview({ systemId }).then((data) => {
288
- setOverview((prev) => {
289
- // Create a map of new items for quick lookup
290
- const newItemsMap = new Map(
291
- data.checks.map((item) => [item.configurationId, item])
292
- );
293
-
294
- // Update existing items in place, add new ones
295
- const merged = prev.map((existing) => {
296
- const updated = newItemsMap.get(existing.configurationId);
297
- if (updated) {
298
- newItemsMap.delete(existing.configurationId);
299
- // Map API response to our internal format
300
- const mappedItem: HealthCheckOverviewItem = {
301
- configurationId: updated.configurationId,
302
- strategyId: updated.strategyId,
303
- name: updated.configurationName,
304
- state: updated.status,
305
- intervalSeconds: updated.intervalSeconds,
306
- lastRunAt: updated.recentRuns[0]?.timestamp
307
- ? new Date(updated.recentRuns[0].timestamp)
308
- : undefined,
309
- stateThresholds: updated.stateThresholds,
310
- recentStatusHistory: updated.recentRuns.map((r) => r.status),
311
- };
312
- // Return updated data but preserve reference if nothing changed
313
- return JSON.stringify(existing) === JSON.stringify(mappedItem)
314
- ? existing
315
- : mappedItem;
316
- }
317
- return existing;
318
- });
319
-
320
- // Add any new items that weren't in the previous list
321
- for (const newItem of newItemsMap.values()) {
322
- merged.push({
323
- configurationId: newItem.configurationId,
324
- strategyId: newItem.strategyId,
325
- name: newItem.configurationName,
326
- state: newItem.status,
327
- intervalSeconds: newItem.intervalSeconds,
328
- lastRunAt: newItem.recentRuns[0]?.timestamp
329
- ? new Date(newItem.recentRuns[0].timestamp)
330
- : undefined,
331
- stateThresholds: newItem.stateThresholds,
332
- recentStatusHistory: newItem.recentRuns.map((r) => r.status),
333
- });
334
- }
335
-
336
- // Remove items that no longer exist
337
- return merged.filter((item) =>
338
- data.checks.some((c) => c.configurationId === item.configurationId)
339
- );
340
- });
341
- });
342
- }
249
+ // Fetch health check overview using useQuery
250
+ const {
251
+ data: overviewData,
252
+ isLoading: initialLoading,
253
+ refetch,
254
+ } = healthCheckClient.getSystemHealthOverview.useQuery({
255
+ systemId,
343
256
  });
344
257
 
258
+ // Transform API response to component format
259
+ const overview: HealthCheckOverviewItem[] = React.useMemo(() => {
260
+ if (!overviewData) return [];
261
+ return overviewData.checks.map((check) => ({
262
+ configurationId: check.configurationId,
263
+ strategyId: check.strategyId,
264
+ name: check.configurationName,
265
+ state: check.status,
266
+ intervalSeconds: check.intervalSeconds,
267
+ lastRunAt: check.recentRuns[0]?.timestamp
268
+ ? new Date(check.recentRuns[0].timestamp)
269
+ : undefined,
270
+ stateThresholds: check.stateThresholds,
271
+ recentStatusHistory: check.recentRuns.map((r) => r.status),
272
+ }));
273
+ }, [overviewData]);
274
+
275
+ // Listen for realtime health check updates to refresh overview
276
+ useSignal(
277
+ HEALTH_CHECK_RUN_COMPLETED,
278
+ useCallback(
279
+ ({ systemId: changedId }) => {
280
+ if (changedId === systemId) {
281
+ void refetch();
282
+ }
283
+ },
284
+ [systemId, refetch]
285
+ )
286
+ );
287
+
345
288
  if (initialLoading) {
346
289
  return <LoadingSpinner />;
347
290
  }
@@ -1,10 +1,11 @@
1
- import React, { useEffect, useState, useCallback } from "react";
2
- import { useApi, type SlotContext } from "@checkstack/frontend-api";
1
+ import React from "react";
2
+ import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
3
3
  import { useSignal } from "@checkstack/signal-frontend";
4
4
  import { SystemStateBadgesSlot } from "@checkstack/catalog-common";
5
5
  import { HEALTH_CHECK_RUN_COMPLETED } from "@checkstack/healthcheck-common";
6
- import { healthCheckApiRef } from "../api";
7
- import { HealthBadge, type HealthStatus } from "@checkstack/ui";
6
+ import { HealthCheckApi } from "../api";
7
+ import { HealthBadge } from "@checkstack/ui";
8
+ import { useSystemBadgeDataOptional } from "@checkstack/dashboard-frontend";
8
9
 
9
10
  type Props = SlotContext<typeof SystemStateBadgesSlot>;
10
11
 
@@ -12,35 +13,43 @@ type Props = SlotContext<typeof SystemStateBadgesSlot>;
12
13
  * Displays a health badge for a system based on its health check results.
13
14
  * Uses the backend's getSystemHealthStatus endpoint which evaluates
14
15
  * health status based on configured state thresholds.
16
+ *
17
+ * When rendered within SystemBadgeDataProvider, uses bulk-fetched data.
18
+ * Otherwise, falls back to individual fetch.
19
+ *
15
20
  * Listens for realtime updates via signals.
16
21
  */
17
22
  export const SystemHealthBadge: React.FC<Props> = ({ system }) => {
18
- const api = useApi(healthCheckApiRef);
19
- const [status, setStatus] = useState<HealthStatus>();
20
-
21
- const refetch = useCallback(() => {
22
- if (!system?.id) return;
23
-
24
- api
25
- .getSystemHealthStatus({ systemId: system.id })
26
- .then((result) => {
27
- setStatus(result.status);
28
- })
29
- .catch(console.error);
30
- }, [system?.id, api]);
31
-
32
- // Initial fetch
33
- useEffect(() => {
34
- refetch();
35
- }, [refetch]);
36
-
37
- // Listen for realtime health check updates
23
+ const healthCheckClient = usePluginClient(HealthCheckApi);
24
+ const badgeData = useSystemBadgeDataOptional();
25
+
26
+ // Try to get data from provider first
27
+ const providerData = badgeData?.getSystemBadgeData(system?.id ?? "");
28
+ const providerStatus = providerData?.health?.status;
29
+
30
+ // Query for health status if not using provider
31
+ // When badgeData exists (inside provider), this query is disabled
32
+ const { data: healthData, refetch } =
33
+ healthCheckClient.getSystemHealthStatus.useQuery(
34
+ { systemId: system?.id ?? "" },
35
+ {
36
+ enabled: !badgeData && !!system?.id,
37
+ staleTime: 30_000, // Prevent unnecessary refetches
38
+ }
39
+ );
40
+
41
+ const localStatus = healthData?.status;
42
+
43
+ // Listen for realtime health check updates (only in fallback mode)
38
44
  useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
39
- if (changedId === system?.id) {
40
- refetch();
45
+ if (!badgeData && changedId === system?.id) {
46
+ void refetch();
41
47
  }
42
48
  });
43
49
 
44
- if (!status) return;
50
+ // Use provider data if available, otherwise use local state
51
+ const status = providerStatus ?? localStatus;
52
+
53
+ if (!status) return <></>;
45
54
  return <HealthBadge status={status} />;
46
55
  };