@checkstack/anomaly-frontend 0.5.4 → 0.5.5

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,44 @@
1
1
  # @checkstack/anomaly-frontend
2
2
 
3
+ ## 0.5.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 56e7c75: Hide navigation, actions and links that the current user cannot use, so anonymous
8
+ and read-only users no longer see entries that lead to "Access Denied" or to
9
+ actions the server would reject.
10
+
11
+ - **Sidebar**: a nav entry can now declare a dynamic `nav.isVisible({ accessRules, isAuthenticated })` predicate (in addition to the static `accessRule`). A group whose every entry is filtered out is no longer rendered. The filtering/grouping logic is extracted to a pure, unit-tested helper.
12
+ - **Infrastructure**: its sidebar entry is shown only when the user can READ at least one contributed tab (queue, cache, …), instead of always (it previously had no static rule because tabs are contributed at runtime).
13
+ - **Notification Settings**: hidden from anonymous users - notifications are per-user, so an anonymous visitor can't have any.
14
+ - **Anomaly Mute / Suppress**: the "Mute" / "Mute all" controls (a per-user preference) are hidden from anonymous visitors; the "Suppress" control is gated on `anomalyAccess.feed.manage`. Both were previously always visible.
15
+ - **Dashboard**: the "Open Catalog" actions (which open the manage-only Catalog config page) are hidden from users without `catalogAccess.system.manage`, and the "View catalog" link is gated on `catalogAccess.system.read`.
16
+ - **Dashboard status signals**: the per-system status rows contributed by plugins (`SystemSignalsSlot`) now render as a LINK only when the user can open the target, and as plain text otherwise. `SystemSignal` gains an optional `accessRule`; the healthcheck, anomaly, and dependency fillers set it for their gated targets (check-history / assignments / dependency-map). Signals pointing at ungated pages (incident / maintenance / SLO detail) stay links.
17
+ - **Plugin Manager**: the "Install plugin" button (which opens the install-gated page) is hidden from users with only `plugin` view access.
18
+ - **Satellites**: the page is entirely manage-gated, but its route/sidebar entry was gated on `read`, so read-only users saw the nav item and hit "Access Denied" on click. The route and nav entry now require `satellite.manage`.
19
+
20
+ The `@checkstack/ai-backend` bump is only the regenerated bundled docs index
21
+ (the frontend routing guide gained the `nav.isVisible` section); no code change.
22
+
23
+ **BREAKING (`@checkstack/frontend-api`):** the `AccessApi` interface gains a
24
+ required `useIsAuthenticated()` method. Custom `AccessApi` implementations must
25
+ add it (it returns `{ loading, isAuthenticated }`). The built-in auth
26
+ implementation and the no-auth fallback already do. `NavEntry` also gains an
27
+ optional `isVisible` predicate (purely additive).
28
+
29
+ - Updated dependencies [56e7c75]
30
+ - Updated dependencies [56e7c75]
31
+ - @checkstack/frontend-api@0.9.0
32
+ - @checkstack/notification-frontend@0.5.5
33
+ - @checkstack/catalog-common@2.3.4
34
+ - @checkstack/healthcheck-frontend@0.23.5
35
+ - @checkstack/ui@1.15.1
36
+ - @checkstack/common@0.15.0
37
+ - @checkstack/anomaly-common@1.3.4
38
+ - @checkstack/healthcheck-common@1.5.4
39
+ - @checkstack/notification-common@1.3.3
40
+ - @checkstack/signal-frontend@0.2.4
41
+
3
42
  ## 0.5.4
4
43
 
5
44
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/anomaly-frontend",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.tsx",
@@ -13,16 +13,16 @@
13
13
  "lint:code": "eslint . --max-warnings 0"
14
14
  },
15
15
  "dependencies": {
16
- "@checkstack/anomaly-common": "1.3.3",
17
- "@checkstack/catalog-common": "2.3.3",
18
- "@checkstack/common": "0.14.1",
19
- "@checkstack/frontend-api": "0.8.0",
20
- "@checkstack/healthcheck-common": "1.5.3",
21
- "@checkstack/healthcheck-frontend": "0.23.4",
22
- "@checkstack/notification-common": "1.3.2",
23
- "@checkstack/notification-frontend": "0.5.4",
24
- "@checkstack/signal-frontend": "0.2.3",
25
- "@checkstack/ui": "1.15.0",
16
+ "@checkstack/anomaly-common": "1.3.4",
17
+ "@checkstack/catalog-common": "2.3.4",
18
+ "@checkstack/common": "0.15.0",
19
+ "@checkstack/frontend-api": "0.9.0",
20
+ "@checkstack/healthcheck-common": "1.5.4",
21
+ "@checkstack/healthcheck-frontend": "0.23.5",
22
+ "@checkstack/notification-common": "1.3.3",
23
+ "@checkstack/notification-frontend": "0.5.5",
24
+ "@checkstack/signal-frontend": "0.2.4",
25
+ "@checkstack/ui": "1.15.1",
26
26
  "date-fns": "^4.4.0",
27
27
  "lucide-react": "^1.17.0",
28
28
  "react": "19.2.7",
@@ -30,7 +30,7 @@
30
30
  "zod": "^4.2.1"
31
31
  },
32
32
  "devDependencies": {
33
- "@checkstack/scripts": "0.6.0",
33
+ "@checkstack/scripts": "0.6.1",
34
34
  "@checkstack/tsconfig": "0.0.7",
35
35
  "@types/react": "^19.0.0",
36
36
  "typescript": "^5.0.0"
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import { Bell, BellOff } from "lucide-react";
3
3
  import { Button, useToast } from "@checkstack/ui";
4
- import { usePluginClient } from "@checkstack/frontend-api";
4
+ import { usePluginClient, useApi, accessApiRef } from "@checkstack/frontend-api";
5
5
  import { AnomalyApi } from "@checkstack/anomaly-common";
6
6
  import { extractErrorMessage } from "@checkstack/common";
7
7
 
@@ -22,6 +22,10 @@ export const AnomalyFieldMuteList: React.FC<{
22
22
  const { systemId } = resource;
23
23
  const anomalyClient = usePluginClient(AnomalyApi);
24
24
  const toast = useToast();
25
+ // Muting is a per-user preference for logged-in users only; anonymous
26
+ // visitors (e.g. reaching this via a direct link) get no mute controls.
27
+ const accessApi = useApi(accessApiRef);
28
+ const { isAuthenticated: canMute } = accessApi.useIsAuthenticated();
25
29
 
26
30
  // Notification mute is a per-user concern orthogonal to global suppression,
27
31
  // so the mute-management list must include suppressed rows — otherwise a
@@ -81,24 +85,26 @@ export const AnomalyFieldMuteList: React.FC<{
81
85
  Mute the entire system or just specific fields. Mutes persist
82
86
  across re-subscribes.
83
87
  </span>
84
- <Button
85
- type="button"
86
- variant={isSystemMuted ? "outline" : "primary"}
87
- size="sm"
88
- className="h-6 px-2 text-[11px] gap-1"
89
- disabled={isPending}
90
- onClick={() => handleToggle("", isSystemMuted)}
91
- >
92
- {isSystemMuted ? (
93
- <>
94
- <BellOff className="h-3 w-3" /> System muted
95
- </>
96
- ) : (
97
- <>
98
- <Bell className="h-3 w-3" /> Mute system
99
- </>
100
- )}
101
- </Button>
88
+ {canMute && (
89
+ <Button
90
+ type="button"
91
+ variant={isSystemMuted ? "outline" : "primary"}
92
+ size="sm"
93
+ className="h-6 px-2 text-[11px] gap-1"
94
+ disabled={isPending}
95
+ onClick={() => handleToggle("", isSystemMuted)}
96
+ >
97
+ {isSystemMuted ? (
98
+ <>
99
+ <BellOff className="h-3 w-3" /> System muted
100
+ </>
101
+ ) : (
102
+ <>
103
+ <Bell className="h-3 w-3" /> Mute system
104
+ </>
105
+ )}
106
+ </Button>
107
+ )}
102
108
  </div>
103
109
 
104
110
  {fieldPaths.length === 0 ? (
@@ -116,23 +122,23 @@ export const AnomalyFieldMuteList: React.FC<{
116
122
  className="flex items-center justify-between px-3 py-1.5 text-xs"
117
123
  >
118
124
  <span className="font-mono truncate">{fp}</span>
119
- <Button
120
- type="button"
121
- variant="ghost"
122
- size="icon"
123
- className="h-6 w-6"
124
- disabled={isPending || isSystemMuted}
125
- title={muted ? "Unmute this field" : "Mute this field"}
126
- onClick={() =>
127
- handleToggle(fp, mutedFields.has(fp))
128
- }
129
- >
130
- {muted ? (
131
- <BellOff className="h-3 w-3" />
132
- ) : (
133
- <Bell className="h-3 w-3" />
134
- )}
135
- </Button>
125
+ {canMute && (
126
+ <Button
127
+ type="button"
128
+ variant="ghost"
129
+ size="icon"
130
+ className="h-6 w-6"
131
+ disabled={isPending || isSystemMuted}
132
+ title={muted ? "Unmute this field" : "Mute this field"}
133
+ onClick={() => handleToggle(fp, mutedFields.has(fp))}
134
+ >
135
+ {muted ? (
136
+ <BellOff className="h-3 w-3" />
137
+ ) : (
138
+ <Bell className="h-3 w-3" />
139
+ )}
140
+ </Button>
141
+ )}
136
142
  </div>
137
143
  );
138
144
  })}
@@ -7,7 +7,10 @@ import {
7
7
  type SystemSignalsMap,
8
8
  } from "@checkstack/catalog-common";
9
9
  import { AnomalyApi } from "@checkstack/anomaly-common";
10
- import { healthcheckRoutes } from "@checkstack/healthcheck-common";
10
+ import {
11
+ healthcheckRoutes,
12
+ healthCheckAccess,
13
+ } from "@checkstack/healthcheck-common";
11
14
 
12
15
  type Props = SlotContext<typeof SystemSignalsSlot>;
13
16
 
@@ -57,6 +60,8 @@ export const AnomalySignalsFiller: React.FC<Props> = ({
57
60
  systemId,
58
61
  configurationId,
59
62
  }),
63
+ // The history detail page is gated; render as text for users without it.
64
+ accessRule: healthCheckAccess.details,
60
65
  since: new Date(startedAt).toISOString(),
61
66
  iconName: "ChartSpline",
62
67
  };
@@ -1,8 +1,14 @@
1
1
  import React from "react";
2
- import { usePluginClient, type SlotContext } from "@checkstack/frontend-api";
2
+ import {
3
+ usePluginClient,
4
+ useApi,
5
+ accessApiRef,
6
+ type SlotContext,
7
+ } from "@checkstack/frontend-api";
3
8
  import { SystemDetailsSlot } from "@checkstack/catalog-common";
4
9
  import {
5
10
  AnomalyApi,
11
+ anomalyAccess,
6
12
  type AnomalyDto,
7
13
  } from "@checkstack/anomaly-common";
8
14
  import { healthcheckRoutes } from "@checkstack/healthcheck-common";
@@ -117,6 +123,8 @@ function AnomalyRow({
117
123
  isToggling,
118
124
  onSuppress,
119
125
  isSuppressing,
126
+ canMute,
127
+ canManage,
120
128
  }: {
121
129
  anomaly: AnomalyDto;
122
130
  systemId: string;
@@ -125,6 +133,10 @@ function AnomalyRow({
125
133
  isToggling: boolean;
126
134
  onSuppress: (anomalyId: string) => void;
127
135
  isSuppressing: boolean;
136
+ /** Whether the user may mute notifications (any logged-in user). */
137
+ canMute: boolean;
138
+ /** Whether the user may suppress the anomaly (anomalyAccess.feed.manage). */
139
+ canManage: boolean;
128
140
  }) {
129
141
  const isSuspicious = anomaly.state === "suspicious";
130
142
  const isDrift = anomaly.kind === "drift";
@@ -219,30 +231,32 @@ function AnomalyRow({
219
231
  )}
220
232
  {deviationValue}σ
221
233
  </Badge>
222
- <Button
223
- type="button"
224
- variant="ghost"
225
- size="icon"
226
- className="h-7 w-7 text-muted-foreground hover:text-foreground"
227
- disabled={isToggling}
228
- title={
229
- isMuted
230
- ? "Unmute notifications for this field"
231
- : "Mute notifications for this field"
232
- }
233
- onClick={(event) => {
234
- event.preventDefault();
235
- event.stopPropagation();
236
- onToggleMute(anomaly.fieldPath, isMuted);
237
- }}
238
- >
239
- {isMuted ? (
240
- <BellOff className="h-3.5 w-3.5" />
241
- ) : (
242
- <Bell className="h-3.5 w-3.5" />
243
- )}
244
- </Button>
245
- {!isSuspicious && (
234
+ {canMute && (
235
+ <Button
236
+ type="button"
237
+ variant="ghost"
238
+ size="icon"
239
+ className="h-7 w-7 text-muted-foreground hover:text-foreground"
240
+ disabled={isToggling}
241
+ title={
242
+ isMuted
243
+ ? "Unmute notifications for this field"
244
+ : "Mute notifications for this field"
245
+ }
246
+ onClick={(event) => {
247
+ event.preventDefault();
248
+ event.stopPropagation();
249
+ onToggleMute(anomaly.fieldPath, isMuted);
250
+ }}
251
+ >
252
+ {isMuted ? (
253
+ <BellOff className="h-3.5 w-3.5" />
254
+ ) : (
255
+ <Bell className="h-3.5 w-3.5" />
256
+ )}
257
+ </Button>
258
+ )}
259
+ {canManage && !isSuspicious && (
246
260
  <Button
247
261
  type="button"
248
262
  variant="ghost"
@@ -272,6 +286,14 @@ function AnomalyRow({
272
286
  export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
273
287
  const anomalyClient = usePluginClient(AnomalyApi);
274
288
  const toast = useToast();
289
+ const accessApi = useApi(accessApiRef);
290
+ // Muting notifications is a per-user preference any LOGGED-IN user may set
291
+ // (contract: userType "user", access []), so it is gated on authentication,
292
+ // not on manage. Suppressing an anomaly is an operator action gated on
293
+ // anomalyAccess.feed.manage. Anonymous visitors get neither. The server
294
+ // enforces both regardless.
295
+ const { isAuthenticated: canMute } = accessApi.useIsAuthenticated();
296
+ const { allowed: canManage } = accessApi.useAccess(anomalyAccess.feed.manage);
275
297
 
276
298
  // Fetch only active anomalies — exclude recovered ones.
277
299
  // Two queries with React Query deduplication: confirmed anomalies + suspicious.
@@ -394,31 +416,33 @@ export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
394
416
  {suspiciousCount}
395
417
  </Badge>
396
418
  )}
397
- <Button
398
- type="button"
399
- variant="ghost"
400
- size="sm"
401
- className="h-6 px-2 text-[11px] gap-1"
402
- disabled={isToggling}
403
- title={
404
- isSystemMuted
405
- ? "Resume anomaly notifications for this system"
406
- : "Stop receiving any anomaly notifications for this system"
407
- }
408
- onClick={() => handleToggleMute("", isSystemMuted)}
409
- >
410
- {isSystemMuted ? (
411
- <>
412
- <BellOff className="h-3 w-3" />
413
- Muted
414
- </>
415
- ) : (
416
- <>
417
- <Bell className="h-3 w-3" />
418
- Mute all
419
- </>
420
- )}
421
- </Button>
419
+ {canMute && (
420
+ <Button
421
+ type="button"
422
+ variant="ghost"
423
+ size="sm"
424
+ className="h-6 px-2 text-[11px] gap-1"
425
+ disabled={isToggling}
426
+ title={
427
+ isSystemMuted
428
+ ? "Resume anomaly notifications for this system"
429
+ : "Stop receiving any anomaly notifications for this system"
430
+ }
431
+ onClick={() => handleToggleMute("", isSystemMuted)}
432
+ >
433
+ {isSystemMuted ? (
434
+ <>
435
+ <BellOff className="h-3 w-3" />
436
+ Muted
437
+ </>
438
+ ) : (
439
+ <>
440
+ <Bell className="h-3 w-3" />
441
+ Mute all
442
+ </>
443
+ )}
444
+ </Button>
445
+ )}
422
446
  </div>
423
447
  </div>
424
448
  </CardHeader>
@@ -434,6 +458,8 @@ export const SystemAnomalyWidget: React.FC<Props> = ({ system }) => {
434
458
  isToggling={isToggling}
435
459
  onSuppress={handleSuppress}
436
460
  isSuppressing={suppressMutation.isPending}
461
+ canMute={canMute}
462
+ canManage={canManage}
437
463
  />
438
464
  ))}
439
465
  </div>