@checkstack/dashboard-frontend 0.8.4 → 0.8.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 +47 -0
- package/package.json +15 -15
- package/src/Dashboard.tsx +37 -12
- package/src/components/ProblemSystemCard.tsx +101 -45
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# @checkstack/dashboard-frontend
|
|
2
2
|
|
|
3
|
+
## 0.8.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 460ffd6: Align dashboard system-signal rows. Text (non-link) signals - shown when the
|
|
8
|
+
viewer can't open the target - were indented ~0.5rem further right than link
|
|
9
|
+
signals, because the link row used a negative horizontal margin (`-mx-2`) to let
|
|
10
|
+
its hover background bleed past the text while the text row did not. The text row
|
|
11
|
+
now uses the same horizontal box, so link and text signals line up.
|
|
12
|
+
- 56e7c75: Hide navigation, actions and links that the current user cannot use, so anonymous
|
|
13
|
+
and read-only users no longer see entries that lead to "Access Denied" or to
|
|
14
|
+
actions the server would reject.
|
|
15
|
+
|
|
16
|
+
- **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.
|
|
17
|
+
- **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).
|
|
18
|
+
- **Notification Settings**: hidden from anonymous users - notifications are per-user, so an anonymous visitor can't have any.
|
|
19
|
+
- **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.
|
|
20
|
+
- **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`.
|
|
21
|
+
- **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.
|
|
22
|
+
- **Plugin Manager**: the "Install plugin" button (which opens the install-gated page) is hidden from users with only `plugin` view access.
|
|
23
|
+
- **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`.
|
|
24
|
+
|
|
25
|
+
The `@checkstack/ai-backend` bump is only the regenerated bundled docs index
|
|
26
|
+
(the frontend routing guide gained the `nav.isVisible` section); no code change.
|
|
27
|
+
|
|
28
|
+
**BREAKING (`@checkstack/frontend-api`):** the `AccessApi` interface gains a
|
|
29
|
+
required `useIsAuthenticated()` method. Custom `AccessApi` implementations must
|
|
30
|
+
add it (it returns `{ loading, isAuthenticated }`). The built-in auth
|
|
31
|
+
implementation and the no-auth fallback already do. `NavEntry` also gains an
|
|
32
|
+
optional `isVisible` predicate (purely additive).
|
|
33
|
+
|
|
34
|
+
- Updated dependencies [56e7c75]
|
|
35
|
+
- Updated dependencies [56e7c75]
|
|
36
|
+
- @checkstack/frontend-api@0.9.0
|
|
37
|
+
- @checkstack/catalog-common@2.3.4
|
|
38
|
+
- @checkstack/ui@1.15.1
|
|
39
|
+
- @checkstack/common@0.15.0
|
|
40
|
+
- @checkstack/healthcheck-common@1.5.4
|
|
41
|
+
- @checkstack/incident-common@1.4.4
|
|
42
|
+
- @checkstack/maintenance-common@1.4.4
|
|
43
|
+
- @checkstack/catalog-frontend@0.11.5
|
|
44
|
+
- @checkstack/tips-frontend@0.3.5
|
|
45
|
+
- @checkstack/command-frontend@0.3.5
|
|
46
|
+
- @checkstack/queue-frontend@0.5.5
|
|
47
|
+
- @checkstack/command-common@0.3.3
|
|
48
|
+
- @checkstack/signal-frontend@0.2.4
|
|
49
|
+
|
|
3
50
|
## 0.8.4
|
|
4
51
|
|
|
5
52
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/dashboard-frontend",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.tsx",
|
|
@@ -14,19 +14,19 @@
|
|
|
14
14
|
"lint:code": "eslint . --max-warnings 0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@checkstack/catalog-common": "2.3.
|
|
18
|
-
"@checkstack/catalog-frontend": "0.11.
|
|
19
|
-
"@checkstack/command-common": "0.3.
|
|
20
|
-
"@checkstack/command-frontend": "0.3.
|
|
21
|
-
"@checkstack/common": "0.
|
|
22
|
-
"@checkstack/frontend-api": "0.
|
|
23
|
-
"@checkstack/healthcheck-common": "1.5.
|
|
24
|
-
"@checkstack/incident-common": "1.4.
|
|
25
|
-
"@checkstack/maintenance-common": "1.4.
|
|
26
|
-
"@checkstack/queue-frontend": "0.5.
|
|
27
|
-
"@checkstack/signal-frontend": "0.2.
|
|
28
|
-
"@checkstack/tips-frontend": "0.3.
|
|
29
|
-
"@checkstack/ui": "1.15.
|
|
17
|
+
"@checkstack/catalog-common": "2.3.4",
|
|
18
|
+
"@checkstack/catalog-frontend": "0.11.5",
|
|
19
|
+
"@checkstack/command-common": "0.3.3",
|
|
20
|
+
"@checkstack/command-frontend": "0.3.5",
|
|
21
|
+
"@checkstack/common": "0.15.0",
|
|
22
|
+
"@checkstack/frontend-api": "0.9.0",
|
|
23
|
+
"@checkstack/healthcheck-common": "1.5.4",
|
|
24
|
+
"@checkstack/incident-common": "1.4.4",
|
|
25
|
+
"@checkstack/maintenance-common": "1.4.4",
|
|
26
|
+
"@checkstack/queue-frontend": "0.5.5",
|
|
27
|
+
"@checkstack/signal-frontend": "0.2.4",
|
|
28
|
+
"@checkstack/tips-frontend": "0.3.5",
|
|
29
|
+
"@checkstack/ui": "1.15.1",
|
|
30
30
|
"date-fns": "^4.4.0",
|
|
31
31
|
"lucide-react": "^1.17.0",
|
|
32
32
|
"react": "19.2.7",
|
|
@@ -36,6 +36,6 @@
|
|
|
36
36
|
"typescript": "^5.0.0",
|
|
37
37
|
"@types/react": "^19.0.0",
|
|
38
38
|
"@checkstack/tsconfig": "0.0.7",
|
|
39
|
-
"@checkstack/scripts": "0.6.
|
|
39
|
+
"@checkstack/scripts": "0.6.1"
|
|
40
40
|
}
|
|
41
41
|
}
|
package/src/Dashboard.tsx
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import React, { useCallback, useMemo, useState } from "react";
|
|
2
2
|
import { Link, useNavigate } from "react-router-dom";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
usePluginClient,
|
|
5
|
+
ExtensionSlot,
|
|
6
|
+
useApi,
|
|
7
|
+
accessApiRef,
|
|
8
|
+
} from "@checkstack/frontend-api";
|
|
4
9
|
import {
|
|
5
10
|
CatalogApi,
|
|
11
|
+
catalogAccess,
|
|
6
12
|
catalogRoutes,
|
|
7
13
|
SystemSignalsSlot,
|
|
8
14
|
type SystemSignalsMap,
|
|
@@ -67,6 +73,16 @@ export const Dashboard: React.FC = () => {
|
|
|
67
73
|
const { isLowPower } = usePerformance();
|
|
68
74
|
const catalogClient = usePluginClient(CatalogApi);
|
|
69
75
|
const navigate = useNavigate();
|
|
76
|
+
const accessApi = useApi(accessApiRef);
|
|
77
|
+
// The Catalog CONFIG page (add/manage systems) requires manage; "Open Catalog"
|
|
78
|
+
// CTAs point there, so hide them from users who can't reach it. The catalog
|
|
79
|
+
// HOME view only needs read, gating the "View catalog" link.
|
|
80
|
+
const { allowed: canManageCatalog } = accessApi.useAccess(
|
|
81
|
+
catalogAccess.system.manage,
|
|
82
|
+
);
|
|
83
|
+
const { allowed: canViewCatalog } = accessApi.useAccess(
|
|
84
|
+
catalogAccess.system.read,
|
|
85
|
+
);
|
|
70
86
|
|
|
71
87
|
const [terminalEntries, setTerminalEntries] = useState<TerminalEntry[]>([]);
|
|
72
88
|
const [activeTone, setActiveTone] = useState<SystemSignalTone | null>(null);
|
|
@@ -161,12 +177,16 @@ export const Dashboard: React.FC = () => {
|
|
|
161
177
|
"Attach health checks so the dashboard surfaces issues the moment they appear.",
|
|
162
178
|
]}
|
|
163
179
|
actions={
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
180
|
+
canManageCatalog ? (
|
|
181
|
+
<Button
|
|
182
|
+
onClick={() =>
|
|
183
|
+
navigate(resolveRoute(catalogRoutes.routes.config))
|
|
184
|
+
}
|
|
185
|
+
>
|
|
186
|
+
<LayoutGrid className="w-4 h-4 mr-2" />
|
|
187
|
+
Open Catalog
|
|
188
|
+
</Button>
|
|
189
|
+
) : undefined
|
|
170
190
|
}
|
|
171
191
|
/>
|
|
172
192
|
);
|
|
@@ -229,10 +249,15 @@ export const Dashboard: React.FC = () => {
|
|
|
229
249
|
attach a health check to it.
|
|
230
250
|
</>
|
|
231
251
|
}
|
|
232
|
-
action={
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
252
|
+
action={
|
|
253
|
+
canManageCatalog
|
|
254
|
+
? {
|
|
255
|
+
label: "Open Catalog",
|
|
256
|
+
onClick: () =>
|
|
257
|
+
navigate(resolveRoute(catalogRoutes.routes.config)),
|
|
258
|
+
}
|
|
259
|
+
: undefined
|
|
260
|
+
}
|
|
236
261
|
actionHint={
|
|
237
262
|
<span className="inline-flex items-center gap-1.5">
|
|
238
263
|
<Lightbulb
|
|
@@ -266,7 +291,7 @@ export const Dashboard: React.FC = () => {
|
|
|
266
291
|
System health
|
|
267
292
|
</h2>
|
|
268
293
|
</div>
|
|
269
|
-
{systemsCount > 0 && (
|
|
294
|
+
{systemsCount > 0 && canViewCatalog && (
|
|
270
295
|
<Link
|
|
271
296
|
to={catalogHref}
|
|
272
297
|
className={cn(
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Link } from "react-router-dom";
|
|
3
3
|
import { formatDistanceToNow } from "date-fns";
|
|
4
|
-
import { resolveRoute } from "@checkstack/common";
|
|
4
|
+
import { resolveRoute, type AccessRule } from "@checkstack/common";
|
|
5
|
+
import { useApi, accessApiRef } from "@checkstack/frontend-api";
|
|
5
6
|
import {
|
|
6
7
|
catalogRoutes,
|
|
7
8
|
type System,
|
|
@@ -36,59 +37,114 @@ const glow: Record<SystemSignalTone, string> = {
|
|
|
36
37
|
info: "from-info/[0.07]",
|
|
37
38
|
};
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
/** The icon + label + detail (+ hover chevron when `interactive`) of a signal. */
|
|
41
|
+
const SignalInner: React.FC<{
|
|
42
|
+
signal: SystemSignal;
|
|
43
|
+
isLowPower: boolean;
|
|
44
|
+
interactive: boolean;
|
|
45
|
+
}> = ({ signal, isLowPower, interactive }) => (
|
|
46
|
+
<>
|
|
47
|
+
<span
|
|
48
|
+
className={cn(
|
|
49
|
+
"flex size-6 shrink-0 items-center justify-center rounded-md",
|
|
50
|
+
chipBg[signal.tone],
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
<DynamicIcon name={signal.iconName} className="h-3.5 w-3.5" />
|
|
54
|
+
</span>
|
|
55
|
+
<span className="shrink-0 text-sm font-medium text-foreground">
|
|
56
|
+
{signal.label}
|
|
57
|
+
</span>
|
|
58
|
+
{signal.detail && (
|
|
59
|
+
<span className="min-w-0 truncate text-xs text-muted-foreground">
|
|
60
|
+
{signal.detail}
|
|
61
|
+
</span>
|
|
62
|
+
)}
|
|
63
|
+
{interactive && (
|
|
64
|
+
<ChevronRight
|
|
46
65
|
className={cn(
|
|
47
|
-
"
|
|
48
|
-
|
|
66
|
+
"ml-auto h-4 w-4 shrink-0 text-muted-foreground/70 opacity-0 group-hover/row:opacity-100",
|
|
67
|
+
!isLowPower && "transition-opacity",
|
|
49
68
|
)}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
aria-hidden="true"
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
/** Plain, non-clickable row (no href, or the user can't access the target). */
|
|
76
|
+
const SignalTextRow: React.FC<{ signal: SystemSignal; isLowPower: boolean }> = ({
|
|
77
|
+
signal,
|
|
78
|
+
isLowPower,
|
|
79
|
+
}) => (
|
|
80
|
+
// Same horizontal box as SignalLinkRow (`-mx-2 px-2`) so text and link rows
|
|
81
|
+
// line up - the link's negative margin would otherwise sit it 0.5rem left.
|
|
82
|
+
<li className="-mx-2 flex items-center gap-2.5 px-2 py-1.5">
|
|
83
|
+
<SignalInner signal={signal} isLowPower={isLowPower} interactive={false} />
|
|
84
|
+
</li>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
/** Clickable deep-link row. */
|
|
88
|
+
const SignalLinkRow: React.FC<{
|
|
89
|
+
signal: SystemSignal;
|
|
90
|
+
href: string;
|
|
91
|
+
isLowPower: boolean;
|
|
92
|
+
}> = ({ signal, href, isLowPower }) => (
|
|
93
|
+
<li>
|
|
94
|
+
<Link
|
|
95
|
+
to={href}
|
|
96
|
+
className={cn(
|
|
97
|
+
"group/row -mx-2 flex items-center gap-2.5 rounded-lg px-2 py-1.5 hover:bg-muted/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
98
|
+
!isLowPower && "transition-colors",
|
|
69
99
|
)}
|
|
70
|
-
|
|
100
|
+
>
|
|
101
|
+
<SignalInner signal={signal} isLowPower={isLowPower} interactive />
|
|
102
|
+
</Link>
|
|
103
|
+
</li>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A signal whose target page is permission-gated: render it as a link only when
|
|
108
|
+
* the user satisfies its access rule, otherwise as plain text (so the user is
|
|
109
|
+
* never sent to an "Access Denied" page). Own component so the `useAccess` hook
|
|
110
|
+
* is called in a stable position regardless of the surrounding list.
|
|
111
|
+
*/
|
|
112
|
+
const GatedSignalRow: React.FC<{
|
|
113
|
+
signal: SystemSignal;
|
|
114
|
+
href: string;
|
|
115
|
+
accessRule: AccessRule;
|
|
116
|
+
isLowPower: boolean;
|
|
117
|
+
}> = ({ signal, href, accessRule, isLowPower }) => {
|
|
118
|
+
const accessApi = useApi(accessApiRef);
|
|
119
|
+
const { allowed } = accessApi.useAccess(accessRule);
|
|
120
|
+
return allowed ? (
|
|
121
|
+
<SignalLinkRow signal={signal} href={href} isLowPower={isLowPower} />
|
|
122
|
+
) : (
|
|
123
|
+
<SignalTextRow signal={signal} isLowPower={isLowPower} />
|
|
71
124
|
);
|
|
125
|
+
};
|
|
72
126
|
|
|
127
|
+
const SignalRow: React.FC<{ signal: SystemSignal; isLowPower: boolean }> = ({
|
|
128
|
+
signal,
|
|
129
|
+
isLowPower,
|
|
130
|
+
}) => {
|
|
131
|
+
// No destination -> plain text.
|
|
73
132
|
if (!signal.href) {
|
|
133
|
+
return <SignalTextRow signal={signal} isLowPower={isLowPower} />;
|
|
134
|
+
}
|
|
135
|
+
// Gated destination -> link only if the user can actually open it.
|
|
136
|
+
if (signal.accessRule) {
|
|
74
137
|
return (
|
|
75
|
-
<
|
|
138
|
+
<GatedSignalRow
|
|
139
|
+
signal={signal}
|
|
140
|
+
href={signal.href}
|
|
141
|
+
accessRule={signal.accessRule}
|
|
142
|
+
isLowPower={isLowPower}
|
|
143
|
+
/>
|
|
76
144
|
);
|
|
77
145
|
}
|
|
78
|
-
|
|
79
|
-
return
|
|
80
|
-
<li>
|
|
81
|
-
<Link
|
|
82
|
-
to={signal.href}
|
|
83
|
-
className={cn(
|
|
84
|
-
"group/row -mx-2 flex items-center gap-2.5 rounded-lg px-2 py-1.5 hover:bg-muted/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
85
|
-
!isLowPower && "transition-colors",
|
|
86
|
-
)}
|
|
87
|
-
>
|
|
88
|
-
{body}
|
|
89
|
-
</Link>
|
|
90
|
-
</li>
|
|
91
|
-
);
|
|
146
|
+
// Destination needs no specific permission -> always a link.
|
|
147
|
+
return <SignalLinkRow signal={signal} href={signal.href} isLowPower={isLowPower} />;
|
|
92
148
|
};
|
|
93
149
|
|
|
94
150
|
/**
|