@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 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
+ &quot;eu-west-1&quot;, &quot;us-east-2&quot;, or
203
+ &quot;datacenter-fra&quot;.
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);
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/frontend.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }