@checkstack/healthcheck-frontend 0.1.0 → 0.3.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.
@@ -7,8 +7,8 @@
7
7
  * Supports nested schemas under `collectors.*` for per-collector metrics.
8
8
  */
9
9
 
10
- import type { ChartType } from "@checkstack/healthcheck-common";
11
10
  import type {
11
+ ChartType,
12
12
  JsonSchemaPropertyCore,
13
13
  JsonSchemaBase,
14
14
  } from "@checkstack/common";
@@ -34,7 +34,7 @@ export type ResultSchema = JsonSchemaBase<ResultSchemaProperty>;
34
34
  * Chart field information extracted from JSON Schema.
35
35
  */
36
36
  export interface ChartField {
37
- /** Field path (supports dot notation for nested fields like "collectors.request.responseTimeMs") */
37
+ /** Field name (simple name for collector fields, path for others) */
38
38
  name: string;
39
39
  /** Chart type to render */
40
40
  chartType: ChartType;
@@ -44,7 +44,7 @@ export interface ChartField {
44
44
  unit?: string;
45
45
  /** JSON Schema type (number, string, boolean, etc.) */
46
46
  schemaType: string;
47
- /** Collector ID if this field is from a collector */
47
+ /** Collector ID if this field is from a collector (used for data lookup) */
48
48
  collectorId?: string;
49
49
  }
50
50
 
@@ -78,7 +78,6 @@ export function extractChartFields(
78
78
  // Extract fields from the collector's result schema
79
79
  const collectorFields = extractFieldsFromProperties(
80
80
  collectorProp.properties,
81
- `collectors.${collectorId}`,
82
81
  collectorId
83
82
  );
84
83
  fields.push(...collectorFields);
@@ -101,7 +100,6 @@ export function extractChartFields(
101
100
  */
102
101
  function extractFieldsFromProperties(
103
102
  properties: Record<string, ResultSchemaProperty>,
104
- pathPrefix: string,
105
103
  collectorId: string
106
104
  ): ChartField[] {
107
105
  const fields: ChartField[] = [];
@@ -110,8 +108,8 @@ function extractFieldsFromProperties(
110
108
  const chartType = prop["x-chart-type"];
111
109
  if (!chartType) continue;
112
110
 
113
- const fullPath = `${pathPrefix}.${fieldName}`;
114
- const field = extractSingleField(fullPath, prop);
111
+ // Use just field name - collectorId is stored separately for data lookup
112
+ const field = extractSingleField(fieldName, prop);
115
113
  field.collectorId = collectorId;
116
114
  // Prefix label with collector ID for clarity
117
115
  if (!prop["x-chart-label"]?.includes(collectorId)) {
@@ -165,28 +163,53 @@ function formatFieldName(name: string): string {
165
163
 
166
164
  /**
167
165
  * Get the value for a field from a data object.
168
- * Supports dot-notation paths like "collectors.request.responseTimeMs".
166
+ * For strategy-level fields, also searches inside collectors as fallback.
167
+ *
168
+ * @param data - The metadata object
169
+ * @param fieldName - Simple field name (no dot notation for collector fields)
170
+ * @param collectorInstanceId - Optional: if provided, looks in collectors[collectorInstanceId]
169
171
  */
170
172
  export function getFieldValue(
171
173
  data: Record<string, unknown> | undefined,
172
- fieldName: string
174
+ fieldName: string,
175
+ collectorInstanceId?: string
173
176
  ): unknown {
174
177
  if (!data) return undefined;
175
178
 
176
- // Simple case: no dot notation
177
- if (!fieldName.includes(".")) {
178
- return data[fieldName];
179
+ // If collectorInstanceId is provided, look in that specific collector's data
180
+ if (collectorInstanceId) {
181
+ const collectors = data.collectors as
182
+ | Record<string, Record<string, unknown>>
183
+ | undefined;
184
+ if (collectors && typeof collectors === "object") {
185
+ const collectorData = collectors[collectorInstanceId];
186
+ if (collectorData && typeof collectorData === "object") {
187
+ return collectorData[fieldName];
188
+ }
189
+ }
190
+ return undefined;
179
191
  }
180
192
 
181
- // Dot notation: traverse the path
182
- const parts = fieldName.split(".");
183
- let current: unknown = data;
193
+ // For non-collector fields, try direct lookup first
194
+ const directValue = data[fieldName];
195
+ if (directValue !== undefined) {
196
+ return directValue;
197
+ }
184
198
 
185
- for (const part of parts) {
186
- if (current === null || current === undefined) return undefined;
187
- if (typeof current !== "object") return undefined;
188
- current = (current as Record<string, unknown>)[part];
199
+ // Fallback: search all collectors for the field (for strategy schema fields)
200
+ const collectors = data.collectors as
201
+ | Record<string, Record<string, unknown>>
202
+ | undefined;
203
+ if (collectors && typeof collectors === "object") {
204
+ for (const collectorData of Object.values(collectors)) {
205
+ if (collectorData && typeof collectorData === "object") {
206
+ const value = collectorData[fieldName];
207
+ if (value !== undefined) {
208
+ return value;
209
+ }
210
+ }
211
+ }
189
212
  }
190
213
 
191
- return current;
214
+ return undefined;
192
215
  }
@@ -1,5 +1,9 @@
1
1
  /**
2
2
  * Hook to fetch and cache strategy schemas.
3
+ *
4
+ * Fetches both strategy result schemas AND collector result schemas,
5
+ * merging them into a unified schema where collector schemas are nested
6
+ * under `properties.collectors.<collectorId>`.
3
7
  */
4
8
 
5
9
  import { useEffect, useState } from "react";
@@ -14,6 +18,9 @@ interface StrategySchemas {
14
18
  /**
15
19
  * Fetch and cache strategy schemas for auto-chart rendering.
16
20
  *
21
+ * Also fetches collector schemas and merges them into the result schema
22
+ * so that chart fields from collectors are properly extracted.
23
+ *
17
24
  * @param strategyId - The strategy ID to fetch schemas for
18
25
  * @returns Schemas for the strategy, or undefined if not found
19
26
  */
@@ -30,13 +37,30 @@ export function useStrategySchemas(strategyId: string): {
30
37
 
31
38
  async function fetchSchemas() {
32
39
  try {
33
- const strategies = await api.getStrategies();
40
+ // Fetch strategy and collectors in parallel
41
+ const [strategies, collectors] = await Promise.all([
42
+ api.getStrategies(),
43
+ api.getCollectors({ strategyId }),
44
+ ]);
45
+
34
46
  const strategy = strategies.find((s) => s.id === strategyId);
35
47
 
36
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
+
37
62
  setSchemas({
38
- resultSchema:
39
- (strategy.resultSchema as Record<string, unknown>) ?? undefined,
63
+ resultSchema: mergedResultSchema,
40
64
  aggregatedResultSchema:
41
65
  (strategy.aggregatedResultSchema as Record<string, unknown>) ??
42
66
  undefined,
@@ -60,3 +84,49 @@ export function useStrategySchemas(strategyId: string): {
60
84
 
61
85
  return { schemas, loading };
62
86
  }
87
+
88
+ /**
89
+ * Merge collector result schemas into a strategy result schema.
90
+ *
91
+ * Creates a schema structure where collectors are nested under
92
+ * `properties.collectors.<collectorId>`, matching the actual data structure
93
+ * stored by the health check executor.
94
+ */
95
+ function mergeCollectorSchemas(
96
+ strategySchema: Record<string, unknown> | undefined,
97
+ collectorProperties: Record<string, unknown>
98
+ ): Record<string, unknown> | undefined {
99
+ // If no collectors, return original schema
100
+ if (Object.keys(collectorProperties).length === 0) {
101
+ return strategySchema;
102
+ }
103
+
104
+ // Build the collectors nested schema
105
+ const collectorsSchema = {
106
+ type: "object",
107
+ properties: collectorProperties,
108
+ };
109
+
110
+ // If no strategy schema, create one just with collectors
111
+ if (!strategySchema) {
112
+ return {
113
+ type: "object",
114
+ properties: {
115
+ collectors: collectorsSchema,
116
+ },
117
+ };
118
+ }
119
+
120
+ // Merge: add collectors to existing properties
121
+ const existingProps = (strategySchema.properties ?? {}) as Record<
122
+ string,
123
+ unknown
124
+ >;
125
+ return {
126
+ ...strategySchema,
127
+ properties: {
128
+ ...existingProps,
129
+ collectors: collectorsSchema,
130
+ },
131
+ };
132
+ }
@@ -81,6 +81,13 @@ const OPERATORS: Record<FieldType, { value: string; label: string }[]> = {
81
81
  ],
82
82
  enum: [{ value: "equals", label: "Equals" }],
83
83
  array: [
84
+ { value: "includes", label: "Includes" },
85
+ { value: "notIncludes", label: "Not Includes" },
86
+ { value: "lengthEquals", label: "Length Equals" },
87
+ { value: "lengthGreaterThan", label: "Length Greater Than" },
88
+ { value: "lengthLessThan", label: "Length Less Than" },
89
+ { value: "isEmpty", label: "Is Empty" },
90
+ { value: "isNotEmpty", label: "Is Not Empty" },
84
91
  { value: "exists", label: "Exists" },
85
92
  { value: "notExists", label: "Not Exists" },
86
93
  ],
@@ -98,6 +105,7 @@ const OPERATORS: Record<FieldType, { value: string; label: string }[]> = {
98
105
  // Operators that don't need a value input
99
106
  const VALUE_LESS_OPERATORS = new Set([
100
107
  "isEmpty",
108
+ "isNotEmpty",
101
109
  "isTrue",
102
110
  "isFalse",
103
111
  "exists",
@@ -302,15 +310,22 @@ export const AssertionBuilder: React.FC<AssertionBuilderProps> = ({
302
310
  <div className="space-y-4">
303
311
  {assertions.map((assertion, index) => {
304
312
  const field = getFieldByPath(assertion.field);
305
- const operators = field ? OPERATORS[field.type] : [];
313
+ // Safely get operators with fallback to empty array
314
+ const operators = field ? OPERATORS[field.type] ?? [] : [];
306
315
  const needsValue = !VALUE_LESS_OPERATORS.has(assertion.operator);
307
316
 
317
+ // Check if current values match available options (prevents Radix UI crash)
318
+ const fieldValueValid = fields.some((f) => f.path === assertion.field);
319
+ const operatorValueValid = operators.some(
320
+ (op) => op.value === assertion.operator
321
+ );
322
+
308
323
  return (
309
324
  <div key={index} className="flex items-start gap-2 flex-wrap">
310
325
  {/* Field selector */}
311
326
  <div className="flex-1 min-w-[120px]">
312
327
  <Select
313
- value={assertion.field}
328
+ value={fieldValueValid ? assertion.field : undefined}
314
329
  onValueChange={(v) => handleFieldChange(index, v)}
315
330
  >
316
331
  <SelectTrigger>
@@ -340,7 +355,7 @@ export const AssertionBuilder: React.FC<AssertionBuilderProps> = ({
340
355
  {/* Operator selector */}
341
356
  <div className="flex-1 min-w-[100px]">
342
357
  <Select
343
- value={assertion.operator}
358
+ value={operatorValueValid ? assertion.operator : undefined}
344
359
  onValueChange={(v) => handleOperatorChange(index, v)}
345
360
  >
346
361
  <SelectTrigger>
@@ -100,6 +100,7 @@ export const CollectorList: React.FC<CollectorListProps> = ({
100
100
  if (!collector) return;
101
101
 
102
102
  const newEntry: CollectorConfigEntry = {
103
+ id: crypto.randomUUID(),
103
104
  collectorId,
104
105
  config: {},
105
106
  assertions: [],
@@ -111,6 +112,11 @@ export const CollectorList: React.FC<CollectorListProps> = ({
111
112
  const handleRemove = (index: number) => {
112
113
  const updated = [...configuredCollectors];
113
114
  updated.splice(index, 1);
115
+
116
+ // Reset validity map to prevent stale entries after index shift
117
+ // The DynamicForm components will re-report their validity on next render
118
+ setValidityMap({});
119
+
114
120
  onChange(updated);
115
121
  };
116
122
 
@@ -44,20 +44,20 @@ export function HealthCheckDiagram({
44
44
  }
45
45
 
46
46
  /**
47
- * Wrapper that shows permission message when user lacks access.
47
+ * Wrapper that shows access message when user lacks access.
48
48
  */
49
- export function HealthCheckDiagramPermissionGate({
50
- hasPermission,
49
+ export function HealthCheckDiagramAccessGate({
50
+ hasAccess,
51
51
  children,
52
52
  }: {
53
- hasPermission: boolean;
53
+ hasAccess: boolean;
54
54
  children: React.ReactNode;
55
55
  }) {
56
- if (!hasPermission) {
56
+ if (!hasAccess) {
57
57
  return (
58
58
  <InfoBanner variant="info">
59
59
  Additional strategy-specific visualizations are available with the
60
- &quot;Read Health Check Details&quot; permission.
60
+ &quot;Read Health Check Details&quot; access rule.
61
61
  </InfoBanner>
62
62
  );
63
63
  }
@@ -13,12 +13,14 @@ import {
13
13
  useToast,
14
14
  Dialog,
15
15
  DialogContent,
16
+ DialogDescription,
16
17
  DialogHeader,
17
18
  DialogTitle,
18
19
  DialogFooter,
19
20
  } from "@checkstack/ui";
20
21
  import { useCollectors } from "../hooks/useCollectors";
21
22
  import { CollectorList } from "./CollectorList";
23
+ import { TeamAccessEditor } from "@checkstack/auth-frontend";
22
24
 
23
25
  interface HealthCheckEditorProps {
24
26
  strategies: HealthCheckStrategyDto[];
@@ -82,7 +84,7 @@ export const HealthCheckEditor: React.FC<HealthCheckEditorProps> = ({
82
84
  strategyId,
83
85
  intervalSeconds: Number.parseInt(interval, 10),
84
86
  config,
85
- collectors: collectors.length > 0 ? collectors : undefined,
87
+ collectors, // Always send the array, even if empty, to allow clearing
86
88
  });
87
89
  } catch (error) {
88
90
  const message =
@@ -102,6 +104,11 @@ export const HealthCheckEditor: React.FC<HealthCheckEditorProps> = ({
102
104
  <DialogTitle>
103
105
  {initialData ? "Edit Health Check" : "Create Health Check"}
104
106
  </DialogTitle>
107
+ <DialogDescription className="sr-only">
108
+ {initialData
109
+ ? "Modify the settings for this health check configuration"
110
+ : "Configure a new health check to monitor your services"}
111
+ </DialogDescription>
105
112
  </DialogHeader>
106
113
 
107
114
  <div className="space-y-6 py-4 max-h-[70vh] overflow-y-auto">
@@ -148,6 +155,16 @@ export const HealthCheckEditor: React.FC<HealthCheckEditorProps> = ({
148
155
  onValidChange={setCollectorsValid}
149
156
  />
150
157
  )}
158
+
159
+ {/* Team Access Editor - only shown for existing configurations */}
160
+ {initialData?.id && (
161
+ <TeamAccessEditor
162
+ resourceType="healthcheck.configuration"
163
+ resourceId={initialData.id}
164
+ compact
165
+ expanded
166
+ />
167
+ )}
151
168
  </div>
152
169
 
153
170
  <DialogFooter>
@@ -3,20 +3,17 @@ import { Link } from "react-router-dom";
3
3
  import { Activity } from "lucide-react";
4
4
  import type { UserMenuItemsContext } from "@checkstack/frontend-api";
5
5
  import { DropdownMenuItem } from "@checkstack/ui";
6
- import { qualifyPermissionId, resolveRoute } from "@checkstack/common";
6
+ import { resolveRoute } from "@checkstack/common";
7
7
  import {
8
8
  healthcheckRoutes,
9
- permissions,
9
+ healthCheckAccess,
10
10
  pluginMetadata,
11
11
  } from "@checkstack/healthcheck-common";
12
12
 
13
13
  export const HealthCheckMenuItems = ({
14
- permissions: userPerms,
14
+ accessRules: userPerms,
15
15
  }: UserMenuItemsContext) => {
16
- const qualifiedId = qualifyPermissionId(
17
- pluginMetadata,
18
- permissions.healthCheckRead
19
- );
16
+ const qualifiedId = `${pluginMetadata.pluginId}.${healthCheckAccess.configuration.read.id}`;
20
17
  const canRead = userPerms.includes("*") || userPerms.includes(qualifiedId);
21
18
 
22
19
  if (!canRead) {
@@ -300,7 +300,7 @@ function CollectorResultCard({
300
300
  function formatKey(key: string): string {
301
301
  return key
302
302
  .replaceAll(/([a-z])([A-Z])/g, "$1 $2")
303
- .replaceAll(/^./, (c) => c.toUpperCase());
303
+ .replace(/^./, (c) => c.toUpperCase());
304
304
  }
305
305
 
306
306
  /**
@@ -2,13 +2,14 @@ import React, { useEffect, useState } from "react";
2
2
  import {
3
3
  useApi,
4
4
  type SlotContext,
5
- permissionApiRef,
5
+ accessApiRef,
6
6
  } from "@checkstack/frontend-api";
7
7
  import { healthCheckApiRef, HealthCheckConfiguration } from "../api";
8
8
  import {
9
9
  Button,
10
10
  Dialog,
11
11
  DialogContent,
12
+ DialogDescription,
12
13
  DialogHeader,
13
14
  DialogTitle,
14
15
  DialogFooter,
@@ -31,6 +32,7 @@ import type { StateThresholds } from "@checkstack/healthcheck-common";
31
32
  import {
32
33
  DEFAULT_STATE_THRESHOLDS,
33
34
  healthcheckRoutes,
35
+ healthCheckAccess,
34
36
  } from "@checkstack/healthcheck-common";
35
37
  import { resolveRoute } from "@checkstack/common";
36
38
  import { DEFAULT_RETENTION_CONFIG } from "@checkstack/healthcheck-common";
@@ -51,10 +53,9 @@ export const SystemHealthCheckAssignment: React.FC<Props> = ({
51
53
  systemName: _systemName,
52
54
  }) => {
53
55
  const api = useApi(healthCheckApiRef);
54
- const permissionApi = useApi(permissionApiRef);
55
- const { allowed: canManage } = permissionApi.useResourcePermission(
56
- "healthcheck",
57
- "manage"
56
+ const accessApi = useApi(accessApiRef);
57
+ const { allowed: canManage } = accessApi.useAccess(
58
+ healthCheckAccess.configuration.manage
58
59
  );
59
60
  const [configs, setConfigs] = useState<HealthCheckConfiguration[]>([]);
60
61
  const [associations, setAssociations] = useState<AssociationState[]>([]);
@@ -79,10 +80,11 @@ export const SystemHealthCheckAssignment: React.FC<Props> = ({
79
80
  const loadData = async () => {
80
81
  setLoading(true);
81
82
  try {
82
- const [allConfigs, systemAssociations] = await Promise.all([
83
- api.getConfigurations(),
84
- api.getSystemAssociations({ systemId }),
85
- ]);
83
+ const [{ configurations: allConfigs }, systemAssociations] =
84
+ await Promise.all([
85
+ api.getConfigurations(),
86
+ api.getSystemAssociations({ systemId }),
87
+ ]);
86
88
  setConfigs(allConfigs);
87
89
  setAssociations(systemAssociations);
88
90
  } catch (error) {
@@ -741,6 +743,9 @@ export const SystemHealthCheckAssignment: React.FC<Props> = ({
741
743
  <DialogContent className="max-w-lg max-h-[80vh] overflow-y-auto">
742
744
  <DialogHeader>
743
745
  <DialogTitle>Health Check Assignments</DialogTitle>
746
+ <DialogDescription className="sr-only">
747
+ Manage health check assignments for this system
748
+ </DialogDescription>
744
749
  </DialogHeader>
745
750
 
746
751
  {loading ? (
@@ -1,8 +1,8 @@
1
1
  import { useEffect, useState, useMemo, useCallback } from "react";
2
- import { useApi, permissionApiRef } from "@checkstack/frontend-api";
2
+ import { useApi, accessApiRef } from "@checkstack/frontend-api";
3
3
  import { healthCheckApiRef } from "../api";
4
4
  import {
5
- permissions,
5
+ healthCheckAccess,
6
6
  DEFAULT_RETENTION_CONFIG,
7
7
  type RetentionConfig,
8
8
  HEALTH_CHECK_RUN_COMPLETED,
@@ -36,10 +36,10 @@ interface UseHealthCheckDataResult {
36
36
  isAggregated: boolean;
37
37
  /** The resolved retention config */
38
38
  retentionConfig: RetentionConfig;
39
- /** Whether user has permission to view detailed data */
40
- hasPermission: boolean;
41
- /** Whether permission is still loading */
42
- permissionLoading: boolean;
39
+ /** Whether user has access to view detailed data */
40
+ hasAccess: boolean;
41
+ /** Whether access is still loading */
42
+ accessLoading: boolean;
43
43
  }
44
44
 
45
45
  /**
@@ -52,14 +52,14 @@ interface UseHealthCheckDataResult {
52
52
  *
53
53
  * @example
54
54
  * ```tsx
55
- * const { context, loading, hasPermission } = useHealthCheckData({
55
+ * const { context, loading, hasAccess } = useHealthCheckData({
56
56
  * systemId,
57
57
  * configurationId,
58
58
  * strategyId,
59
59
  * dateRange: { startDate, endDate },
60
60
  * });
61
61
  *
62
- * if (!hasPermission) return <NoPermissionMessage />;
62
+ * if (!hasAccess) return <NoAccessMessage />;
63
63
  * if (loading) return <LoadingSpinner />;
64
64
  * if (!context) return null;
65
65
  *
@@ -75,11 +75,11 @@ export function useHealthCheckData({
75
75
  offset = 0,
76
76
  }: UseHealthCheckDataProps): UseHealthCheckDataResult {
77
77
  const api = useApi(healthCheckApiRef);
78
- const permissionApi = useApi(permissionApiRef);
78
+ const accessApi = useApi(accessApiRef);
79
79
 
80
- // Permission state
81
- const { allowed: hasPermission, loading: permissionLoading } =
82
- permissionApi.usePermission(permissions.healthCheckDetailsRead.id);
80
+ // Access state
81
+ const { allowed: hasAccess, loading: accessLoading } =
82
+ accessApi.useAccess(healthCheckAccess.details);
83
83
 
84
84
  // Retention config state
85
85
  const [retentionConfig, setRetentionConfig] = useState<RetentionConfig>(
@@ -160,24 +160,24 @@ export function useHealthCheckData({
160
160
 
161
161
  // Fetch raw data when in raw mode
162
162
  useEffect(() => {
163
- if (!hasPermission || permissionLoading || retentionLoading || isAggregated)
163
+ if (!hasAccess || accessLoading || retentionLoading || isAggregated)
164
164
  return;
165
165
  fetchRawData(true);
166
166
  }, [
167
167
  fetchRawData,
168
- hasPermission,
169
- permissionLoading,
168
+ hasAccess,
169
+ accessLoading,
170
170
  retentionLoading,
171
171
  isAggregated,
172
172
  ]);
173
173
 
174
174
  // Listen for realtime health check updates to refresh data silently
175
175
  useSignal(HEALTH_CHECK_RUN_COMPLETED, ({ systemId: changedId }) => {
176
- // Only refresh if we're in raw mode (not aggregated) and have permission
176
+ // Only refresh if we're in raw mode (not aggregated) and have access
177
177
  if (
178
178
  changedId === systemId &&
179
- hasPermission &&
180
- !permissionLoading &&
179
+ hasAccess &&
180
+ !accessLoading &&
181
181
  !retentionLoading &&
182
182
  !isAggregated
183
183
  ) {
@@ -188,8 +188,8 @@ export function useHealthCheckData({
188
188
  // Fetch aggregated data when in aggregated mode
189
189
  useEffect(() => {
190
190
  if (
191
- !hasPermission ||
192
- permissionLoading ||
191
+ !hasAccess ||
192
+ accessLoading ||
193
193
  retentionLoading ||
194
194
  !isAggregated
195
195
  )
@@ -198,7 +198,7 @@ export function useHealthCheckData({
198
198
  setAggregatedLoading(true);
199
199
  // Use daily buckets for ranges > 30 days, hourly otherwise
200
200
  const bucketSize = dateRangeDays > 30 ? "daily" : "hourly";
201
- // Use detailed endpoint to get aggregatedResult since we have permission
201
+ // Use detailed endpoint to get aggregatedResult since we have access
202
202
  api
203
203
  .getDetailedAggregatedHistory({
204
204
  systemId,
@@ -217,8 +217,8 @@ export function useHealthCheckData({
217
217
  api,
218
218
  systemId,
219
219
  configurationId,
220
- hasPermission,
221
- permissionLoading,
220
+ hasAccess,
221
+ accessLoading,
222
222
  retentionLoading,
223
223
  isAggregated,
224
224
  dateRangeDays,
@@ -227,7 +227,7 @@ export function useHealthCheckData({
227
227
  ]);
228
228
 
229
229
  const context = useMemo((): HealthCheckDiagramSlotContext | undefined => {
230
- if (!hasPermission || permissionLoading || retentionLoading) {
230
+ if (!hasAccess || accessLoading || retentionLoading) {
231
231
  return undefined;
232
232
  }
233
233
 
@@ -249,8 +249,8 @@ export function useHealthCheckData({
249
249
  runs: rawRuns,
250
250
  };
251
251
  }, [
252
- hasPermission,
253
- permissionLoading,
252
+ hasAccess,
253
+ accessLoading,
254
254
  retentionLoading,
255
255
  isAggregated,
256
256
  systemId,
@@ -261,7 +261,7 @@ export function useHealthCheckData({
261
261
  ]);
262
262
 
263
263
  const loading =
264
- permissionLoading ||
264
+ accessLoading ||
265
265
  retentionLoading ||
266
266
  (isAggregated ? aggregatedLoading : rawLoading);
267
267
 
@@ -270,7 +270,7 @@ export function useHealthCheckData({
270
270
  loading,
271
271
  isAggregated,
272
272
  retentionConfig,
273
- hasPermission,
274
- permissionLoading,
273
+ hasAccess,
274
+ accessLoading,
275
275
  };
276
276
  }
package/src/index.tsx CHANGED
@@ -13,7 +13,7 @@ import { HealthCheckMenuItems } from "./components/HealthCheckMenuItems";
13
13
  import { HealthCheckSystemOverview } from "./components/HealthCheckSystemOverview";
14
14
  import { SystemHealthCheckAssignment } from "./components/SystemHealthCheckAssignment";
15
15
  import { SystemHealthBadge } from "./components/SystemHealthBadge";
16
- import { permissions } from "@checkstack/healthcheck-common";
16
+ import { healthCheckAccess } from "@checkstack/healthcheck-common";
17
17
  import { autoChartExtension } from "./auto-charts";
18
18
 
19
19
  import {
@@ -49,19 +49,19 @@ export default createFrontendPlugin({
49
49
  route: healthcheckRoutes.routes.config,
50
50
  element: <HealthCheckConfigPage />,
51
51
  title: "Health Checks",
52
- permission: permissions.healthCheckManage,
52
+ accessRule: healthCheckAccess.configuration.manage,
53
53
  },
54
54
  {
55
55
  route: healthcheckRoutes.routes.history,
56
56
  element: <HealthCheckHistoryPage />,
57
57
  title: "Health Check History",
58
- permission: permissions.healthCheckRead,
58
+ accessRule: healthCheckAccess.configuration.read,
59
59
  },
60
60
  {
61
61
  route: healthcheckRoutes.routes.historyDetail,
62
62
  element: <HealthCheckHistoryDetailPage />,
63
63
  title: "Health Check Detail",
64
- permission: permissions.healthCheckDetailsRead,
64
+ accessRule: healthCheckAccess.details,
65
65
  },
66
66
  ],
67
67
  apis: [