@checkstack/satellite-frontend 0.2.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 +40 -0
- package/package.json +30 -0
- package/src/components/CreateSatelliteDialog.tsx +219 -0
- package/src/components/SatelliteMenuItems.tsx +30 -0
- package/src/components/SatelliteStatusBadge.tsx +18 -0
- package/src/index.tsx +31 -0
- package/src/pages/SatelliteListPage.tsx +183 -0
- package/tsconfig.json +6 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @checkstack/satellite-frontend
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 26d8bae: Distributed satellite health checks and Assignment IDE page
|
|
8
|
+
|
|
9
|
+
**Satellite System**
|
|
10
|
+
|
|
11
|
+
- New `satellite-backend`, `satellite-common`, `satellite-frontend`, and `satellite` agent packages for distributed health check execution
|
|
12
|
+
- WebSocket-based satellite connectivity with authentication, heartbeats, and live configuration push
|
|
13
|
+
- Satellite management UI with create dialog, status badges, and list page
|
|
14
|
+
|
|
15
|
+
**Live Configuration Updates**
|
|
16
|
+
|
|
17
|
+
- Added `assignmentChanged` hook to `healthcheck-backend` for cross-plugin communication
|
|
18
|
+
- `satellite-backend` subscribes to assignment changes and pushes config updates to connected satellites in real-time
|
|
19
|
+
|
|
20
|
+
**Assignment IDE Page**
|
|
21
|
+
|
|
22
|
+
- Replaced the 1028-line modal-based `SystemHealthCheckAssignment` component with a full-page IDE layout
|
|
23
|
+
- New modular components: `AssignmentTree`, `GeneralPanel`, `ThresholdsPanel`, `RetentionPanel`, `ExecutionPanel`
|
|
24
|
+
- Added unassign capability and sorted assignment lists for stable ordering
|
|
25
|
+
|
|
26
|
+
**Shared IDE Primitives**
|
|
27
|
+
|
|
28
|
+
- Extracted `IDETreeNode`, `IDETreeSection`, `IDEStatusBar`, `IDELayout` to `@checkstack/ui` for cross-plugin reuse
|
|
29
|
+
- Migrated existing health check IDE editor to use shared primitives
|
|
30
|
+
|
|
31
|
+
**Infrastructure**
|
|
32
|
+
|
|
33
|
+
- Added `Dockerfile.satellite` for containerized satellite deployment
|
|
34
|
+
- WebSocket route registry in `@checkstack/backend` and `@checkstack/backend-api`
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- Updated dependencies [26d8bae]
|
|
39
|
+
- @checkstack/ui@1.3.0
|
|
40
|
+
- @checkstack/satellite-common@0.2.0
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/satellite-frontend",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.tsx",
|
|
6
|
+
"checkstack": {
|
|
7
|
+
"type": "frontend"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"lint": "bun run lint:code",
|
|
12
|
+
"lint:code": "eslint . --max-warnings 0"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@checkstack/common": "0.6.5",
|
|
16
|
+
"@checkstack/frontend-api": "0.3.9",
|
|
17
|
+
"@checkstack/satellite-common": "0.1.0",
|
|
18
|
+
"@checkstack/signal-frontend": "0.0.15",
|
|
19
|
+
"@checkstack/ui": "1.2.1",
|
|
20
|
+
"lucide-react": "^0.344.0",
|
|
21
|
+
"react": "^18.2.0",
|
|
22
|
+
"react-router-dom": "^6.20.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@checkstack/tsconfig": "0.0.5",
|
|
26
|
+
"@checkstack/scripts": "0.1.2",
|
|
27
|
+
"@types/react": "^18.2.0",
|
|
28
|
+
"typescript": "^5.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { usePluginClient } from "@checkstack/frontend-api";
|
|
3
|
+
import { SatelliteApi } from "@checkstack/satellite-common";
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogDescription,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
Button,
|
|
12
|
+
Input,
|
|
13
|
+
Label,
|
|
14
|
+
useToast,
|
|
15
|
+
} from "@checkstack/ui";
|
|
16
|
+
import { Copy, AlertTriangle } from "lucide-react";
|
|
17
|
+
import { extractErrorMessage } from "@checkstack/common";
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
open: boolean;
|
|
21
|
+
onOpenChange: (open: boolean) => void;
|
|
22
|
+
onCreated: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface CreatedCredentials {
|
|
26
|
+
clientId: string;
|
|
27
|
+
token: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const CreateSatelliteDialog: React.FC<Props> = ({
|
|
31
|
+
open,
|
|
32
|
+
onOpenChange,
|
|
33
|
+
onCreated,
|
|
34
|
+
}) => {
|
|
35
|
+
const satelliteClient = usePluginClient(SatelliteApi);
|
|
36
|
+
const toast = useToast();
|
|
37
|
+
|
|
38
|
+
const [name, setName] = useState("");
|
|
39
|
+
const [region, setRegion] = useState("");
|
|
40
|
+
const [credentials, setCredentials] = useState<
|
|
41
|
+
CreatedCredentials | undefined
|
|
42
|
+
>();
|
|
43
|
+
|
|
44
|
+
const createMutation = satelliteClient.createSatellite.useMutation({
|
|
45
|
+
onSuccess: (data) => {
|
|
46
|
+
setCredentials({
|
|
47
|
+
clientId: data.satellite.id,
|
|
48
|
+
token: data.token,
|
|
49
|
+
});
|
|
50
|
+
toast.success("Satellite created successfully");
|
|
51
|
+
onCreated();
|
|
52
|
+
},
|
|
53
|
+
onError: (error) => {
|
|
54
|
+
toast.error(extractErrorMessage(error, "Failed to create satellite"));
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const handleSubmit = () => {
|
|
59
|
+
if (!name.trim()) {
|
|
60
|
+
toast.error("Name is required");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!region.trim()) {
|
|
64
|
+
toast.error("Region is required");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
createMutation.mutate({
|
|
69
|
+
name: name.trim(),
|
|
70
|
+
region: region.trim(),
|
|
71
|
+
tags: {},
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleClose = () => {
|
|
76
|
+
setName("");
|
|
77
|
+
setRegion("");
|
|
78
|
+
setCredentials(undefined);
|
|
79
|
+
onOpenChange(false);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const copyToClipboard = async (text: string, label: string) => {
|
|
83
|
+
try {
|
|
84
|
+
await navigator.clipboard.writeText(text);
|
|
85
|
+
toast.success(`${label} copied to clipboard`);
|
|
86
|
+
} catch {
|
|
87
|
+
toast.error("Failed to copy to clipboard");
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// After creation, show credentials
|
|
92
|
+
if (credentials) {
|
|
93
|
+
return (
|
|
94
|
+
<Dialog open={open} onOpenChange={handleClose}>
|
|
95
|
+
<DialogContent>
|
|
96
|
+
<DialogHeader>
|
|
97
|
+
<DialogTitle>Satellite Created</DialogTitle>
|
|
98
|
+
<DialogDescription>
|
|
99
|
+
Save these credentials securely. The token will not be shown again.
|
|
100
|
+
</DialogDescription>
|
|
101
|
+
</DialogHeader>
|
|
102
|
+
|
|
103
|
+
<div className="grid gap-4 py-4">
|
|
104
|
+
<div className="rounded-md border border-warning/50 bg-warning/10 p-3 flex items-start gap-2">
|
|
105
|
+
<AlertTriangle className="h-4 w-4 text-warning mt-0.5 shrink-0" />
|
|
106
|
+
<p className="text-sm text-warning">
|
|
107
|
+
Copy and save both values now. The token cannot be retrieved
|
|
108
|
+
after closing this dialog.
|
|
109
|
+
</p>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div className="grid gap-2">
|
|
113
|
+
<Label htmlFor="client-id">Client ID</Label>
|
|
114
|
+
<div className="flex gap-2">
|
|
115
|
+
<Input
|
|
116
|
+
id="client-id"
|
|
117
|
+
value={credentials.clientId}
|
|
118
|
+
readOnly
|
|
119
|
+
className="font-mono text-sm"
|
|
120
|
+
/>
|
|
121
|
+
<Button
|
|
122
|
+
variant="outline"
|
|
123
|
+
size="icon"
|
|
124
|
+
onClick={() =>
|
|
125
|
+
void copyToClipboard(credentials.clientId, "Client ID")
|
|
126
|
+
}
|
|
127
|
+
>
|
|
128
|
+
<Copy className="h-4 w-4" />
|
|
129
|
+
</Button>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div className="grid gap-2">
|
|
134
|
+
<Label htmlFor="token">Token</Label>
|
|
135
|
+
<div className="flex gap-2">
|
|
136
|
+
<Input
|
|
137
|
+
id="token"
|
|
138
|
+
value={credentials.token}
|
|
139
|
+
readOnly
|
|
140
|
+
className="font-mono text-sm"
|
|
141
|
+
/>
|
|
142
|
+
<Button
|
|
143
|
+
variant="outline"
|
|
144
|
+
size="icon"
|
|
145
|
+
onClick={() =>
|
|
146
|
+
void copyToClipboard(credentials.token, "Token")
|
|
147
|
+
}
|
|
148
|
+
>
|
|
149
|
+
<Copy className="h-4 w-4" />
|
|
150
|
+
</Button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div className="grid gap-2 mt-2">
|
|
155
|
+
<Label>Environment Variables</Label>
|
|
156
|
+
<pre className="rounded-md bg-muted p-3 text-xs font-mono overflow-x-auto">
|
|
157
|
+
{`CHECKSTACK_CORE_URL=<your-core-url>\nCHECKSTACK_SATELLITE_CLIENT_ID=${credentials.clientId}\nCHECKSTACK_SATELLITE_TOKEN=${credentials.token}`}
|
|
158
|
+
</pre>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<DialogFooter>
|
|
163
|
+
<Button onClick={handleClose}>Done</Button>
|
|
164
|
+
</DialogFooter>
|
|
165
|
+
</DialogContent>
|
|
166
|
+
</Dialog>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Creation form
|
|
171
|
+
return (
|
|
172
|
+
<Dialog open={open} onOpenChange={handleClose}>
|
|
173
|
+
<DialogContent>
|
|
174
|
+
<DialogHeader>
|
|
175
|
+
<DialogTitle>Create Satellite</DialogTitle>
|
|
176
|
+
<DialogDescription>
|
|
177
|
+
Deploy a satellite node to run health checks from a remote location.
|
|
178
|
+
</DialogDescription>
|
|
179
|
+
</DialogHeader>
|
|
180
|
+
|
|
181
|
+
<div className="grid gap-4 py-4">
|
|
182
|
+
<div className="grid gap-2">
|
|
183
|
+
<Label htmlFor="sat-name">Name</Label>
|
|
184
|
+
<Input
|
|
185
|
+
id="sat-name"
|
|
186
|
+
value={name}
|
|
187
|
+
onChange={(e) => setName(e.target.value)}
|
|
188
|
+
placeholder="EU West Production"
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div className="grid gap-2">
|
|
193
|
+
<Label htmlFor="sat-region">Region</Label>
|
|
194
|
+
<Input
|
|
195
|
+
id="sat-region"
|
|
196
|
+
value={region}
|
|
197
|
+
onChange={(e) => setRegion(e.target.value)}
|
|
198
|
+
placeholder="eu-west-1"
|
|
199
|
+
/>
|
|
200
|
+
<p className="text-xs text-muted-foreground">
|
|
201
|
+
A descriptive identifier for the geographic location, e.g.
|
|
202
|
+
"eu-west-1", "us-east-2", or
|
|
203
|
+
"datacenter-fra".
|
|
204
|
+
</p>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<DialogFooter>
|
|
209
|
+
<Button variant="outline" onClick={handleClose}>
|
|
210
|
+
Cancel
|
|
211
|
+
</Button>
|
|
212
|
+
<Button onClick={handleSubmit} disabled={createMutation.isPending}>
|
|
213
|
+
{createMutation.isPending ? "Creating..." : "Create Satellite"}
|
|
214
|
+
</Button>
|
|
215
|
+
</DialogFooter>
|
|
216
|
+
</DialogContent>
|
|
217
|
+
</Dialog>
|
|
218
|
+
);
|
|
219
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import { Satellite } from "lucide-react";
|
|
4
|
+
import type { UserMenuItemsContext } from "@checkstack/frontend-api";
|
|
5
|
+
import { DropdownMenuItem } from "@checkstack/ui";
|
|
6
|
+
import { resolveRoute } from "@checkstack/common";
|
|
7
|
+
import {
|
|
8
|
+
satelliteRoutes,
|
|
9
|
+
satelliteAccess,
|
|
10
|
+
pluginMetadata,
|
|
11
|
+
} from "@checkstack/satellite-common";
|
|
12
|
+
|
|
13
|
+
export const SatelliteMenuItems = ({
|
|
14
|
+
accessRules: userPerms,
|
|
15
|
+
}: UserMenuItemsContext) => {
|
|
16
|
+
const qualifiedId = `${pluginMetadata.pluginId}.${satelliteAccess.satellite.read.id}`;
|
|
17
|
+
const canRead = userPerms.includes("*") || userPerms.includes(qualifiedId);
|
|
18
|
+
|
|
19
|
+
if (!canRead) {
|
|
20
|
+
return <React.Fragment />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Link to={resolveRoute(satelliteRoutes.routes.list)}>
|
|
25
|
+
<DropdownMenuItem icon={<Satellite className="w-4 h-4" />}>
|
|
26
|
+
Satellites
|
|
27
|
+
</DropdownMenuItem>
|
|
28
|
+
</Link>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Badge } from "@checkstack/ui";
|
|
3
|
+
import type { SatelliteStatus } from "@checkstack/satellite-common";
|
|
4
|
+
|
|
5
|
+
const STATUS_CONFIG: Record<
|
|
6
|
+
SatelliteStatus,
|
|
7
|
+
{ label: string; variant: "success" | "destructive" }
|
|
8
|
+
> = {
|
|
9
|
+
online: { label: "Online", variant: "success" },
|
|
10
|
+
offline: { label: "Offline", variant: "destructive" },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const SatelliteStatusBadge: React.FC<{
|
|
14
|
+
status: SatelliteStatus;
|
|
15
|
+
}> = ({ status }) => {
|
|
16
|
+
const config = STATUS_CONFIG[status];
|
|
17
|
+
return <Badge variant={config.variant}>{config.label}</Badge>;
|
|
18
|
+
};
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFrontendPlugin,
|
|
3
|
+
createSlotExtension,
|
|
4
|
+
UserMenuItemsSlot,
|
|
5
|
+
} from "@checkstack/frontend-api";
|
|
6
|
+
import {
|
|
7
|
+
satelliteRoutes,
|
|
8
|
+
pluginMetadata,
|
|
9
|
+
satelliteAccess,
|
|
10
|
+
} from "@checkstack/satellite-common";
|
|
11
|
+
import { SatelliteListPage } from "./pages/SatelliteListPage";
|
|
12
|
+
import { SatelliteMenuItems } from "./components/SatelliteMenuItems";
|
|
13
|
+
|
|
14
|
+
export default createFrontendPlugin({
|
|
15
|
+
metadata: pluginMetadata,
|
|
16
|
+
routes: [
|
|
17
|
+
{
|
|
18
|
+
route: satelliteRoutes.routes.list,
|
|
19
|
+
element: <SatelliteListPage />,
|
|
20
|
+
title: "Satellites",
|
|
21
|
+
accessRule: satelliteAccess.satellite.read,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
apis: [],
|
|
25
|
+
extensions: [
|
|
26
|
+
createSlotExtension(UserMenuItemsSlot, {
|
|
27
|
+
id: "satellite.user-menu.items",
|
|
28
|
+
component: SatelliteMenuItems,
|
|
29
|
+
}),
|
|
30
|
+
],
|
|
31
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
usePluginClient,
|
|
4
|
+
accessApiRef,
|
|
5
|
+
useApi,
|
|
6
|
+
wrapInSuspense,
|
|
7
|
+
} from "@checkstack/frontend-api";
|
|
8
|
+
import { useSignal } from "@checkstack/signal-frontend";
|
|
9
|
+
import {
|
|
10
|
+
SatelliteApi,
|
|
11
|
+
satelliteAccess,
|
|
12
|
+
SATELLITE_STATUS_CHANGED,
|
|
13
|
+
} from "@checkstack/satellite-common";
|
|
14
|
+
import type { SatelliteWithStatus } from "@checkstack/satellite-common";
|
|
15
|
+
import {
|
|
16
|
+
Card,
|
|
17
|
+
CardHeader,
|
|
18
|
+
CardTitle,
|
|
19
|
+
CardContent,
|
|
20
|
+
Button,
|
|
21
|
+
LoadingSpinner,
|
|
22
|
+
EmptyState,
|
|
23
|
+
Table,
|
|
24
|
+
TableHeader,
|
|
25
|
+
TableRow,
|
|
26
|
+
TableHead,
|
|
27
|
+
TableBody,
|
|
28
|
+
TableCell,
|
|
29
|
+
useToast,
|
|
30
|
+
ConfirmationModal,
|
|
31
|
+
PageLayout,
|
|
32
|
+
} from "@checkstack/ui";
|
|
33
|
+
import { Plus, Satellite, Trash2, MapPin } from "lucide-react";
|
|
34
|
+
import { SatelliteStatusBadge } from "../components/SatelliteStatusBadge";
|
|
35
|
+
import { CreateSatelliteDialog } from "../components/CreateSatelliteDialog";
|
|
36
|
+
import { extractErrorMessage } from "@checkstack/common";
|
|
37
|
+
|
|
38
|
+
const SatelliteListPageContent: React.FC = () => {
|
|
39
|
+
const satelliteClient = usePluginClient(SatelliteApi);
|
|
40
|
+
const accessApi = useApi(accessApiRef);
|
|
41
|
+
const toast = useToast();
|
|
42
|
+
|
|
43
|
+
const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
|
|
44
|
+
satelliteAccess.satellite.manage,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
48
|
+
const [deleteTarget, setDeleteTarget] = useState<
|
|
49
|
+
SatelliteWithStatus | undefined
|
|
50
|
+
>();
|
|
51
|
+
|
|
52
|
+
const {
|
|
53
|
+
data: satellites,
|
|
54
|
+
isLoading,
|
|
55
|
+
refetch,
|
|
56
|
+
} = satelliteClient.listSatellites.useQuery();
|
|
57
|
+
|
|
58
|
+
// Real-time status updates
|
|
59
|
+
useSignal(SATELLITE_STATUS_CHANGED, () => {
|
|
60
|
+
void refetch();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const deleteMutation = satelliteClient.deleteSatellite.useMutation({
|
|
64
|
+
onSuccess: () => {
|
|
65
|
+
toast.success("Satellite deleted");
|
|
66
|
+
void refetch();
|
|
67
|
+
setDeleteTarget(undefined);
|
|
68
|
+
},
|
|
69
|
+
onError: (error) => {
|
|
70
|
+
toast.error(extractErrorMessage(error, "Failed to delete satellite"));
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const handleDelete = () => {
|
|
75
|
+
if (!deleteTarget) return;
|
|
76
|
+
deleteMutation.mutate({ id: deleteTarget.id });
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const satelliteList = satellites?.satellites ?? [];
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<PageLayout
|
|
83
|
+
title="Satellites"
|
|
84
|
+
subtitle="Manage distributed satellite nodes for remote health check execution"
|
|
85
|
+
icon={Satellite}
|
|
86
|
+
loading={accessLoading}
|
|
87
|
+
allowed={canManage}
|
|
88
|
+
actions={
|
|
89
|
+
<Button onClick={() => setCreateOpen(true)}>
|
|
90
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
91
|
+
Create Satellite
|
|
92
|
+
</Button>
|
|
93
|
+
}
|
|
94
|
+
>
|
|
95
|
+
<Card>
|
|
96
|
+
<CardHeader className="border-b border-border">
|
|
97
|
+
<div className="flex items-center gap-2">
|
|
98
|
+
<Satellite className="h-5 w-5 text-muted-foreground" />
|
|
99
|
+
<CardTitle>Satellite Nodes</CardTitle>
|
|
100
|
+
</div>
|
|
101
|
+
</CardHeader>
|
|
102
|
+
<CardContent className="p-0">
|
|
103
|
+
{isLoading ? (
|
|
104
|
+
<div className="p-12 flex justify-center">
|
|
105
|
+
<LoadingSpinner />
|
|
106
|
+
</div>
|
|
107
|
+
) : satelliteList.length === 0 ? (
|
|
108
|
+
<EmptyState
|
|
109
|
+
title="No satellites configured"
|
|
110
|
+
description="Deploy satellite nodes to execute health checks from multiple geographic locations."
|
|
111
|
+
/>
|
|
112
|
+
) : (
|
|
113
|
+
<Table>
|
|
114
|
+
<TableHeader>
|
|
115
|
+
<TableRow>
|
|
116
|
+
<TableHead>Name</TableHead>
|
|
117
|
+
<TableHead>Region</TableHead>
|
|
118
|
+
<TableHead>Status</TableHead>
|
|
119
|
+
<TableHead>Version</TableHead>
|
|
120
|
+
<TableHead className="w-20">Actions</TableHead>
|
|
121
|
+
</TableRow>
|
|
122
|
+
</TableHeader>
|
|
123
|
+
<TableBody>
|
|
124
|
+
{satelliteList.map((sat) => (
|
|
125
|
+
<TableRow key={sat.id}>
|
|
126
|
+
<TableCell>
|
|
127
|
+
<p className="font-medium">{sat.name}</p>
|
|
128
|
+
<p className="text-xs text-muted-foreground font-mono">
|
|
129
|
+
{sat.id}
|
|
130
|
+
</p>
|
|
131
|
+
</TableCell>
|
|
132
|
+
<TableCell>
|
|
133
|
+
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
|
|
134
|
+
<MapPin className="h-3.5 w-3.5" />
|
|
135
|
+
{sat.region}
|
|
136
|
+
</div>
|
|
137
|
+
</TableCell>
|
|
138
|
+
<TableCell>
|
|
139
|
+
<SatelliteStatusBadge status={sat.status} />
|
|
140
|
+
</TableCell>
|
|
141
|
+
<TableCell>
|
|
142
|
+
<span className="text-sm text-muted-foreground font-mono">
|
|
143
|
+
{sat.version ?? "—"}
|
|
144
|
+
</span>
|
|
145
|
+
</TableCell>
|
|
146
|
+
<TableCell>
|
|
147
|
+
<Button
|
|
148
|
+
variant="ghost"
|
|
149
|
+
size="sm"
|
|
150
|
+
onClick={() => setDeleteTarget(sat)}
|
|
151
|
+
>
|
|
152
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
153
|
+
</Button>
|
|
154
|
+
</TableCell>
|
|
155
|
+
</TableRow>
|
|
156
|
+
))}
|
|
157
|
+
</TableBody>
|
|
158
|
+
</Table>
|
|
159
|
+
)}
|
|
160
|
+
</CardContent>
|
|
161
|
+
</Card>
|
|
162
|
+
|
|
163
|
+
<CreateSatelliteDialog
|
|
164
|
+
open={createOpen}
|
|
165
|
+
onOpenChange={setCreateOpen}
|
|
166
|
+
onCreated={() => void refetch()}
|
|
167
|
+
/>
|
|
168
|
+
|
|
169
|
+
<ConfirmationModal
|
|
170
|
+
isOpen={!!deleteTarget}
|
|
171
|
+
onClose={() => setDeleteTarget(undefined)}
|
|
172
|
+
title="Delete Satellite"
|
|
173
|
+
message={`Are you sure you want to delete satellite "${deleteTarget?.name}"? This will remove all satellite assignments from health checks.`}
|
|
174
|
+
confirmText="Delete"
|
|
175
|
+
variant="danger"
|
|
176
|
+
onConfirm={handleDelete}
|
|
177
|
+
isLoading={deleteMutation.isPending}
|
|
178
|
+
/>
|
|
179
|
+
</PageLayout>
|
|
180
|
+
);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const SatelliteListPage = wrapInSuspense(SatelliteListPageContent);
|