@checkstack/ui 1.14.0 → 1.15.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.
@@ -2,4 +2,5 @@ import type { AccessApi } from "@checkstack/frontend-api";
2
2
 
3
3
  export const mockAccessApi: AccessApi = {
4
4
  useAccess: () => ({ loading: false, allowed: true }),
5
+ useIsAuthenticated: () => ({ loading: false, isAuthenticated: true }),
5
6
  };
package/CHANGELOG.md CHANGED
@@ -1,5 +1,121 @@
1
1
  # @checkstack/ui
2
2
 
3
+ ## 1.15.1
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
+ - 56e7c75: Fix frontend access checks to use FULLY-QUALIFIED access-rule ids, and resolve
30
+ the anonymous role on the frontend.
31
+
32
+ Granted access-rule ids are stored fully-qualified as `{pluginId}.{ruleId}` (e.g.
33
+ `incident.incident.read`) so two plugins defining the same short rule id never
34
+ collide. The frontend, however, was checking the UNqualified id (`incident.read`)
35
+ via `isAccessRuleSatisfied`, so every check failed for any user without the `*`
36
+ (admin) grant - masked in development because dev-auth grants `*`. This silently
37
+ broke ALL non-admin frontend gating (route guards, sidebar entries, and
38
+ `useAccess`-based button/link gating).
39
+
40
+ - **`@checkstack/common`**: `AccessRule` now carries a REQUIRED owning `pluginId`;
41
+ `access()` / `accessPair()` require and stamp it; `isAccessRuleSatisfied`
42
+ qualifies the rule (`{pluginId}.{id}`, plus the manage->read escalation) and
43
+ matches ONLY the qualified form. There is intentionally NO unqualified fallback
44
+ - matching a bare id would let one plugin's grant satisfy another plugin's
45
+ identically-named rule (a cross-plugin privilege-escalation flaw). Every plugin
46
+ that defines access rules now passes its own `pluginId`.
47
+ - **`@checkstack/backend`**: `pluginManager.getAllAccessRules()` no longer strips
48
+ the `pluginId` field (the rule `id` is already fully-qualified for the DB sync).
49
+ - **Route guard** (`@checkstack/frontend` / `@checkstack/frontend-api`) now
50
+ checks the FULL rule object (so it qualifies and escalates), not a bare id.
51
+ - **Anonymous role on the frontend**: the `accessRules` procedure is now
52
+ `public`, returning the configurable anonymous role's grants to unauthenticated
53
+ callers; `useAccessRules` fetches them for guests instead of returning an empty
54
+ set. So anonymous UI now reflects exactly what the anonymous role is allowed -
55
+ which an admin can change (`isPublic` is only the seeded default).
56
+ - Incident / maintenance / SLO detail routes are now read-gated (their read rule
57
+ is an `isPublic` default, so the anonymous role holds it unless an admin
58
+ revokes it); their dashboard status signals carry that rule and render as a
59
+ link only when the viewer may open it.
60
+
61
+ **BREAKING (`@checkstack/common`):** `AccessRule.pluginId` is now REQUIRED, and
62
+ `access()` / `accessPair()` require a `pluginId` option. `isAccessRuleSatisfied`
63
+ matches ONLY the fully-qualified `{pluginId}.{ruleId}` form - the previous
64
+ unqualified fallback is removed, because it was a cross-plugin
65
+ privilege-escalation flaw. Any code constructing an `AccessRule` or calling
66
+ `access()`/`accessPair()` must supply the owning `pluginId`.
67
+
68
+ Verified live against an anonymous caller: read pages resolve (qualified match),
69
+ manage actions are denied, manage->read escalation and `*` still work.
70
+
71
+ - Updated dependencies [56e7c75]
72
+ - Updated dependencies [56e7c75]
73
+ - @checkstack/frontend-api@0.9.0
74
+ - @checkstack/common@0.15.0
75
+ - @checkstack/template-engine@0.4.3
76
+
77
+ ## 1.15.0
78
+
79
+ ### Minor Changes
80
+
81
+ - fb705df: Upgrade React 18 to React 19 across the platform.
82
+
83
+ **BREAKING (runtime frontend plugins):** React is shared as a Module Federation
84
+ singleton, so the host now provides **React 19** to every runtime plugin.
85
+ Frontend plugins built against React 18 must be rebuilt against React 19
86
+ (`react` / `react-dom` `^19`). The scaffold templates and the host/plugin MF
87
+ `requiredVersion` are updated to `^19`. `react` (and now `react-dom`) are pinned
88
+ to a single version across the workspace via syncpack so the singleton can never
89
+ skew (react and react-dom must match exactly).
90
+
91
+ The React 19 removed-API surface was audited - the codebase used only no-arg
92
+ `useRef()` (now `useRef<T | undefined>(undefined)`); no `ReactDOM.render`,
93
+ legacy context, string refs, or function-component `defaultProps`. This also
94
+ clears the `IMPORT_IS_UNDEFINED` build warnings for `React.use` /
95
+ `React.useOptimistic` (react-router 7 feature-detection), which React 19 exports.
96
+
97
+ The downstream `*-frontend` packages (and `@checkstack/infrastructure-common`)
98
+ receive only the mechanical `react` dependency bump (`patch`); the framework
99
+ packages carrying the shared-singleton change are bumped `minor`.
100
+
101
+ ### Patch Changes
102
+
103
+ - 9d8961c: Fix the double-scrolling on the AI chat page (`/ai/chat`). The page sized its
104
+ layout with a fixed `calc(100vh - 220px)` height, which overshot the available
105
+ space when the page subtitle wrapped to two lines - so the whole page scrolled
106
+ on top of the message list's own scroll.
107
+
108
+ `PageLayout` gains an opt-in `fillHeight` prop that fills the viewport via a
109
+ bounded flex height chain (established in the app shell) instead of viewport
110
+ math; the chat page uses it so only the message list scrolls and the page itself
111
+ never does. Normal document-flow pages are unaffected (they still scroll the
112
+ main area as before).
113
+
114
+ - Updated dependencies [fb705df]
115
+ - @checkstack/frontend-api@0.8.0
116
+ - @checkstack/common@0.14.1
117
+ - @checkstack/template-engine@0.4.2
118
+
3
119
  ## 1.14.0
4
120
 
5
121
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/ui",
3
- "version": "1.14.0",
3
+ "version": "1.15.1",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -13,9 +13,9 @@
13
13
  "./src/*": "./src/*"
14
14
  },
15
15
  "dependencies": {
16
- "@checkstack/common": "0.14.1",
17
- "@checkstack/frontend-api": "0.7.2",
18
- "@checkstack/template-engine": "0.4.2",
16
+ "@checkstack/common": "0.15.0",
17
+ "@checkstack/frontend-api": "0.9.0",
18
+ "@checkstack/template-engine": "0.4.3",
19
19
  "@codingame/monaco-vscode-editor-api": "25.1.2",
20
20
  "@codingame/monaco-vscode-languages-service-override": "25.1.2",
21
21
  "@codingame/monaco-vscode-standalone-json-language-features": "25.1.2",
@@ -37,9 +37,9 @@
37
37
  "jsonc-parser": "^3.3.1",
38
38
  "lucide-react": "^1.17.0",
39
39
  "monaco-languageclient": "10.7.0",
40
- "react": "^18.3.1",
40
+ "react": "19.2.7",
41
41
  "react-day-picker": "^9.13.0",
42
- "react-dom": "^18.2.0",
42
+ "react-dom": "19.2.7",
43
43
  "react-markdown": "^10.1.0",
44
44
  "react-router-dom": "^7.16.0",
45
45
  "recharts": "^3.6.0",
@@ -51,8 +51,8 @@
51
51
  "zod": "^4.2.1"
52
52
  },
53
53
  "devDependencies": {
54
- "@checkstack/scripts": "0.5.0",
55
- "@checkstack/test-utils-frontend": "0.1.0",
54
+ "@checkstack/scripts": "0.6.1",
55
+ "@checkstack/test-utils-frontend": "0.1.1",
56
56
  "@checkstack/tsconfig": "0.0.7",
57
57
  "@storybook/addon-a11y": "^10.4.1",
58
58
  "@storybook/addon-docs": "^10.4.1",
@@ -62,8 +62,8 @@
62
62
  "@testing-library/react": "^16.0.0",
63
63
  "@types/bun": "^1.3.14",
64
64
  "@types/node": "^20.19.27",
65
- "@types/react": "^18.2.0",
66
- "@types/react-dom": "^18.2.0",
65
+ "@types/react": "^19.0.0",
66
+ "@types/react-dom": "^19.0.0",
67
67
  "@vitejs/plugin-react": "^6.0.2",
68
68
  "bun-types": "^1.3.14",
69
69
  "autoprefixer": "^10.4.18",
@@ -31,12 +31,14 @@ export const NavItem: React.FC<NavItemProps> = ({
31
31
  // Always call hooks at top level
32
32
  const accessApi = useApi(accessApiRef);
33
33
 
34
- // Create a dummy access rule for when accessRule is undefined
34
+ // Create a dummy access rule for when accessRule is undefined (its result is
35
+ // ignored - `hasAccess` below falls back to `true` when no rule is provided).
35
36
  const dummyRule: AccessRule = {
36
37
  id: "",
37
38
  resource: "",
38
39
  level: "read",
39
40
  description: "",
41
+ pluginId: "",
40
42
  };
41
43
  const { allowed, loading } = accessApi.useAccess(accessRule ?? dummyRule);
42
44
  const hasAccess = accessRule ? allowed : true;
@@ -7,6 +7,7 @@ import {
7
7
  LoadingSpinner,
8
8
  AccessDenied,
9
9
  } from "..";
10
+ import { cn } from "../utils";
10
11
 
11
12
  interface PageLayoutProps {
12
13
  title: string;
@@ -16,6 +17,15 @@ interface PageLayoutProps {
16
17
  loading?: boolean;
17
18
  allowed?: boolean;
18
19
  children: React.ReactNode;
20
+ /**
21
+ * Make the content area fill the viewport height instead of growing with its
22
+ * content. The page's own children then own their scrolling (e.g. a chat
23
+ * message list), so the page itself never scrolls. Use for full-height app
24
+ * surfaces (chat, canvases); leave off for normal document-flow pages, which
25
+ * scroll the main area as usual. Relies on the bounded height chain in
26
+ * core/frontend's App shell.
27
+ */
28
+ fillHeight?: boolean;
19
29
  maxWidth?:
20
30
  | "sm"
21
31
  | "md"
@@ -38,6 +48,7 @@ export const PageLayout: React.FC<PageLayoutProps> = ({
38
48
  loading,
39
49
  allowed,
40
50
  children,
51
+ fillHeight = false,
41
52
  maxWidth = "7xl",
42
53
  }) => {
43
54
  // If loading is explicitly true, show loading state
@@ -82,18 +93,23 @@ export const PageLayout: React.FC<PageLayoutProps> = ({
82
93
  }
83
94
 
84
95
  return (
85
- <Page>
96
+ <Page className={fillHeight ? "min-h-0" : undefined}>
86
97
  <PageHeader
87
98
  title={title}
88
99
  subtitle={subtitle}
89
100
  icon={icon}
90
101
  actions={actions}
91
102
  />
92
- <PageContent>
103
+ <PageContent
104
+ className={fillHeight ? "flex min-h-0 flex-col" : undefined}
105
+ >
93
106
  <div
94
- className={
95
- maxWidth === "full" ? "space-y-6" : `max-w-${maxWidth} space-y-6`
96
- }
107
+ className={cn(
108
+ maxWidth === "full" ? undefined : `max-w-${maxWidth}`,
109
+ // fillHeight: fill the bounded content area and let children scroll.
110
+ // Otherwise: normal document flow with vertical rhythm.
111
+ fillHeight ? "flex min-h-0 flex-1 flex-col" : "space-y-6",
112
+ )}
97
113
  >
98
114
  {children}
99
115
  </div>
@@ -14,8 +14,8 @@ export function useAnimatedNumber(
14
14
  decimals = 2,
15
15
  ): string {
16
16
  const [displayValue, setDisplayValue] = useState(targetValue);
17
- const animationRef = useRef<number>();
18
- const startTimeRef = useRef<number>();
17
+ const animationRef = useRef<number | undefined>(undefined);
18
+ const startTimeRef = useRef<number | undefined>(undefined);
19
19
  const startValueRef = useRef<number | undefined>(undefined);
20
20
 
21
21
  useEffect(() => {