@checkstack/dashboard-frontend 0.0.2
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 +125 -0
- package/package.json +35 -0
- package/src/Dashboard.tsx +389 -0
- package/src/index.tsx +20 -0
- package/tsconfig.json +6 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# @checkstack/dashboard-frontend
|
|
2
|
+
|
|
3
|
+
## 0.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d20d274: Initial release of all @checkstack packages. Rebranded from Checkmate to Checkstack with new npm organization @checkstack and domain checkstack.dev.
|
|
8
|
+
- Updated dependencies [d20d274]
|
|
9
|
+
- @checkstack/auth-frontend@0.0.2
|
|
10
|
+
- @checkstack/catalog-common@0.0.2
|
|
11
|
+
- @checkstack/catalog-frontend@0.0.2
|
|
12
|
+
- @checkstack/command-common@0.0.2
|
|
13
|
+
- @checkstack/command-frontend@0.0.2
|
|
14
|
+
- @checkstack/common@0.0.2
|
|
15
|
+
- @checkstack/frontend-api@0.0.2
|
|
16
|
+
- @checkstack/healthcheck-common@0.0.2
|
|
17
|
+
- @checkstack/incident-common@0.0.2
|
|
18
|
+
- @checkstack/maintenance-common@0.0.2
|
|
19
|
+
- @checkstack/notification-common@0.0.2
|
|
20
|
+
- @checkstack/signal-frontend@0.0.2
|
|
21
|
+
- @checkstack/ui@0.0.2
|
|
22
|
+
|
|
23
|
+
## 0.1.0
|
|
24
|
+
|
|
25
|
+
### Minor Changes
|
|
26
|
+
|
|
27
|
+
- ae33df2: Move command palette from dashboard to centered navbar position
|
|
28
|
+
|
|
29
|
+
- Converted `command-frontend` into a plugin with `NavbarCenterSlot` extension
|
|
30
|
+
- Added compact `NavbarSearch` component with responsive search trigger
|
|
31
|
+
- Moved `SearchDialog` from dashboard-frontend to command-frontend
|
|
32
|
+
- Keyboard shortcut (⌘K / Ctrl+K) now works on every page
|
|
33
|
+
- Renamed navbar slots for clarity:
|
|
34
|
+
- `NavbarSlot` → `NavbarRightSlot`
|
|
35
|
+
- `NavbarMainSlot` → `NavbarLeftSlot`
|
|
36
|
+
- Added new `NavbarCenterSlot` for centered content
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- a65e002: Add compile-time type safety for Lucide icon names
|
|
41
|
+
|
|
42
|
+
- Add `LucideIconName` type and `lucideIconSchema` Zod schema to `@checkstack/common`
|
|
43
|
+
- Update backend interfaces (`AuthStrategy`, `NotificationStrategy`, `IntegrationProvider`, `CommandDefinition`) to use `LucideIconName`
|
|
44
|
+
- Update RPC contracts to use `lucideIconSchema` for proper type inference across RPC boundaries
|
|
45
|
+
- Simplify `SocialProviderButton` to use `DynamicIcon` directly (removes 30+ lines of pascalCase conversion)
|
|
46
|
+
- Replace static `iconMap` in `SearchDialog` with `DynamicIcon` for dynamic icon rendering
|
|
47
|
+
- Add fallback handling in `DynamicIcon` when icon name isn't found
|
|
48
|
+
- Fix legacy kebab-case icon names to PascalCase: `mail`→`Mail`, `send`→`Send`, `github`→`Github`, `key-round`→`KeyRound`, `network`→`Network`, `AlertCircle`→`CircleAlert`
|
|
49
|
+
|
|
50
|
+
- Updated dependencies [52231ef]
|
|
51
|
+
- Updated dependencies [b0124ef]
|
|
52
|
+
- Updated dependencies [54cc787]
|
|
53
|
+
- Updated dependencies [a65e002]
|
|
54
|
+
- Updated dependencies [ae33df2]
|
|
55
|
+
- Updated dependencies [a65e002]
|
|
56
|
+
- Updated dependencies [32ea706]
|
|
57
|
+
- @checkstack/auth-frontend@0.3.0
|
|
58
|
+
- @checkstack/ui@0.1.2
|
|
59
|
+
- @checkstack/catalog-frontend@0.1.0
|
|
60
|
+
- @checkstack/common@0.2.0
|
|
61
|
+
- @checkstack/command-frontend@0.1.0
|
|
62
|
+
- @checkstack/frontend-api@0.1.0
|
|
63
|
+
- @checkstack/catalog-common@0.1.2
|
|
64
|
+
- @checkstack/command-common@0.0.3
|
|
65
|
+
- @checkstack/healthcheck-common@0.1.1
|
|
66
|
+
- @checkstack/incident-common@0.1.2
|
|
67
|
+
- @checkstack/maintenance-common@0.1.2
|
|
68
|
+
- @checkstack/notification-common@0.1.1
|
|
69
|
+
- @checkstack/signal-frontend@0.1.1
|
|
70
|
+
|
|
71
|
+
## 0.0.5
|
|
72
|
+
|
|
73
|
+
### Patch Changes
|
|
74
|
+
|
|
75
|
+
- Updated dependencies [1bf71bb]
|
|
76
|
+
- @checkstack/auth-frontend@0.2.1
|
|
77
|
+
- @checkstack/catalog-frontend@0.0.5
|
|
78
|
+
|
|
79
|
+
## 0.0.4
|
|
80
|
+
|
|
81
|
+
### Patch Changes
|
|
82
|
+
|
|
83
|
+
- Updated dependencies [e26c08e]
|
|
84
|
+
- @checkstack/auth-frontend@0.2.0
|
|
85
|
+
- @checkstack/catalog-frontend@0.0.4
|
|
86
|
+
|
|
87
|
+
## 0.0.3
|
|
88
|
+
|
|
89
|
+
### Patch Changes
|
|
90
|
+
|
|
91
|
+
- Updated dependencies [0f8cc7d]
|
|
92
|
+
- @checkstack/frontend-api@0.0.3
|
|
93
|
+
- @checkstack/auth-frontend@0.1.1
|
|
94
|
+
- @checkstack/catalog-common@0.1.1
|
|
95
|
+
- @checkstack/catalog-frontend@0.0.3
|
|
96
|
+
- @checkstack/command-frontend@0.0.3
|
|
97
|
+
- @checkstack/incident-common@0.1.1
|
|
98
|
+
- @checkstack/maintenance-common@0.1.1
|
|
99
|
+
- @checkstack/ui@0.1.1
|
|
100
|
+
|
|
101
|
+
## 0.0.2
|
|
102
|
+
|
|
103
|
+
### Patch Changes
|
|
104
|
+
|
|
105
|
+
- Updated dependencies [eff5b4e]
|
|
106
|
+
- Updated dependencies [ffc28f6]
|
|
107
|
+
- Updated dependencies [4dd644d]
|
|
108
|
+
- Updated dependencies [ae19ff6]
|
|
109
|
+
- Updated dependencies [0babb9c]
|
|
110
|
+
- Updated dependencies [32f2535]
|
|
111
|
+
- Updated dependencies [b55fae6]
|
|
112
|
+
- Updated dependencies [b354ab3]
|
|
113
|
+
- @checkstack/maintenance-common@0.1.0
|
|
114
|
+
- @checkstack/ui@0.1.0
|
|
115
|
+
- @checkstack/common@0.1.0
|
|
116
|
+
- @checkstack/catalog-common@0.1.0
|
|
117
|
+
- @checkstack/notification-common@0.1.0
|
|
118
|
+
- @checkstack/incident-common@0.1.0
|
|
119
|
+
- @checkstack/healthcheck-common@0.1.0
|
|
120
|
+
- @checkstack/auth-frontend@0.1.0
|
|
121
|
+
- @checkstack/signal-frontend@0.1.0
|
|
122
|
+
- @checkstack/catalog-frontend@0.0.2
|
|
123
|
+
- @checkstack/command-common@0.0.2
|
|
124
|
+
- @checkstack/command-frontend@0.0.2
|
|
125
|
+
- @checkstack/frontend-api@0.0.2
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/dashboard-frontend",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.tsx",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"typecheck": "tsc --noEmit",
|
|
8
|
+
"lint": "bun run lint:code",
|
|
9
|
+
"lint:code": "eslint . --max-warnings 0"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@checkstack/frontend-api": "workspace:*",
|
|
13
|
+
"@checkstack/auth-frontend": "workspace:*",
|
|
14
|
+
"@checkstack/common": "workspace:*",
|
|
15
|
+
"@checkstack/command-frontend": "workspace:*",
|
|
16
|
+
"@checkstack/command-common": "workspace:*",
|
|
17
|
+
"@checkstack/notification-common": "workspace:*",
|
|
18
|
+
"@checkstack/catalog-common": "workspace:*",
|
|
19
|
+
"@checkstack/incident-common": "workspace:*",
|
|
20
|
+
"@checkstack/maintenance-common": "workspace:*",
|
|
21
|
+
"@checkstack/healthcheck-common": "workspace:*",
|
|
22
|
+
"@checkstack/signal-frontend": "workspace:*",
|
|
23
|
+
"@checkstack/ui": "workspace:*",
|
|
24
|
+
"@checkstack/catalog-frontend": "workspace:*",
|
|
25
|
+
"react": "^18.2.0",
|
|
26
|
+
"react-router-dom": "^6.22.0",
|
|
27
|
+
"lucide-react": "^0.344.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "^5.0.0",
|
|
31
|
+
"@types/react": "^18.2.0",
|
|
32
|
+
"@checkstack/tsconfig": "workspace:*",
|
|
33
|
+
"@checkstack/scripts": "workspace:*"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import React, { useEffect, useState, useCallback } from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
3
|
+
import {
|
|
4
|
+
useApi,
|
|
5
|
+
rpcApiRef,
|
|
6
|
+
ExtensionSlot,
|
|
7
|
+
} from "@checkstack/frontend-api";
|
|
8
|
+
import { catalogApiRef } from "@checkstack/catalog-frontend";
|
|
9
|
+
import {
|
|
10
|
+
catalogRoutes,
|
|
11
|
+
SystemStateBadgesSlot,
|
|
12
|
+
System,
|
|
13
|
+
Group,
|
|
14
|
+
} from "@checkstack/catalog-common";
|
|
15
|
+
import { resolveRoute } from "@checkstack/common";
|
|
16
|
+
import {
|
|
17
|
+
NotificationApi,
|
|
18
|
+
type EnrichedSubscription,
|
|
19
|
+
} from "@checkstack/notification-common";
|
|
20
|
+
import { IncidentApi } from "@checkstack/incident-common";
|
|
21
|
+
import { MaintenanceApi } from "@checkstack/maintenance-common";
|
|
22
|
+
import { HEALTH_CHECK_RUN_COMPLETED } from "@checkstack/healthcheck-common";
|
|
23
|
+
import { useSignal } from "@checkstack/signal-frontend";
|
|
24
|
+
import {
|
|
25
|
+
Card,
|
|
26
|
+
CardHeader,
|
|
27
|
+
CardTitle,
|
|
28
|
+
CardContent,
|
|
29
|
+
SectionHeader,
|
|
30
|
+
StatusCard,
|
|
31
|
+
EmptyState,
|
|
32
|
+
LoadingSpinner,
|
|
33
|
+
SubscribeButton,
|
|
34
|
+
useToast,
|
|
35
|
+
AnimatedCounter,
|
|
36
|
+
TerminalFeed,
|
|
37
|
+
type TerminalEntry,
|
|
38
|
+
} from "@checkstack/ui";
|
|
39
|
+
import {
|
|
40
|
+
LayoutGrid,
|
|
41
|
+
Server,
|
|
42
|
+
Activity,
|
|
43
|
+
ChevronRight,
|
|
44
|
+
AlertTriangle,
|
|
45
|
+
Wrench,
|
|
46
|
+
Terminal,
|
|
47
|
+
} from "lucide-react";
|
|
48
|
+
import { authApiRef } from "@checkstack/auth-frontend/api";
|
|
49
|
+
|
|
50
|
+
const CATALOG_PLUGIN_ID = "catalog";
|
|
51
|
+
const MAX_TERMINAL_ENTRIES = 8;
|
|
52
|
+
|
|
53
|
+
const getGroupId = (groupId: string) => `${CATALOG_PLUGIN_ID}.group.${groupId}`;
|
|
54
|
+
|
|
55
|
+
interface GroupWithSystems extends Group {
|
|
56
|
+
systems: System[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Map health check status to terminal entry variant
|
|
60
|
+
const statusToVariant = (
|
|
61
|
+
status: "healthy" | "degraded" | "unhealthy"
|
|
62
|
+
): TerminalEntry["variant"] => {
|
|
63
|
+
switch (status) {
|
|
64
|
+
case "healthy": {
|
|
65
|
+
return "success";
|
|
66
|
+
}
|
|
67
|
+
case "degraded": {
|
|
68
|
+
return "warning";
|
|
69
|
+
}
|
|
70
|
+
case "unhealthy": {
|
|
71
|
+
return "error";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const Dashboard: React.FC = () => {
|
|
77
|
+
const catalogApi = useApi(catalogApiRef);
|
|
78
|
+
const rpcApi = useApi(rpcApiRef);
|
|
79
|
+
const notificationApi = rpcApi.forPlugin(NotificationApi);
|
|
80
|
+
const incidentApi = rpcApi.forPlugin(IncidentApi);
|
|
81
|
+
const maintenanceApi = rpcApi.forPlugin(MaintenanceApi);
|
|
82
|
+
const navigate = useNavigate();
|
|
83
|
+
const toast = useToast();
|
|
84
|
+
const authApi = useApi(authApiRef);
|
|
85
|
+
const { data: session } = authApi.useSession();
|
|
86
|
+
|
|
87
|
+
const [groupsWithSystems, setGroupsWithSystems] = useState<
|
|
88
|
+
GroupWithSystems[]
|
|
89
|
+
>([]);
|
|
90
|
+
const [loading, setLoading] = useState(true);
|
|
91
|
+
|
|
92
|
+
// Overview statistics state
|
|
93
|
+
const [systemsCount, setSystemsCount] = useState(0);
|
|
94
|
+
const [activeIncidentsCount, setActiveIncidentsCount] = useState(0);
|
|
95
|
+
const [activeMaintenancesCount, setActiveMaintenancesCount] = useState(0);
|
|
96
|
+
|
|
97
|
+
// Terminal feed entries from real healthcheck signals
|
|
98
|
+
const [terminalEntries, setTerminalEntries] = useState<TerminalEntry[]>([]);
|
|
99
|
+
|
|
100
|
+
// Subscription state
|
|
101
|
+
const [subscriptions, setSubscriptions] = useState<EnrichedSubscription[]>(
|
|
102
|
+
[]
|
|
103
|
+
);
|
|
104
|
+
const [subscriptionLoading, setSubscriptionLoading] = useState<
|
|
105
|
+
Record<string, boolean>
|
|
106
|
+
>({});
|
|
107
|
+
|
|
108
|
+
// Listen for health check runs and add to terminal feed
|
|
109
|
+
useSignal(
|
|
110
|
+
HEALTH_CHECK_RUN_COMPLETED,
|
|
111
|
+
({ systemName, configurationName, status, latencyMs }) => {
|
|
112
|
+
const newEntry: TerminalEntry = {
|
|
113
|
+
id: `${configurationName}-${Date.now()}`,
|
|
114
|
+
timestamp: new Date(),
|
|
115
|
+
content: `${systemName} (${configurationName}) → ${status}`,
|
|
116
|
+
variant: statusToVariant(status),
|
|
117
|
+
suffix: latencyMs === undefined ? undefined : `${latencyMs}ms`,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
setTerminalEntries((prev) =>
|
|
121
|
+
[newEntry, ...prev].slice(0, MAX_TERMINAL_ENTRIES)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (session) {
|
|
128
|
+
notificationApi.getSubscriptions().then(setSubscriptions);
|
|
129
|
+
}
|
|
130
|
+
}, [session, notificationApi]);
|
|
131
|
+
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
Promise.all([
|
|
134
|
+
catalogApi.getGroups(),
|
|
135
|
+
catalogApi.getSystems(),
|
|
136
|
+
incidentApi.listIncidents({ includeResolved: false }),
|
|
137
|
+
maintenanceApi.listMaintenances({ status: "in_progress" }),
|
|
138
|
+
])
|
|
139
|
+
.then(([groups, systems, incidents, maintenances]) => {
|
|
140
|
+
// Set overview statistics
|
|
141
|
+
setSystemsCount(systems.length);
|
|
142
|
+
setActiveIncidentsCount(incidents.length);
|
|
143
|
+
setActiveMaintenancesCount(maintenances.length);
|
|
144
|
+
|
|
145
|
+
// Create a map of system IDs to systems
|
|
146
|
+
const systemMap = new Map(systems.map((s) => [s.id, s]));
|
|
147
|
+
|
|
148
|
+
// Map groups to include their systems
|
|
149
|
+
const groupsData: GroupWithSystems[] = groups.map((group) => {
|
|
150
|
+
const groupSystems = (group.systemIds || [])
|
|
151
|
+
.map((id) => systemMap.get(id))
|
|
152
|
+
.filter((s): s is System => s !== undefined);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
...group,
|
|
156
|
+
systems: groupSystems,
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
setGroupsWithSystems(groupsData);
|
|
161
|
+
})
|
|
162
|
+
.catch(console.error)
|
|
163
|
+
.finally(() => setLoading(false));
|
|
164
|
+
}, [catalogApi, incidentApi, maintenanceApi]);
|
|
165
|
+
|
|
166
|
+
const handleSystemClick = (systemId: string) => {
|
|
167
|
+
navigate(resolveRoute(catalogRoutes.routes.systemDetail, { systemId }));
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const isSubscribed = (groupId: string) => {
|
|
171
|
+
const fullId = getGroupId(groupId);
|
|
172
|
+
return subscriptions.some((s) => s.groupId === fullId);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const handleSubscribe = useCallback(
|
|
176
|
+
async (groupId: string) => {
|
|
177
|
+
const fullId = getGroupId(groupId);
|
|
178
|
+
setSubscriptionLoading((prev) => ({ ...prev, [groupId]: true }));
|
|
179
|
+
try {
|
|
180
|
+
await notificationApi.subscribe({ groupId: fullId });
|
|
181
|
+
setSubscriptions((prev) => [
|
|
182
|
+
...prev,
|
|
183
|
+
{
|
|
184
|
+
groupId: fullId,
|
|
185
|
+
groupName: "",
|
|
186
|
+
groupDescription: "",
|
|
187
|
+
ownerPlugin: CATALOG_PLUGIN_ID,
|
|
188
|
+
subscribedAt: new Date(),
|
|
189
|
+
},
|
|
190
|
+
]);
|
|
191
|
+
toast.success("Subscribed to group notifications");
|
|
192
|
+
} catch (error) {
|
|
193
|
+
const message =
|
|
194
|
+
error instanceof Error ? error.message : "Failed to subscribe";
|
|
195
|
+
toast.error(message);
|
|
196
|
+
} finally {
|
|
197
|
+
setSubscriptionLoading((prev) => ({ ...prev, [groupId]: false }));
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
[notificationApi, toast]
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const handleUnsubscribe = useCallback(
|
|
204
|
+
async (groupId: string) => {
|
|
205
|
+
const fullId = getGroupId(groupId);
|
|
206
|
+
setSubscriptionLoading((prev) => ({ ...prev, [groupId]: true }));
|
|
207
|
+
try {
|
|
208
|
+
await notificationApi.unsubscribe({ groupId: fullId });
|
|
209
|
+
setSubscriptions((prev) => prev.filter((s) => s.groupId !== fullId));
|
|
210
|
+
toast.success("Unsubscribed from group notifications");
|
|
211
|
+
} catch (error) {
|
|
212
|
+
const message =
|
|
213
|
+
error instanceof Error ? error.message : "Failed to unsubscribe";
|
|
214
|
+
toast.error(message);
|
|
215
|
+
} finally {
|
|
216
|
+
setSubscriptionLoading((prev) => ({ ...prev, [groupId]: false }));
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
[notificationApi, toast]
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const renderGroupsContent = () => {
|
|
223
|
+
if (loading) {
|
|
224
|
+
return <LoadingSpinner />;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (groupsWithSystems.length === 0) {
|
|
228
|
+
return (
|
|
229
|
+
<EmptyState
|
|
230
|
+
title="No system groups found"
|
|
231
|
+
description="Visit the Catalog to create your first group."
|
|
232
|
+
icon={<Server className="w-12 h-12" />}
|
|
233
|
+
/>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div className="space-y-4">
|
|
239
|
+
{groupsWithSystems.map((group) => (
|
|
240
|
+
<Card
|
|
241
|
+
key={group.id}
|
|
242
|
+
className="border-border shadow-sm hover:shadow-md transition-shadow"
|
|
243
|
+
>
|
|
244
|
+
<CardHeader className="border-b border-border bg-muted/30">
|
|
245
|
+
<div className="flex items-center gap-2">
|
|
246
|
+
<LayoutGrid className="h-5 w-5 text-muted-foreground" />
|
|
247
|
+
<CardTitle className="text-lg font-semibold text-foreground">
|
|
248
|
+
{group.name}
|
|
249
|
+
</CardTitle>
|
|
250
|
+
<span className="ml-auto text-sm text-muted-foreground mr-2">
|
|
251
|
+
{group.systems.length}{" "}
|
|
252
|
+
{group.systems.length === 1 ? "system" : "systems"}
|
|
253
|
+
</span>
|
|
254
|
+
{session && (
|
|
255
|
+
<SubscribeButton
|
|
256
|
+
isSubscribed={isSubscribed(group.id)}
|
|
257
|
+
onSubscribe={() => handleSubscribe(group.id)}
|
|
258
|
+
onUnsubscribe={() => handleUnsubscribe(group.id)}
|
|
259
|
+
loading={subscriptionLoading[group.id] || false}
|
|
260
|
+
/>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
</CardHeader>
|
|
264
|
+
<CardContent className="p-4">
|
|
265
|
+
{group.systems.length === 0 ? (
|
|
266
|
+
<div className="py-8 text-center">
|
|
267
|
+
<p className="text-sm text-muted-foreground">
|
|
268
|
+
No systems in this group yet
|
|
269
|
+
</p>
|
|
270
|
+
</div>
|
|
271
|
+
) : (
|
|
272
|
+
<div
|
|
273
|
+
className={`grid gap-3 ${
|
|
274
|
+
group.systems.length === 1
|
|
275
|
+
? "grid-cols-1"
|
|
276
|
+
: "grid-cols-1 sm:grid-cols-2"
|
|
277
|
+
}`}
|
|
278
|
+
>
|
|
279
|
+
{group.systems.map((system) => (
|
|
280
|
+
<button
|
|
281
|
+
key={system.id}
|
|
282
|
+
onClick={() => handleSystemClick(system.id)}
|
|
283
|
+
className="flex items-center justify-between gap-3 rounded-lg border border-border bg-card px-4 py-3 transition-all cursor-pointer hover:border-border/80 hover:shadow-sm text-left"
|
|
284
|
+
>
|
|
285
|
+
<div className="flex items-center gap-3 min-w-0 flex-1">
|
|
286
|
+
<Activity className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
|
287
|
+
<p className="text-sm font-medium text-foreground truncate">
|
|
288
|
+
{system.name}
|
|
289
|
+
</p>
|
|
290
|
+
</div>
|
|
291
|
+
<ExtensionSlot
|
|
292
|
+
slot={SystemStateBadgesSlot}
|
|
293
|
+
context={{ system }}
|
|
294
|
+
/>
|
|
295
|
+
<ChevronRight className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
|
296
|
+
</button>
|
|
297
|
+
))}
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
</CardContent>
|
|
301
|
+
</Card>
|
|
302
|
+
))}
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<>
|
|
309
|
+
<div className="space-y-8 animate-in fade-in duration-500">
|
|
310
|
+
{/* Overview Section */}
|
|
311
|
+
<section>
|
|
312
|
+
<SectionHeader
|
|
313
|
+
title="Overview"
|
|
314
|
+
icon={<Activity className="w-5 h-5" />}
|
|
315
|
+
/>
|
|
316
|
+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
317
|
+
<StatusCard
|
|
318
|
+
title="Total Systems"
|
|
319
|
+
value={loading ? "..." : <AnimatedCounter value={systemsCount} />}
|
|
320
|
+
description="Monitored systems in your catalog"
|
|
321
|
+
icon={<Server className="w-4 h-4" />}
|
|
322
|
+
/>
|
|
323
|
+
|
|
324
|
+
<StatusCard
|
|
325
|
+
variant={activeIncidentsCount > 0 ? "gradient" : "default"}
|
|
326
|
+
title="Active Incidents"
|
|
327
|
+
value={
|
|
328
|
+
loading ? (
|
|
329
|
+
"..."
|
|
330
|
+
) : (
|
|
331
|
+
<AnimatedCounter value={activeIncidentsCount} />
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
description={
|
|
335
|
+
activeIncidentsCount === 0
|
|
336
|
+
? "All systems operating normally"
|
|
337
|
+
: "Unresolved issues requiring attention"
|
|
338
|
+
}
|
|
339
|
+
icon={<AlertTriangle className="w-4 h-4" />}
|
|
340
|
+
/>
|
|
341
|
+
|
|
342
|
+
<StatusCard
|
|
343
|
+
title="Active Maintenances"
|
|
344
|
+
value={
|
|
345
|
+
loading ? (
|
|
346
|
+
"..."
|
|
347
|
+
) : (
|
|
348
|
+
<AnimatedCounter value={activeMaintenancesCount} />
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
description={
|
|
352
|
+
activeMaintenancesCount === 0
|
|
353
|
+
? "No scheduled maintenance"
|
|
354
|
+
: "Ongoing or scheduled maintenance windows"
|
|
355
|
+
}
|
|
356
|
+
icon={<Wrench className="w-4 h-4" />}
|
|
357
|
+
/>
|
|
358
|
+
</div>
|
|
359
|
+
</section>
|
|
360
|
+
|
|
361
|
+
{/* Terminal Feed and System Groups - Two Column Layout */}
|
|
362
|
+
<div className="grid gap-8 lg:grid-cols-3">
|
|
363
|
+
{/* Terminal Feed */}
|
|
364
|
+
<section className="lg:col-span-1">
|
|
365
|
+
<SectionHeader
|
|
366
|
+
title="Recent Activity"
|
|
367
|
+
icon={<Terminal className="w-5 h-5" />}
|
|
368
|
+
/>
|
|
369
|
+
<TerminalFeed
|
|
370
|
+
entries={terminalEntries}
|
|
371
|
+
maxEntries={MAX_TERMINAL_ENTRIES}
|
|
372
|
+
maxHeight="350px"
|
|
373
|
+
title="checkstack status --watch"
|
|
374
|
+
/>
|
|
375
|
+
</section>
|
|
376
|
+
|
|
377
|
+
{/* System Groups */}
|
|
378
|
+
<section className="lg:col-span-2">
|
|
379
|
+
<SectionHeader
|
|
380
|
+
title="System Groups"
|
|
381
|
+
icon={<LayoutGrid className="w-5 h-5" />}
|
|
382
|
+
/>
|
|
383
|
+
{renderGroupsContent()}
|
|
384
|
+
</section>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
</>
|
|
388
|
+
);
|
|
389
|
+
};
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { FrontendPlugin, DashboardSlot } from "@checkstack/frontend-api";
|
|
2
|
+
import { definePluginMetadata } from "@checkstack/common";
|
|
3
|
+
import { Dashboard } from "./Dashboard";
|
|
4
|
+
|
|
5
|
+
const pluginMetadata = definePluginMetadata({
|
|
6
|
+
pluginId: "dashboard",
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const dashboardPlugin: FrontendPlugin = {
|
|
10
|
+
metadata: pluginMetadata,
|
|
11
|
+
extensions: [
|
|
12
|
+
{
|
|
13
|
+
id: "dashboard-main",
|
|
14
|
+
slot: DashboardSlot,
|
|
15
|
+
component: Dashboard as React.ComponentType<unknown>,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default dashboardPlugin;
|