@checkstack/catalog-frontend 0.9.1 → 0.10.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.
- package/CHANGELOG.md +132 -0
- package/package.json +13 -12
- package/src/components/CatalogConfigPage.tsx +63 -11
- package/src/components/DraggableSystem.tsx +22 -19
- package/src/components/DroppableGroup.tsx +28 -8
- package/src/components/SystemDetailPage.tsx +38 -1
- package/src/components/SystemEditor.tsx +4 -0
- package/src/components/SystemLinksEditor.tsx +63 -0
- package/src/index.tsx +1 -0
- package/tsconfig.json +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,137 @@
|
|
|
1
1
|
# @checkstack/catalog-frontend
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1ef2e79: feat: hotlinks on incidents/maintenances and additional links on systems
|
|
8
|
+
|
|
9
|
+
Users with `manage` access on an incident, maintenance, or system can now
|
|
10
|
+
attach free-form URL "hotlinks" — Jira tickets, runbooks, dashboards, ticket
|
|
11
|
+
tools, etc. — alongside the existing fields.
|
|
12
|
+
|
|
13
|
+
- **Incidents** & **maintenances**: links live on the entity itself and are
|
|
14
|
+
surfaced both in the editor dialog and on the public detail page. Two new
|
|
15
|
+
RPC procedures per plugin (`addLink`, `removeLink`) gated behind the
|
|
16
|
+
existing `manage` access rule. Links are returned as part of
|
|
17
|
+
`getIncident` / `getMaintenance` and cache-invalidated on every link
|
|
18
|
+
mutation.
|
|
19
|
+
- **Systems**: a parallel `system_links` table with `getSystemLinks`,
|
|
20
|
+
`addSystemLink`, `removeSystemLink` procedures. Surfaced inside the
|
|
21
|
+
system editor (next to contacts) and on the read-only system detail
|
|
22
|
+
sidebar. Cache-scoped per-system so list endpoints remain hot.
|
|
23
|
+
- **Shared UI**: a `LinksEditor` component in `@checkstack/ui` does the
|
|
24
|
+
presentation; the three plugins each own their own RPC wiring.
|
|
25
|
+
|
|
26
|
+
Database changes ship as additive migrations (new `incident_links`,
|
|
27
|
+
`maintenance_links`, `system_links` tables, all FK-cascaded on parent
|
|
28
|
+
delete). No existing columns or rows are touched.
|
|
29
|
+
|
|
30
|
+
The system incident and maintenance history pages now sort by relevance:
|
|
31
|
+
active entries (non-`resolved` incidents, `scheduled` or `in_progress`
|
|
32
|
+
maintenances) appear at the top, with creation date descending as the
|
|
33
|
+
tiebreaker.
|
|
34
|
+
|
|
35
|
+
- 3547670: Wire the new tips infrastructure across the frontends:
|
|
36
|
+
|
|
37
|
+
**Empty-state coaching.** Replace generic "no items" copy with onboarding
|
|
38
|
+
guidance — short description, three numbered steps and a primary CTA — on
|
|
39
|
+
every EmptyState that has a meaningful next action. Affects: catalog
|
|
40
|
+
(systems + groups), dashboard, health-check page, integrations (subscriptions
|
|
41
|
+
|
|
42
|
+
- provider connections), GitOps providers + secrets, GitOps provenance,
|
|
43
|
+
SLO config + overview, maintenance config, satellites, plugin manager,
|
|
44
|
+
incident config, announcements. Read-only EmptyStates (incident history,
|
|
45
|
+
maintenance history, plugin events) get clearer descriptions explaining
|
|
46
|
+
what would populate them.
|
|
47
|
+
|
|
48
|
+
**First-run anchored tips.** Add `<Tip>` popovers to the most important
|
|
49
|
+
"Create" affordances so first-time users see a one-line explanation of
|
|
50
|
+
what they're about to make and why it matters: catalog “Add System” /
|
|
51
|
+
“Add Group”, healthcheck “Create Check”, integrations “New Subscription”,
|
|
52
|
+
GitOps “Add Provider”, SLO “Create SLO”, maintenance “Create Maintenance”,
|
|
53
|
+
satellite “Create Satellite”, plugin-manager “Install plugin”, incident
|
|
54
|
+
“Report Incident”, announcement “New Announcement”. Each tip is dismissed
|
|
55
|
+
per user (server-backed when signed in, localStorage otherwise) and
|
|
56
|
+
namespaced through `qualifyTipId(plugin, …)` so it cannot escape the
|
|
57
|
+
plugin's own namespace.
|
|
58
|
+
|
|
59
|
+
**Welcome banner on the dashboard.** A `<TipBanner>` at the top of the
|
|
60
|
+
dashboard introduces Checkstack's main flow ("add a system, then a health
|
|
61
|
+
check") with a one-click jump into the catalog.
|
|
62
|
+
|
|
63
|
+
### Patch Changes
|
|
64
|
+
|
|
65
|
+
- f6f9a5c: Surface the source repository for GitOps-managed entities and gate the
|
|
66
|
+
system→group remove button on the system's lock state.
|
|
67
|
+
|
|
68
|
+
- `provenanceSchema` now carries a `sourceUrl` field, derived on the
|
|
69
|
+
backend from the provider type, baseUrl, repository and filePath. URLs
|
|
70
|
+
are constructed for github.com / gitlab.com and self-hosted
|
|
71
|
+
GitHub/GitLab where the API base ends in `/api/v3` or `/api/v4`. Other
|
|
72
|
+
baseUrls fall back to `null` so the UI keeps showing the raw path.
|
|
73
|
+
- New `useProvenanceLocks` hook (bulk variant of `useProvenanceLock`)
|
|
74
|
+
for views that render many entities and need to look up locks
|
|
75
|
+
client-side.
|
|
76
|
+
- New `<GitOpsSourceBadge>` popover component that replaces the bare
|
|
77
|
+
GitBranch icon on system and group catalog cards. The popover
|
|
78
|
+
surfaces the repository, file path, and a "View in source provider"
|
|
79
|
+
deep link.
|
|
80
|
+
- `<GitOpsLockBanner>` repo line is now a real link when a sourceUrl is
|
|
81
|
+
available.
|
|
82
|
+
- The system→group remove button in the catalog now disables itself
|
|
83
|
+
when the system is GitOps-managed, matching the backend lock that was
|
|
84
|
+
already in place.
|
|
85
|
+
|
|
86
|
+
- 950d6ec: Fix mobile UserMenu items rendering at zero height, group menu items by
|
|
87
|
+
section, and unstack cramped card headers on small viewports.
|
|
88
|
+
|
|
89
|
+
- **UserMenu mobile bug**: On mobile, the user-menu Sheet rendered every
|
|
90
|
+
menu item as a grid row, which combined with `flex-shrink: 1` on each
|
|
91
|
+
item collapsed the buttons whose internal layout uses `display: flex`
|
|
92
|
+
(the items registered with `useNavigate` rather than `<Link>`) to zero
|
|
93
|
+
content height. Switched the mobile container to a flex column with
|
|
94
|
+
`[&>*]:shrink-0` and added `min-h-0` so the sheet scrolls correctly
|
|
95
|
+
when the list overflows.
|
|
96
|
+
|
|
97
|
+
- **UserMenu grouping**: Slot extensions now accept an optional `group`
|
|
98
|
+
field. The user menu buckets `UserMenuItemsSlot` extensions by `group`
|
|
99
|
+
and renders each group under a labeled header (`Workspace`,
|
|
100
|
+
`Reliability`, `Configuration`, `Documentation`, `Account`). Existing
|
|
101
|
+
core plugins are tagged with the appropriate group; third-party plugins
|
|
102
|
+
can pick any of these or supply their own label. Untagged extensions
|
|
103
|
+
render last with no header. `UserMenuItemsBottomSlot` is unaffected.
|
|
104
|
+
|
|
105
|
+
- **Card header responsiveness**: `CardHeaderRow` (the primitive shared by
|
|
106
|
+
Incident, Maintenance, Auth, Catalog, GitOps and other config cards) now
|
|
107
|
+
stacks vertically on narrow viewports and only switches to a single row
|
|
108
|
+
at the `sm` breakpoint, so titles and adjacent filter controls (e.g.
|
|
109
|
+
status `Select`, "Show resolved" checkbox) no longer cram together on
|
|
110
|
+
mobile. Refactored the Incident and Maintenance config pages to use the
|
|
111
|
+
primitive instead of a hand-rolled `flex items-center justify-between`
|
|
112
|
+
row, and made their `Select` triggers full-width on mobile.
|
|
113
|
+
|
|
114
|
+
- Updated dependencies [42abfff]
|
|
115
|
+
- Updated dependencies [3547670]
|
|
116
|
+
- Updated dependencies [f6f9a5c]
|
|
117
|
+
- Updated dependencies [1ef2e79]
|
|
118
|
+
- Updated dependencies [aa89bc5]
|
|
119
|
+
- Updated dependencies [3547670]
|
|
120
|
+
- Updated dependencies [3547670]
|
|
121
|
+
- Updated dependencies [950d6ec]
|
|
122
|
+
- Updated dependencies [3547670]
|
|
123
|
+
- Updated dependencies [3547670]
|
|
124
|
+
- @checkstack/common@0.9.0
|
|
125
|
+
- @checkstack/ui@1.8.0
|
|
126
|
+
- @checkstack/gitops-frontend@0.4.0
|
|
127
|
+
- @checkstack/catalog-common@2.1.0
|
|
128
|
+
- @checkstack/frontend-api@0.5.0
|
|
129
|
+
- @checkstack/notification-frontend@0.4.0
|
|
130
|
+
- @checkstack/tips-frontend@0.2.0
|
|
131
|
+
- @checkstack/auth-frontend@0.6.0
|
|
132
|
+
- @checkstack/auth-common@0.6.6
|
|
133
|
+
- @checkstack/notification-common@1.0.2
|
|
134
|
+
|
|
3
135
|
## 0.9.1
|
|
4
136
|
|
|
5
137
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/catalog-frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.tsx",
|
|
@@ -13,15 +13,16 @@
|
|
|
13
13
|
"lint:code": "eslint . --max-warnings 0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@checkstack/auth-common": "0.6.
|
|
17
|
-
"@checkstack/auth-frontend": "0.5.
|
|
18
|
-
"@checkstack/catalog-common": "2.0.
|
|
19
|
-
"@checkstack/common": "0.
|
|
20
|
-
"@checkstack/frontend-api": "0.4.
|
|
21
|
-
"@checkstack/gitops-frontend": "0.3.
|
|
22
|
-
"@checkstack/notification-common": "1.0.
|
|
23
|
-
"@checkstack/notification-frontend": "0.3.
|
|
24
|
-
"@checkstack/
|
|
16
|
+
"@checkstack/auth-common": "0.6.5",
|
|
17
|
+
"@checkstack/auth-frontend": "0.5.33",
|
|
18
|
+
"@checkstack/catalog-common": "2.0.1",
|
|
19
|
+
"@checkstack/common": "0.8.0",
|
|
20
|
+
"@checkstack/frontend-api": "0.4.2",
|
|
21
|
+
"@checkstack/gitops-frontend": "0.3.8",
|
|
22
|
+
"@checkstack/notification-common": "1.0.1",
|
|
23
|
+
"@checkstack/notification-frontend": "0.3.1",
|
|
24
|
+
"@checkstack/tips-frontend": "0.1.0",
|
|
25
|
+
"@checkstack/ui": "1.7.1",
|
|
25
26
|
"@dnd-kit/core": "^6.3.1",
|
|
26
27
|
"@dnd-kit/utilities": "^3.2.2",
|
|
27
28
|
"lucide-react": "^0.344.0",
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"typescript": "^5.0.0",
|
|
33
34
|
"@types/react": "^18.2.0",
|
|
34
|
-
"@checkstack/tsconfig": "0.0.
|
|
35
|
-
"@checkstack/scripts": "0.
|
|
35
|
+
"@checkstack/tsconfig": "0.0.7",
|
|
36
|
+
"@checkstack/scripts": "0.3.0"
|
|
36
37
|
}
|
|
37
38
|
}
|
|
@@ -17,7 +17,11 @@ import {
|
|
|
17
17
|
usePluginClient,
|
|
18
18
|
} from "@checkstack/frontend-api";
|
|
19
19
|
import { System, CatalogApi } from "../api";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
catalogAccess,
|
|
22
|
+
pluginMetadata as catalogPluginMetadata,
|
|
23
|
+
} from "@checkstack/catalog-common";
|
|
24
|
+
import { Tip } from "@checkstack/tips-frontend";
|
|
21
25
|
import {
|
|
22
26
|
PageLayout,
|
|
23
27
|
Card,
|
|
@@ -337,10 +341,19 @@ export const CatalogConfigPage = () => {
|
|
|
337
341
|
<Server className="w-5 h-5 text-muted-foreground" />
|
|
338
342
|
Systems
|
|
339
343
|
</CardTitle>
|
|
340
|
-
<
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
+
<Tip
|
|
345
|
+
plugin={catalogPluginMetadata}
|
|
346
|
+
id="systems.create"
|
|
347
|
+
title="Start here: add a system"
|
|
348
|
+
description="A system is anything you want Checkstack to keep an eye on — a service, a host, a job, a database. Almost everything else (health checks, SLOs, incidents, notifications) hangs off systems."
|
|
349
|
+
side="bottom"
|
|
350
|
+
align="end"
|
|
351
|
+
>
|
|
352
|
+
<Button size="sm" onClick={() => setIsSystemEditorOpen(true)}>
|
|
353
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
354
|
+
Add System
|
|
355
|
+
</Button>
|
|
356
|
+
</Tip>
|
|
344
357
|
</CardHeaderRow>
|
|
345
358
|
{systems.length > 0 && groups.length > 0 && (
|
|
346
359
|
<p className="text-xs text-muted-foreground mt-1">
|
|
@@ -351,7 +364,22 @@ export const CatalogConfigPage = () => {
|
|
|
351
364
|
</CardHeader>
|
|
352
365
|
<CardContent className="space-y-4">
|
|
353
366
|
{systems.length === 0 ? (
|
|
354
|
-
<EmptyState
|
|
367
|
+
<EmptyState
|
|
368
|
+
icon={<Server className="size-10" />}
|
|
369
|
+
title="No systems yet"
|
|
370
|
+
description="Systems are the things you monitor. Once you add one, you can attach health checks, SLOs, maintenance windows and incident history to it."
|
|
371
|
+
steps={[
|
|
372
|
+
"Click “Add System” to register your first service, host or job.",
|
|
373
|
+
"Group related systems so dashboards and on-call rotations stay tidy.",
|
|
374
|
+
"Wire health checks to a system so its health status reflects reality and subscribers get notified on changes.",
|
|
375
|
+
]}
|
|
376
|
+
actions={
|
|
377
|
+
<Button onClick={() => setIsSystemEditorOpen(true)}>
|
|
378
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
379
|
+
Add your first system
|
|
380
|
+
</Button>
|
|
381
|
+
}
|
|
382
|
+
/>
|
|
355
383
|
) : (
|
|
356
384
|
<div className="space-y-2">
|
|
357
385
|
{systems.map((system) => (
|
|
@@ -381,10 +409,19 @@ export const CatalogConfigPage = () => {
|
|
|
381
409
|
<LayoutGrid className="w-5 h-5 text-muted-foreground" />
|
|
382
410
|
Groups
|
|
383
411
|
</CardTitle>
|
|
384
|
-
<
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
412
|
+
<Tip
|
|
413
|
+
plugin={catalogPluginMetadata}
|
|
414
|
+
id="groups.create"
|
|
415
|
+
title="Group systems that belong together"
|
|
416
|
+
description="Groups are how Checkstack rolls up status: a group is healthy when all of its systems are healthy. Use them per team, per product area, or per environment."
|
|
417
|
+
side="bottom"
|
|
418
|
+
align="end"
|
|
419
|
+
>
|
|
420
|
+
<Button size="sm" onClick={() => setIsGroupEditorOpen(true)}>
|
|
421
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
422
|
+
Add Group
|
|
423
|
+
</Button>
|
|
424
|
+
</Tip>
|
|
388
425
|
</CardHeaderRow>
|
|
389
426
|
{groups.length > 0 && systems.length > 0 && (
|
|
390
427
|
<p className="text-xs text-muted-foreground mt-1">
|
|
@@ -394,7 +431,22 @@ export const CatalogConfigPage = () => {
|
|
|
394
431
|
</CardHeader>
|
|
395
432
|
<CardContent className="space-y-4">
|
|
396
433
|
{groups.length === 0 ? (
|
|
397
|
-
<EmptyState
|
|
434
|
+
<EmptyState
|
|
435
|
+
icon={<LayoutGrid className="size-10" />}
|
|
436
|
+
title="No groups yet"
|
|
437
|
+
description="Groups roll up the health of multiple systems into a single status — useful for teams, products or environments."
|
|
438
|
+
steps={[
|
|
439
|
+
"Click “Add Group” and give it a meaningful name.",
|
|
440
|
+
"Drag systems from the left panel into the group, or use the assign button on each system.",
|
|
441
|
+
"Subscribe to the group from the status page to alert your team on any rolled-up incident.",
|
|
442
|
+
]}
|
|
443
|
+
actions={
|
|
444
|
+
<Button onClick={() => setIsGroupEditorOpen(true)}>
|
|
445
|
+
<Plus className="w-4 h-4 mr-2" />
|
|
446
|
+
Add your first group
|
|
447
|
+
</Button>
|
|
448
|
+
}
|
|
449
|
+
/>
|
|
398
450
|
) : (
|
|
399
451
|
<div className="space-y-2">
|
|
400
452
|
{groups.map((group) => (
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { useDraggable } from "@dnd-kit/core";
|
|
3
|
-
import { GripVertical, Edit, Trash2, FolderPlus
|
|
3
|
+
import { GripVertical, Edit, Trash2, FolderPlus } from "lucide-react";
|
|
4
4
|
import { Button } from "@checkstack/ui";
|
|
5
5
|
import { ExtensionSlot } from "@checkstack/frontend-api";
|
|
6
6
|
import { CatalogSystemActionsSlot } from "@checkstack/catalog-common";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
useProvenanceLock,
|
|
9
|
+
GitOpsSourceBadge,
|
|
10
|
+
} from "@checkstack/gitops-frontend";
|
|
8
11
|
import type { Group, System } from "../api";
|
|
9
12
|
|
|
10
13
|
interface DraggableSystemProps {
|
|
@@ -39,7 +42,7 @@ export const DraggableSystem = ({
|
|
|
39
42
|
}: DraggableSystemProps) => {
|
|
40
43
|
const [isPickerOpen, setIsPickerOpen] = useState(false);
|
|
41
44
|
|
|
42
|
-
const { isLocked } = useProvenanceLock({
|
|
45
|
+
const { isLocked, provenance } = useProvenanceLock({
|
|
43
46
|
kind: "System",
|
|
44
47
|
entityId: system.id,
|
|
45
48
|
});
|
|
@@ -58,23 +61,23 @@ export const DraggableSystem = ({
|
|
|
58
61
|
>
|
|
59
62
|
{/* Main row: grip + name/description */}
|
|
60
63
|
<div className="flex items-start gap-2 p-3 pb-2">
|
|
61
|
-
{/* Grip handle — only this element triggers the drag
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
className=
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
64
|
+
{/* Grip handle — only this element triggers the drag.
|
|
65
|
+
When GitOps-locked, swap in the source badge so users can click
|
|
66
|
+
through to the file that owns this system. */}
|
|
67
|
+
{isLocked && provenance ? (
|
|
68
|
+
<div className="flex-shrink-0 mt-0.5">
|
|
69
|
+
<GitOpsSourceBadge provenance={provenance} />
|
|
70
|
+
</div>
|
|
71
|
+
) : (
|
|
72
|
+
<div
|
|
73
|
+
{...listeners}
|
|
74
|
+
{...attributes}
|
|
75
|
+
className="flex-shrink-0 mt-0.5 text-muted-foreground/40 touch-none cursor-grab active:cursor-grabbing hover:text-muted-foreground"
|
|
76
|
+
aria-label={`Drag ${system.name}`}
|
|
77
|
+
>
|
|
75
78
|
<GripVertical className="w-4 h-4" />
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
</div>
|
|
80
|
+
)}
|
|
78
81
|
|
|
79
82
|
{/* Name + description — gets all remaining width, never truncated */}
|
|
80
83
|
<div className="flex-1 min-w-0">
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { useDroppable } from "@dnd-kit/core";
|
|
2
2
|
import { EditableText, Button } from "@checkstack/ui";
|
|
3
|
-
import { Trash2
|
|
4
|
-
import {
|
|
3
|
+
import { Trash2 } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
useProvenanceLock,
|
|
6
|
+
useProvenanceLocks,
|
|
7
|
+
GitOpsSourceBadge,
|
|
8
|
+
} from "@checkstack/gitops-frontend";
|
|
5
9
|
import type { Group, System } from "../api";
|
|
6
10
|
|
|
7
11
|
interface DroppableGroupProps {
|
|
@@ -36,11 +40,13 @@ export const DroppableGroup = ({
|
|
|
36
40
|
}: DroppableGroupProps) => {
|
|
37
41
|
const { setNodeRef } = useDroppable({ id: group.id });
|
|
38
42
|
|
|
39
|
-
const { isLocked } = useProvenanceLock({
|
|
43
|
+
const { isLocked, provenance } = useProvenanceLock({
|
|
40
44
|
kind: "Group",
|
|
41
45
|
entityId: group.id,
|
|
42
46
|
});
|
|
43
47
|
|
|
48
|
+
const { getLock } = useProvenanceLocks();
|
|
49
|
+
|
|
44
50
|
const groupSystems = (group.systemIds ?? [])
|
|
45
51
|
.map((sysId) => systems.find((s) => s.id === sysId))
|
|
46
52
|
.filter((sys): sys is System => !!sys);
|
|
@@ -63,10 +69,8 @@ export const DroppableGroup = ({
|
|
|
63
69
|
{/* Group header */}
|
|
64
70
|
<div className="flex items-center justify-between">
|
|
65
71
|
<div className="flex items-center gap-2 flex-1">
|
|
66
|
-
{isLocked && (
|
|
67
|
-
<
|
|
68
|
-
<GitBranch className="w-4 h-4 text-primary shrink-0" />
|
|
69
|
-
</span>
|
|
72
|
+
{isLocked && provenance && (
|
|
73
|
+
<GitOpsSourceBadge provenance={provenance} />
|
|
70
74
|
)}
|
|
71
75
|
<div className="flex-1">
|
|
72
76
|
<EditableText
|
|
@@ -110,6 +114,8 @@ export const DroppableGroup = ({
|
|
|
110
114
|
<div className="pl-2 space-y-1">
|
|
111
115
|
{groupSystems.map((sys) => {
|
|
112
116
|
const isNew = newlyAddedSystemId === sys.id;
|
|
117
|
+
const systemLock = getLock({ kind: "System", entityId: sys.id });
|
|
118
|
+
const systemLocked = systemLock.isLocked;
|
|
113
119
|
return (
|
|
114
120
|
<div
|
|
115
121
|
key={sys.id}
|
|
@@ -119,11 +125,25 @@ export const DroppableGroup = ({
|
|
|
119
125
|
: "border-border shadow-none"
|
|
120
126
|
}`}
|
|
121
127
|
>
|
|
122
|
-
<span className="text-foreground truncate">
|
|
128
|
+
<span className="text-foreground truncate flex items-center gap-1.5">
|
|
129
|
+
{systemLocked && systemLock.provenance ? (
|
|
130
|
+
<GitOpsSourceBadge
|
|
131
|
+
provenance={systemLock.provenance}
|
|
132
|
+
iconClassName="w-3 h-3 text-primary"
|
|
133
|
+
/>
|
|
134
|
+
) : null}
|
|
135
|
+
{sys.name}
|
|
136
|
+
</span>
|
|
123
137
|
<Button
|
|
124
138
|
variant="ghost"
|
|
125
139
|
className="text-destructive/60 hover:text-destructive h-6 w-6 p-0 flex-shrink-0"
|
|
126
140
|
onClick={() => onRemoveSystem(group.id, sys.id)}
|
|
141
|
+
disabled={systemLocked}
|
|
142
|
+
title={
|
|
143
|
+
systemLocked
|
|
144
|
+
? "Managed by GitOps"
|
|
145
|
+
: `Remove ${sys.name} from ${group.name}`
|
|
146
|
+
}
|
|
127
147
|
aria-label={`Remove ${sys.name} from ${group.name}`}
|
|
128
148
|
>
|
|
129
149
|
<Trash2 className="w-3 h-3" />
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from "@checkstack/ui";
|
|
25
25
|
import { authApiRef } from "@checkstack/auth-frontend/api";
|
|
26
26
|
|
|
27
|
-
import { Activity, Calendar, Mail, User } from "lucide-react";
|
|
27
|
+
import { Activity, Calendar, ExternalLink, Mail, User } from "lucide-react";
|
|
28
28
|
|
|
29
29
|
export const SystemDetailPage: React.FC = () => {
|
|
30
30
|
const { systemId } = useParams<{ systemId: string }>();
|
|
@@ -49,6 +49,12 @@ export const SystemDetailPage: React.FC = () => {
|
|
|
49
49
|
{ enabled: !!systemId },
|
|
50
50
|
);
|
|
51
51
|
|
|
52
|
+
// Fetch additional links for this system
|
|
53
|
+
const { data: linksData } = catalogClient.getSystemLinks.useQuery(
|
|
54
|
+
{ systemId: systemId ?? "" },
|
|
55
|
+
{ enabled: !!systemId },
|
|
56
|
+
);
|
|
57
|
+
|
|
52
58
|
// Find the system from the fetched data
|
|
53
59
|
const system = systemsData?.systems.find((s) => s.id === systemId);
|
|
54
60
|
const loading = systemsLoading || groupsLoading;
|
|
@@ -206,6 +212,37 @@ export const SystemDetailPage: React.FC = () => {
|
|
|
206
212
|
|
|
207
213
|
<div className="h-px bg-border" />
|
|
208
214
|
|
|
215
|
+
{/* Additional Links */}
|
|
216
|
+
<div className="space-y-2">
|
|
217
|
+
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
218
|
+
Additional Links
|
|
219
|
+
</h3>
|
|
220
|
+
{!linksData || linksData.length === 0 ? (
|
|
221
|
+
<p className="text-sm text-muted-foreground">No links</p>
|
|
222
|
+
) : (
|
|
223
|
+
<div className="space-y-1.5">
|
|
224
|
+
{linksData.map((link) => (
|
|
225
|
+
<div
|
|
226
|
+
key={link.id}
|
|
227
|
+
className="flex items-center gap-2 text-sm"
|
|
228
|
+
>
|
|
229
|
+
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
230
|
+
<a
|
|
231
|
+
href={link.url}
|
|
232
|
+
target="_blank"
|
|
233
|
+
rel="noopener noreferrer"
|
|
234
|
+
className="text-primary hover:underline truncate"
|
|
235
|
+
>
|
|
236
|
+
{link.label ?? link.url}
|
|
237
|
+
</a>
|
|
238
|
+
</div>
|
|
239
|
+
))}
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<div className="h-px bg-border" />
|
|
245
|
+
|
|
209
246
|
{/* Groups */}
|
|
210
247
|
<div className="space-y-2">
|
|
211
248
|
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "@checkstack/ui";
|
|
14
14
|
import { TeamAccessEditor } from "@checkstack/auth-frontend";
|
|
15
15
|
import { ContactsEditor } from "./ContactsEditor";
|
|
16
|
+
import { SystemLinksEditor } from "./SystemLinksEditor";
|
|
16
17
|
import { ExtensionSlot } from "@checkstack/frontend-api";
|
|
17
18
|
import { SystemEditorSlot } from "@checkstack/catalog-common";
|
|
18
19
|
import { extractErrorMessage } from "@checkstack/common";
|
|
@@ -107,6 +108,9 @@ export const SystemEditor: React.FC<SystemEditorProps> = ({
|
|
|
107
108
|
{/* Contacts Editor - only shown for existing systems */}
|
|
108
109
|
{initialData?.id && <ContactsEditor systemId={initialData.id} />}
|
|
109
110
|
|
|
111
|
+
{/* Additional Links - only shown for existing systems */}
|
|
112
|
+
{initialData?.id && <SystemLinksEditor systemId={initialData.id} />}
|
|
113
|
+
|
|
110
114
|
{/* Team Access Editor - only shown for existing systems */}
|
|
111
115
|
{initialData?.id && (
|
|
112
116
|
<TeamAccessEditor
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { LinksEditor, useToast } from "@checkstack/ui";
|
|
3
|
+
import {
|
|
4
|
+
usePluginClient,
|
|
5
|
+
useApi,
|
|
6
|
+
accessApiRef,
|
|
7
|
+
} from "@checkstack/frontend-api";
|
|
8
|
+
import { CatalogApi, catalogAccess } from "@checkstack/catalog-common";
|
|
9
|
+
import { extractErrorMessage } from "@checkstack/common";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
systemId: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Catalog-specific wrapper around the shared {@link LinksEditor}. Owns the
|
|
17
|
+
* RPC + cache wiring so callers (system editor dialog, detail page sidebar,
|
|
18
|
+
* etc.) just drop in the systemId.
|
|
19
|
+
*/
|
|
20
|
+
export const SystemLinksEditor: React.FC<Props> = ({ systemId }) => {
|
|
21
|
+
const catalogClient = usePluginClient(CatalogApi);
|
|
22
|
+
const accessApi = useApi(accessApiRef);
|
|
23
|
+
const toast = useToast();
|
|
24
|
+
|
|
25
|
+
const { allowed: canManage } = accessApi.useAccess(catalogAccess.system.manage);
|
|
26
|
+
|
|
27
|
+
const { data: links = [], refetch } =
|
|
28
|
+
catalogClient.getSystemLinks.useQuery({ systemId });
|
|
29
|
+
|
|
30
|
+
const addMutation = catalogClient.addSystemLink.useMutation({
|
|
31
|
+
onSuccess: () => {
|
|
32
|
+
void refetch();
|
|
33
|
+
},
|
|
34
|
+
onError: (error) => {
|
|
35
|
+
toast.error(extractErrorMessage(error, "Failed to add link"));
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const removeMutation = catalogClient.removeSystemLink.useMutation({
|
|
40
|
+
onSuccess: () => {
|
|
41
|
+
void refetch();
|
|
42
|
+
},
|
|
43
|
+
onError: (error) => {
|
|
44
|
+
toast.error(extractErrorMessage(error, "Failed to remove link"));
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<LinksEditor
|
|
50
|
+
title="Additional Links"
|
|
51
|
+
description="Jira boards, ticket tools, dashboards, or any URL related to this system."
|
|
52
|
+
links={links}
|
|
53
|
+
canManage={canManage}
|
|
54
|
+
busy={addMutation.isPending || removeMutation.isPending}
|
|
55
|
+
onAdd={async ({ label, url }) => {
|
|
56
|
+
await addMutation.mutateAsync({ systemId, label, url });
|
|
57
|
+
}}
|
|
58
|
+
onRemove={async (link) => {
|
|
59
|
+
await removeMutation.mutateAsync(link.id);
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
};
|
package/src/index.tsx
CHANGED