@yourrealm/sdk 0.0.0-alpha.1378.ce03c8dc
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/LICENSE-EXCEPTION +26 -0
- package/README.md +34 -0
- package/dist/chunk-3R6KEFOI.js +22 -0
- package/dist/chunk-67BDAMYX.js +376 -0
- package/dist/chunk-AITGS27R.js +139 -0
- package/dist/chunk-CMAEHQ4F.js +6 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-O3UIK34B.js +1430 -0
- package/dist/chunk-VHTUSZUX.js +176 -0
- package/dist/cli.js +9006 -0
- package/dist/docker.js +54 -0
- package/dist/index.js +1053 -0
- package/dist/ingress.js +436 -0
- package/dist/jsx-runtime.js +2 -0
- package/dist/preact.js +22 -0
- package/dist/service-url.js +11 -0
- package/dist/services.js +213 -0
- package/dist/test.js +5069 -0
- package/dist/ui.js +254 -0
- package/dist/worker-protocol.js +89 -0
- package/package.json +112 -0
- package/src/app-icon.ts +74 -0
- package/src/apply.ts +52 -0
- package/src/bundle-ui.ts +211 -0
- package/src/bundle.ts +546 -0
- package/src/cli/build-dev-landing.ts +84 -0
- package/src/cli/build-install-panel.ts +90 -0
- package/src/cli/build-runtime.ts +133 -0
- package/src/cli/build.ts +63 -0
- package/src/cli/dev.ts +107 -0
- package/src/cli/down.ts +60 -0
- package/src/cli/main.ts +56 -0
- package/src/cli/test.ts +61 -0
- package/src/cli/validate.ts +37 -0
- package/src/compile.ts +100 -0
- package/src/create-app.ts +982 -0
- package/src/deep-merge.ts +33 -0
- package/src/dev/fixture.ts +48 -0
- package/src/dev/install-handler.ts +383 -0
- package/src/dev/server.ts +968 -0
- package/src/dev/stack.ts +108 -0
- package/src/dev/state.ts +159 -0
- package/src/docker.ts +96 -0
- package/src/errors.ts +61 -0
- package/src/factory.ts +481 -0
- package/src/garage.ts +56 -0
- package/src/generator/hash.ts +6 -0
- package/src/generator/resolve-refs.ts +207 -0
- package/src/grants-builder.ts +100 -0
- package/src/grants.ts +260 -0
- package/src/home-api.ts +172 -0
- package/src/home-cookie.ts +10 -0
- package/src/home-registry.ts +120 -0
- package/src/home.ts +187 -0
- package/src/host-registry.ts +143 -0
- package/src/http-header.ts +20 -0
- package/src/identifiers.ts +49 -0
- package/src/index.ts +66 -0
- package/src/info.ts +64 -0
- package/src/ini.ts +50 -0
- package/src/json.ts +14 -0
- package/src/jsx-runtime/index.ts +5 -0
- package/src/keys.ts +29 -0
- package/src/mysql.ts +56 -0
- package/src/pack-build.ts +108 -0
- package/src/pack.ts +115 -0
- package/src/peer-registry.ts +121 -0
- package/src/peer-world.ts +117 -0
- package/src/permission-meta.ts +116 -0
- package/src/permissions.ts +108 -0
- package/src/pkg-root.ts +34 -0
- package/src/postgres.ts +79 -0
- package/src/preact/index.ts +27 -0
- package/src/redis.ts +43 -0
- package/src/ref.ts +271 -0
- package/src/remote-registry.ts +33 -0
- package/src/required-host-kinds.ts +110 -0
- package/src/resolve-context.ts +111 -0
- package/src/resolve.ts +471 -0
- package/src/runtime/docker/compose/compose-schema.ts +118 -0
- package/src/runtime/docker/compose/convert-to-docker-compose.ts +278 -0
- package/src/runtime/docker/compose/default-hardening.ts +21 -0
- package/src/runtime/docker/compose/effects.ts +107 -0
- package/src/runtime/docker/compose/index.ts +17 -0
- package/src/runtime/docker/compose/labels.ts +39 -0
- package/src/runtime/docker/compose/types.ts +78 -0
- package/src/runtime/docker/home-proxy-server/index.ts +177 -0
- package/src/runtime/docker/home-proxy-server/protocol.ts +53 -0
- package/src/runtime/docker/ingress/build-config.ts +819 -0
- package/src/runtime/docker/ingress/index.ts +16 -0
- package/src/runtime/docker/ingress/types.ts +182 -0
- package/src/runtime/docker/log-frames.ts +121 -0
- package/src/runtime/docker/logs-server/index.ts +366 -0
- package/src/runtime/docker/spawn-server/context-key.ts +82 -0
- package/src/runtime/docker/spawn-server/index.ts +492 -0
- package/src/runtime/docker/spawn-server/protocol.ts +70 -0
- package/src/runtime/docker/spawn-server/spec.ts +122 -0
- package/src/runtime/docker/stats-server/disks.ts +161 -0
- package/src/runtime/docker/stats-server/index.ts +322 -0
- package/src/runtime/home-client/index.ts +71 -0
- package/src/runtime/log-line.ts +24 -0
- package/src/runtime/logs-client/index.ts +175 -0
- package/src/runtime/spawn-client/index.ts +93 -0
- package/src/runtime/spawn-client/kaniko.ts +79 -0
- package/src/runtime/stats-client/index.ts +65 -0
- package/src/runtime/stats-sample.ts +95 -0
- package/src/safe-json.ts +37 -0
- package/src/sandbox.ts +1209 -0
- package/src/schema/build-apps.ts +38 -0
- package/src/schema/home.ts +217 -0
- package/src/schema/host.ts +792 -0
- package/src/schema/inputs.ts +26 -0
- package/src/schema/nodes.ts +75 -0
- package/src/schema/peer.ts +400 -0
- package/src/schema/public-domain.ts +116 -0
- package/src/schema/remote.ts +74 -0
- package/src/security.ts +594 -0
- package/src/serialize.ts +21 -0
- package/src/service-definition.ts +284 -0
- package/src/service-url.ts +64 -0
- package/src/service.ts +39 -0
- package/src/services/index.ts +5 -0
- package/src/shared.ts +65 -0
- package/src/speculative-silo.ts +96 -0
- package/src/stable-secret.ts +14 -0
- package/src/surfaces.ts +264 -0
- package/src/tailwind-bin.ts +56 -0
- package/src/test/boot.ts +192 -0
- package/src/test/compile.ts +501 -0
- package/src/test/describe.ts +117 -0
- package/src/test/index.ts +11 -0
- package/src/test/ingress-fixture.ts +37 -0
- package/src/test/types.ts +318 -0
- package/src/trusted-headers.ts +79 -0
- package/src/types.ts +126 -0
- package/src/ui/components/aside.tsx +96 -0
- package/src/ui/components/badge.tsx +42 -0
- package/src/ui/components/button.tsx +59 -0
- package/src/ui/components/card.tsx +106 -0
- package/src/ui/components/checkbox.tsx +37 -0
- package/src/ui/components/code-block.tsx +29 -0
- package/src/ui/components/combobox.tsx +119 -0
- package/src/ui/components/config-field.tsx +284 -0
- package/src/ui/components/dialog.tsx +137 -0
- package/src/ui/components/domain-editor.tsx +317 -0
- package/src/ui/components/domain-list.ts +205 -0
- package/src/ui/components/dropdown-menu.tsx +73 -0
- package/src/ui/components/empty-state.tsx +51 -0
- package/src/ui/components/highlight.ts +59 -0
- package/src/ui/components/icons.tsx +101 -0
- package/src/ui/components/image-posture.tsx +85 -0
- package/src/ui/components/input.tsx +28 -0
- package/src/ui/components/label.tsx +16 -0
- package/src/ui/components/line-chart.tsx +113 -0
- package/src/ui/components/page.tsx +39 -0
- package/src/ui/components/permission-row.tsx +262 -0
- package/src/ui/components/permission-section.tsx +51 -0
- package/src/ui/components/pie-chart.tsx +245 -0
- package/src/ui/components/schema-block.tsx +60 -0
- package/src/ui/components/separator.tsx +42 -0
- package/src/ui/components/sheet.tsx +130 -0
- package/src/ui/components/sidebar.tsx +650 -0
- package/src/ui/components/silo-picker.tsx +510 -0
- package/src/ui/components/skeleton.tsx +13 -0
- package/src/ui/components/sonner.tsx +33 -0
- package/src/ui/components/switch.tsx +58 -0
- package/src/ui/components/table-view.ts +219 -0
- package/src/ui/components/table.tsx +506 -0
- package/src/ui/components/tabs.tsx +59 -0
- package/src/ui/components/timeago.tsx +84 -0
- package/src/ui/components/tooltip.tsx +56 -0
- package/src/ui/components.json +21 -0
- package/src/ui/dev/dev-shell.tsx +145 -0
- package/src/ui/home/home-grid.tsx +177 -0
- package/src/ui/hooks/use-mobile.ts +19 -0
- package/src/ui/index.ts +189 -0
- package/src/ui/install/adapter.ts +193 -0
- package/src/ui/install/deploy-targets-picker.tsx +185 -0
- package/src/ui/install/install-app-panel.tsx +912 -0
- package/src/ui/install/install-error.tsx +62 -0
- package/src/ui/install/perms-inactive.ts +50 -0
- package/src/ui/install/required-fields.ts +37 -0
- package/src/ui/install/tabs-form.tsx +557 -0
- package/src/ui/tokens.css +279 -0
- package/src/ui/utils.ts +78 -0
- package/src/ui-error.ts +139 -0
- package/src/ui-runtime/dev-landing-entry.tsx +258 -0
- package/src/ui-runtime/index.ts +83 -0
- package/src/ui-runtime/install-entry.tsx +119 -0
- package/src/ui-runtime/mount.ts +367 -0
- package/src/ui-runtime/runtime.css +56 -0
- package/src/ui-surface.ts +451 -0
- package/src/valkey.ts +43 -0
- package/src/worker/extract-tar.ts +125 -0
- package/src/worker/identity.ts +183 -0
- package/src/worker/index.ts +16 -0
- package/src/worker/public-ip.ts +92 -0
- package/src/worker/queue.ts +54 -0
- package/src/worker/reconcile.ts +127 -0
- package/src/worker/start-worker.ts +227 -0
- package/src/worker/ui-proxy.ts +335 -0
- package/src/worker/uplink.ts +523 -0
- package/src/worker/validate.ts +34 -0
- package/src/worker/verify-relay.ts +102 -0
- package/src/worker-protocol/app-dirs.ts +94 -0
- package/src/worker-protocol/home-proxy-paths.ts +76 -0
- package/src/worker-protocol/index.ts +55 -0
- package/src/worker-protocol/ingress.ts +166 -0
- package/src/worker-protocol/logs-socket-path.ts +50 -0
- package/src/worker-protocol/networks.ts +39 -0
- package/src/worker-protocol/node-version.ts +70 -0
- package/src/worker-protocol/silo-paths.ts +114 -0
- package/src/worker-protocol/spawn-paths.ts +165 -0
- package/src/worker-protocol/stats-socket-path.ts +40 -0
- package/src/worker-protocol/update-files.ts +77 -0
- package/src/worker-protocol/uplink/constants.ts +86 -0
- package/src/worker-protocol/uplink/errors.ts +61 -0
- package/src/worker-protocol/uplink/frame.ts +114 -0
- package/src/worker-protocol/uplink/identity.ts +40 -0
- package/src/worker-protocol/uplink/index.ts +63 -0
- package/src/worker-protocol/uplink/methods.ts +462 -0
- package/src/worker-protocol/uplink/mux.ts +646 -0
- package/src/worker-protocol/uplink/protocol.ts +11 -0
- package/src/worker-protocol/uplink/rpc.ts +309 -0
- package/src/worker-protocol/uplink/session.ts +540 -0
- package/src/worker-protocol/uplink/test-support.ts +188 -0
- package/src/worker-protocol/worker-mount.ts +27 -0
- package/src/worker-protocol/worker-state.ts +190 -0
- package/src/xml.ts +49 -0
- package/src/yaml.ts +15 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Transport contract between the install panel and whoever's hosting it. Home
|
|
2
|
+
// wraps `fetch` to its own /api/apps endpoints; `realm-sdk dev` supplies one
|
|
3
|
+
// over its local dev server, and any future operator surface (Realm Cloud
|
|
4
|
+
// admin, a CLI confirm step) can do the same.
|
|
5
|
+
//
|
|
6
|
+
// The panel does no HTTP itself — it only knows the adapter shape. Adapter
|
|
7
|
+
// methods either resolve with a parsed payload or throw an
|
|
8
|
+
// `InstallAdapterError` carrying a render-ready `InstallErrorShape`. Anything
|
|
9
|
+
// else thrown is surfaced as a network error by the panel.
|
|
10
|
+
|
|
11
|
+
import type { Grants, Violation } from "../../home.ts";
|
|
12
|
+
import type { ConfigFieldTree, SerializablePermission } from "../../sandbox.ts";
|
|
13
|
+
|
|
14
|
+
// ---- Deploy targets ------------------------------------------------
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* One worker's id — the box a single app is pinned to. A system app
|
|
18
|
+
* has no deploy target to carry: it runs on every compatible worker by
|
|
19
|
+
* virtue of being one, so the host derives that from the source and the
|
|
20
|
+
* panel sends nothing.
|
|
21
|
+
*/
|
|
22
|
+
export type DeployTargets = string;
|
|
23
|
+
|
|
24
|
+
// ---- Render-ready error shape ---------------------------------------
|
|
25
|
+
|
|
26
|
+
export type InstallErrorShape =
|
|
27
|
+
| { kind: "violations"; violations: Violation[] }
|
|
28
|
+
| { kind: "message"; message: string }
|
|
29
|
+
| { kind: "network"; message: string };
|
|
30
|
+
|
|
31
|
+
// ---- Preview + install payloads ------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Image posture row from the preview endpoint. `digest === null`
|
|
35
|
+
* means Home hasn't pinned this ref yet — preview is cache-only,
|
|
36
|
+
* so a brand-new ref shows as unpinned until install commits.
|
|
37
|
+
*/
|
|
38
|
+
export interface PreviewImage {
|
|
39
|
+
ref: string;
|
|
40
|
+
digest: string | null;
|
|
41
|
+
runsAs: string | null;
|
|
42
|
+
runsAsRoot: boolean;
|
|
43
|
+
exposes: string[];
|
|
44
|
+
volumes: string[];
|
|
45
|
+
imageCreatedAt: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PreviewResult {
|
|
49
|
+
permissionsFlat: Record<string, SerializablePermission>;
|
|
50
|
+
configFields: ConfigFieldTree;
|
|
51
|
+
images: PreviewImage[] | null;
|
|
52
|
+
/**
|
|
53
|
+
* True when the app's resolved services declare any persistent (bind-mount)
|
|
54
|
+
* volume. Drives the "Install as template" explainer's note that apps created
|
|
55
|
+
* from a template won't keep volumes (Home strips them for preview rows). NOT
|
|
56
|
+
* the same as a `PreviewImage.volumes` (image-VOLUME) posture.
|
|
57
|
+
*/
|
|
58
|
+
hasPersistentVolumes: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* App kind. "single" means the app runs on one selected worker
|
|
61
|
+
* (install panel shows a single-pick). "system" means the app fans
|
|
62
|
+
* out across every compatible worker (static caption, no picker).
|
|
63
|
+
* See realm-ts/workers.mdx.
|
|
64
|
+
*/
|
|
65
|
+
kind: "single" | "system";
|
|
66
|
+
/**
|
|
67
|
+
* Existing fixture values, if the host has them. `realm-sdk dev`
|
|
68
|
+
* surfaces these from `realm-sdk.dev.ts` so the panel pre-fills the
|
|
69
|
+
* form with the last install's choices, turning the install flow into
|
|
70
|
+
* an edit-and-resave flow on second visit. Home never sets these — its
|
|
71
|
+
* preview reflects "fresh install only".
|
|
72
|
+
*/
|
|
73
|
+
currentConfig?: Record<string, unknown>;
|
|
74
|
+
currentGrants?: Grants;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface InstallPayload {
|
|
78
|
+
source: string;
|
|
79
|
+
/**
|
|
80
|
+
* The single app's chosen worker. Omitted for a system app — placement
|
|
81
|
+
* is derived from the source's kind, not submitted.
|
|
82
|
+
*/
|
|
83
|
+
deployed?: DeployTargets;
|
|
84
|
+
config: Record<string, unknown>;
|
|
85
|
+
grants: Grants;
|
|
86
|
+
/**
|
|
87
|
+
* Upstream registry id when the source came from a known store listing
|
|
88
|
+
* (e.g. "realm/counter-fresh"). Lets the host correlate installs with
|
|
89
|
+
* their upstream entry. Absent for hand-pasted source.
|
|
90
|
+
*/
|
|
91
|
+
sourceId?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Install as a parked template instead of a persistent app. A template
|
|
94
|
+
* never deploys; a `builder`-scoped app (e.g. Forge) stamps
|
|
95
|
+
* `preview` children from it. Set by the panel's "Install as template"
|
|
96
|
+
* button; the deploy target the operator picks still applies, since
|
|
97
|
+
* previews inherit the template's `deployed`.
|
|
98
|
+
*/
|
|
99
|
+
role?: "template";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---- Silos ----------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A Silo row as the picker renders it. `hostPath` is always shown: it's the
|
|
106
|
+
* one field that says which bytes an operator is about to hand an app.
|
|
107
|
+
*/
|
|
108
|
+
export interface SiloOption {
|
|
109
|
+
id: string;
|
|
110
|
+
name: string;
|
|
111
|
+
workerId: string;
|
|
112
|
+
/** The worker's slug, for the surfaces that name the box to the operator. */
|
|
113
|
+
workerSlug: string;
|
|
114
|
+
hostPath: string;
|
|
115
|
+
containerPath: string;
|
|
116
|
+
gid: number;
|
|
117
|
+
/**
|
|
118
|
+
* The command that hands an adopted directory to this Silo's group, or null
|
|
119
|
+
* when there's nothing left to run - a Realm-managed directory (the worker
|
|
120
|
+
* prepares those) or one the operator already confirmed. Non-null means the
|
|
121
|
+
* Silo is pickable but may not be readable yet, which is what the card warns
|
|
122
|
+
* about. Computed by the host, never by the picker: an operator who follows a
|
|
123
|
+
* stale command gets a Silo that looks ready and shares nothing.
|
|
124
|
+
*/
|
|
125
|
+
setupCommand: string | null;
|
|
126
|
+
/**
|
|
127
|
+
* Published service directory (name → port). Absent = unknown - the picker
|
|
128
|
+
* keeps the Silo pickable and the server's install gate stays
|
|
129
|
+
* authoritative; `{}` = genuinely memberless. Drives the require-map
|
|
130
|
+
* filtering (a Silo missing a required service renders unpickable).
|
|
131
|
+
*/
|
|
132
|
+
offered?: Record<string, number>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---- Adapter contract ----------------------------------------------
|
|
136
|
+
|
|
137
|
+
export interface InstallAdapter {
|
|
138
|
+
/** Parse + schema-extract the realm.ts source. */
|
|
139
|
+
preview(source: string): Promise<PreviewResult>;
|
|
140
|
+
/** Persist the candidate app + grants. Resolves with the new app's id. */
|
|
141
|
+
install(payload: InstallPayload): Promise<{ id: string }>;
|
|
142
|
+
/**
|
|
143
|
+
* Every Silo the host knows. Fetched only when the app declares a
|
|
144
|
+
* `host.silo` permission; the picker filters to the chosen worker.
|
|
145
|
+
*
|
|
146
|
+
* Optional, and its absence is the answer to "does this host do Silos at
|
|
147
|
+
* all": a Silo is a Home record on a named worker, and `realm-sdk dev` has
|
|
148
|
+
* neither. A host that omits this gets no picker and no Silo install gate,
|
|
149
|
+
* rather than a picker stuck asking for a worker it will never offer.
|
|
150
|
+
*/
|
|
151
|
+
listSilos?(): Promise<SiloOption[]>;
|
|
152
|
+
/**
|
|
153
|
+
* Create one from inside the picker. The worker comes from the deploy
|
|
154
|
+
* target the operator already picked, so it isn't asked for twice.
|
|
155
|
+
*
|
|
156
|
+
* Separately optional from `listSilos`: a host could serve a fixed set it
|
|
157
|
+
* doesn't let you add to. The picker keeps its create card out when this is
|
|
158
|
+
* absent.
|
|
159
|
+
*/
|
|
160
|
+
createSilo?(input: {
|
|
161
|
+
name: string;
|
|
162
|
+
workerId: string;
|
|
163
|
+
containerPath?: string;
|
|
164
|
+
/** A directory the operator already has. Omitted = Realm makes one. */
|
|
165
|
+
hostPath?: string;
|
|
166
|
+
}): Promise<SiloOption>;
|
|
167
|
+
/**
|
|
168
|
+
* Record that the operator ran an adopted Silo's setup command, so the
|
|
169
|
+
* warning on its card retires without a trip to the Silos page.
|
|
170
|
+
*
|
|
171
|
+
* Separately optional again: a host that lists Silos it doesn't let you adopt
|
|
172
|
+
* into never needs it, and the card falls back to the warning alone.
|
|
173
|
+
*/
|
|
174
|
+
acknowledgeSilo?(id: string): Promise<void>;
|
|
175
|
+
/**
|
|
176
|
+
* Called after `install` resolves, with the new app's id. The side-effect
|
|
177
|
+
* lives outside the call so the adapter owns the navigation. Home lands on
|
|
178
|
+
* the app's summary page; dev mode would mount the app's own UI in place.
|
|
179
|
+
*/
|
|
180
|
+
onInstalled(appId: string): void;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Adapter-side failure carrying a parsed InstallErrorShape. The
|
|
185
|
+
* panel catches this to render the structured error. Anything else
|
|
186
|
+
* thrown from an adapter method is treated as a network error.
|
|
187
|
+
*/
|
|
188
|
+
export class InstallAdapterError extends Error {
|
|
189
|
+
constructor(public readonly shape: InstallErrorShape) {
|
|
190
|
+
super(shape.kind === "violations" ? "grant violations" : shape.message);
|
|
191
|
+
this.name = "InstallAdapterError";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useSignal } from "@preact/signals";
|
|
2
|
+
import { CaretDownIcon } from "../components/icons.tsx";
|
|
3
|
+
import { Label } from "../components/label.tsx";
|
|
4
|
+
import type { HostPermissionKind, WorkerCapability } from "../../home.ts";
|
|
5
|
+
import type { DeployTargets } from "./adapter.ts";
|
|
6
|
+
|
|
7
|
+
export type { DeployTargets };
|
|
8
|
+
|
|
9
|
+
interface Worker {
|
|
10
|
+
id: string;
|
|
11
|
+
slug: string;
|
|
12
|
+
name: string | null;
|
|
13
|
+
capabilities?: WorkerCapability[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface DeployTargetsPickerProps {
|
|
17
|
+
workers: Worker[];
|
|
18
|
+
/**
|
|
19
|
+
* Current selection: one worker's id (single apps). `null` is
|
|
20
|
+
* "nothing picked" — the picker emits it for a single app with no
|
|
21
|
+
* choice made yet, and it's the standing value for a system app,
|
|
22
|
+
* which shows a static caption instead of a list.
|
|
23
|
+
*/
|
|
24
|
+
value: DeployTargets | null;
|
|
25
|
+
onChange: (v: DeployTargets | null) => void;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* App kind from the `createApp` / `createSystemApp` factory. "single"
|
|
29
|
+
* renders a radio list of workers; "system" renders a caption (no
|
|
30
|
+
* choice).
|
|
31
|
+
*/
|
|
32
|
+
kind: "single" | "system";
|
|
33
|
+
/**
|
|
34
|
+
* Required host kinds the candidate app declares. Workers that don't
|
|
35
|
+
* advertise every kind are disabled in the picker. The server-side
|
|
36
|
+
* gate is the authoritative source — this is just a hint.
|
|
37
|
+
*/
|
|
38
|
+
requiredKinds?: HostPermissionKind[];
|
|
39
|
+
/**
|
|
40
|
+
* Collapse to a one-line "deploys to X · Change" row once a worker is
|
|
41
|
+
* selected. The install panel sets this: the common case is one capable
|
|
42
|
+
* worker, already pre-selected, and a radio list of one is noise. With
|
|
43
|
+
* nothing selected — no workers, or none that satisfy `requiredKinds` —
|
|
44
|
+
* the list renders expanded regardless, because *why* there's no
|
|
45
|
+
* selection is the thing the operator needs to see.
|
|
46
|
+
*/
|
|
47
|
+
collapsible?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function DeployTargetsPicker({
|
|
51
|
+
workers,
|
|
52
|
+
value,
|
|
53
|
+
onChange,
|
|
54
|
+
disabled,
|
|
55
|
+
kind,
|
|
56
|
+
requiredKinds,
|
|
57
|
+
collapsible,
|
|
58
|
+
}: DeployTargetsPickerProps) {
|
|
59
|
+
const expanded = useSignal(false);
|
|
60
|
+
function missingFor(w: Worker): HostPermissionKind[] {
|
|
61
|
+
if (!requiredKinds || requiredKinds.length === 0) return [];
|
|
62
|
+
const caps = new Set(w.capabilities ?? []);
|
|
63
|
+
return requiredKinds.filter((k) => !caps.has(k));
|
|
64
|
+
}
|
|
65
|
+
const supportedCount = workers.filter((w) => missingFor(w).length === 0).length;
|
|
66
|
+
const showSupportHint =
|
|
67
|
+
!!requiredKinds && requiredKinds.length > 0 && supportedCount < workers.length;
|
|
68
|
+
|
|
69
|
+
if (kind === "system") {
|
|
70
|
+
return (
|
|
71
|
+
<div class="flex flex-col gap-3 rounded-2xl border border-border bg-card p-4">
|
|
72
|
+
<div class="flex flex-col gap-0.5">
|
|
73
|
+
<span class="text-sm font-bold text-foreground">Deploys to every worker</span>
|
|
74
|
+
<span class="text-xs text-muted-foreground">
|
|
75
|
+
Every current and future compatible worker runs this app.
|
|
76
|
+
</span>
|
|
77
|
+
{showSupportHint && (
|
|
78
|
+
<span class="text-xs text-muted-foreground">
|
|
79
|
+
Currently {supportedCount} of {workers.length} workers (others lack required
|
|
80
|
+
capabilities).
|
|
81
|
+
</span>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const selected = typeof value === "string" ? value : null;
|
|
89
|
+
const pick = (workerId: string) => {
|
|
90
|
+
onChange(workerId);
|
|
91
|
+
// Picking is the whole point of expanding; fold back up so the step
|
|
92
|
+
// reads as one settled line again.
|
|
93
|
+
if (collapsible) expanded.value = false;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// The collapsed line is a toggle, not a "Change" shortcut that only appears
|
|
97
|
+
// when there's something to change: opening it on a one-worker Realm and
|
|
98
|
+
// seeing the single radio already ticked is the answer to "what did it pick
|
|
99
|
+
// for me, and did I have a say?". Hiding the control there leaves that
|
|
100
|
+
// unanswerable.
|
|
101
|
+
if (collapsible && selected !== null && !expanded.value) {
|
|
102
|
+
const w = workers.find((x) => x.id === selected);
|
|
103
|
+
return (
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
disabled={disabled}
|
|
107
|
+
onClick={() => (expanded.value = true)}
|
|
108
|
+
class="flex w-fit items-center gap-2 text-xs text-muted-foreground hover:text-foreground cursor-pointer disabled:cursor-not-allowed"
|
|
109
|
+
data-testid="deploy-target-collapsed"
|
|
110
|
+
>
|
|
111
|
+
<span>Deploys to</span>
|
|
112
|
+
<span class="font-mono font-semibold text-foreground">
|
|
113
|
+
{w?.name ?? w?.slug ?? selected}
|
|
114
|
+
</span>
|
|
115
|
+
{w?.name && <span class="font-mono">{w.slug}</span>}
|
|
116
|
+
<CaretDownIcon className="size-3" />
|
|
117
|
+
</button>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Collapsing back needs its own affordance: re-clicking an already-checked
|
|
122
|
+
// radio fires no change event, so on a one-worker Realm the pick can't do it.
|
|
123
|
+
const canCollapse = collapsible && selected !== null;
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div class="flex flex-col gap-3 rounded-2xl border border-border bg-card p-4">
|
|
127
|
+
<div class="flex flex-col gap-2">
|
|
128
|
+
{canCollapse ? (
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
onClick={() => (expanded.value = false)}
|
|
132
|
+
class="flex w-fit items-center gap-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground hover:text-foreground cursor-pointer"
|
|
133
|
+
data-testid="deploy-target-collapse"
|
|
134
|
+
>
|
|
135
|
+
Pick a worker
|
|
136
|
+
<CaretDownIcon className="size-3 rotate-180" />
|
|
137
|
+
</button>
|
|
138
|
+
) : (
|
|
139
|
+
<span class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
140
|
+
Pick a worker
|
|
141
|
+
</span>
|
|
142
|
+
)}
|
|
143
|
+
{workers.length === 0 ? (
|
|
144
|
+
<p class="text-xs text-muted-foreground">
|
|
145
|
+
No workers enrolled yet. Enroll one on the Workers page.
|
|
146
|
+
</p>
|
|
147
|
+
) : (
|
|
148
|
+
workers.map((w) => {
|
|
149
|
+
const id = `target-${w.slug}`;
|
|
150
|
+
const missing = missingFor(w);
|
|
151
|
+
const unsupported = missing.length > 0;
|
|
152
|
+
const checked = selected === w.id;
|
|
153
|
+
return (
|
|
154
|
+
<div key={w.id} class="flex items-start gap-2">
|
|
155
|
+
<input
|
|
156
|
+
type="radio"
|
|
157
|
+
id={id}
|
|
158
|
+
name="deploy-target-single"
|
|
159
|
+
checked={checked}
|
|
160
|
+
disabled={disabled || unsupported}
|
|
161
|
+
onChange={(e) => {
|
|
162
|
+
if ((e.target as HTMLInputElement).checked) pick(w.id);
|
|
163
|
+
}}
|
|
164
|
+
class="mt-1 cursor-pointer disabled:cursor-not-allowed"
|
|
165
|
+
/>
|
|
166
|
+
<Label
|
|
167
|
+
htmlFor={id}
|
|
168
|
+
className={`text-sm font-normal ${unsupported ? "opacity-60" : ""}`}
|
|
169
|
+
>
|
|
170
|
+
<span class="font-mono font-semibold">{w.slug}</span>
|
|
171
|
+
{w.name && <span class="ml-2 text-muted-foreground">{w.name}</span>}
|
|
172
|
+
{unsupported && (
|
|
173
|
+
<span class="block text-xs text-muted-foreground">
|
|
174
|
+
Missing: <span class="font-mono">{missing.join(", ")}</span>
|
|
175
|
+
</span>
|
|
176
|
+
)}
|
|
177
|
+
</Label>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
})
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|