@burdenoff/microfe-vibecontrols 2026.520.1 → 2026.520.3
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/dist/components/agents/AgentForm.js +152 -148
- package/dist/components/agents/AgentForm.js.map +1 -1
- package/dist/components/catalog/CatalogForm.js +46 -42
- package/dist/components/catalog/CatalogForm.js.map +1 -1
- package/dist/components/idp-components/ComponentForm.js +69 -65
- package/dist/components/idp-components/ComponentForm.js.map +1 -1
- package/dist/components/shared/EntityTagPicker.js +5 -1
- package/dist/components/shared/EntityTagPicker.js.map +1 -1
- package/dist/components/tunnels/TunnelCreateWizard.js +55 -56
- package/dist/components/tunnels/TunnelCreateWizard.js.map +1 -1
- package/dist/hooks/useDraftEntityTags.js +29 -0
- package/dist/hooks/useDraftEntityTags.js.map +1 -0
- package/dist/pages/ActionsPage.js +109 -96
- package/dist/pages/ActionsPage.js.map +1 -1
- package/dist/pages/AgentsPage.js +345 -336
- package/dist/pages/AgentsPage.js.map +1 -1
- package/dist/pages/CatalogPage.js +107 -97
- package/dist/pages/CatalogPage.js.map +1 -1
- package/dist/pages/ComponentsPage.js +127 -116
- package/dist/pages/ComponentsPage.js.map +1 -1
- package/dist/pages/SessionsPage.js +598 -586
- package/dist/pages/SessionsPage.js.map +1 -1
- package/dist/pages/TargetsPage.js +499 -485
- package/dist/pages/TargetsPage.js.map +1 -1
- package/dist/pages/VibeDeckPage.js +310 -300
- package/dist/pages/VibeDeckPage.js.map +1 -1
- package/dist/pages/VibesPage.js +310 -296
- package/dist/pages/VibesPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@ import { ErrorState as o } from "../shared/ErrorState.js";
|
|
|
6
6
|
import { QuotaExhaustedDialog as s } from "../shared/QuotaExhaustedDialog.js";
|
|
7
7
|
import { TunnelEnvironment as c, TunnelProtocol as l, TunnelProviderKey as u } from "../../generated/wspace-types.js";
|
|
8
8
|
import { getQuotaDialogContent as d, isQuotaExhaustedError as f } from "../../utils/quotaUtils.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { EntityTagPicker as p } from "../shared/EntityTagPicker.js";
|
|
10
|
+
import { useDraftEntityTags as m } from "../../hooks/useDraftEntityTags.js";
|
|
11
11
|
import { useCallback as h, useMemo as g, useState as _ } from "react";
|
|
12
12
|
import { useNavigate as v } from "react-router-dom";
|
|
13
13
|
import { AnimatePresence as y, motion as b } from "framer-motion";
|
|
@@ -50,32 +50,32 @@ var w = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/, T = {
|
|
|
50
50
|
}
|
|
51
51
|
];
|
|
52
52
|
function D() {
|
|
53
|
-
let D = e(), A = v(), [j, M] = _(0), [N, P] = _(T), [F, I] = _(null), [L, R] = _(!1), [z, B] = _(
|
|
53
|
+
let D = e(), A = v(), [j, M] = _(0), [N, P] = _(T), [F, I] = _(null), [L, R] = _(!1), [z, B] = _(!1), { setDraftTagIds: V, flushTags: H } = m("VIBECONTROLS_TUNNEL"), { data: U, loading: W, error: G } = r(), { data: K } = n({ variables: { pagination: { limit: 100 } } }), [q, { loading: J }] = t(), Y = h((e, t) => {
|
|
54
54
|
P((n) => ({
|
|
55
55
|
...n,
|
|
56
56
|
[e]: t
|
|
57
57
|
}));
|
|
58
|
-
}, []),
|
|
58
|
+
}, []), X = h((e) => {
|
|
59
59
|
P((t) => ({
|
|
60
60
|
...t,
|
|
61
61
|
providerKey: e,
|
|
62
62
|
hostnameKind: e === u.Cloudflare ? "auto" : "managed"
|
|
63
63
|
}));
|
|
64
|
-
}, []),
|
|
64
|
+
}, []), Z = g(() => [...U?.tunnelProviders ?? []].sort((e, t) => {
|
|
65
65
|
let n = (e) => e === "VIBETUNNELS" ? 0 : e === "CLOUDFLARE" ? 1 : 2, r = n(e.key) - n(t.key);
|
|
66
66
|
return r === 0 ? e.key.localeCompare(t.key) : r;
|
|
67
|
-
}), [
|
|
68
|
-
if (!
|
|
67
|
+
}), [U]), Q = g(() => Z.find((e) => e.key === N.providerKey), [Z, N.providerKey])?.capabilities, $ = N.providerKey === u.Cloudflare, ee = g(() => {
|
|
68
|
+
if (!Q) return [l.Http];
|
|
69
69
|
let e = [];
|
|
70
|
-
return
|
|
71
|
-
}, [
|
|
70
|
+
return Q.supportsHttp && e.push(l.Http), Q.supportsHttps && e.push(l.Https), Q.supportsTcp && e.push(l.Tcp), e;
|
|
71
|
+
}, [Q]), te = g(() => j === 0 ? !!(N.providerKey && N.agentId && N.name.trim()) : j === 1 ? N.localPort > 0 && N.localPort < 65536 : j === 2 ? N.hostnameKind === "managed" ? !!(N.subdomain && w.test(N.subdomain)) : N.hostnameKind === "custom" ? N.customDomain.trim().includes(".") : $ : !0, [
|
|
72
72
|
j,
|
|
73
73
|
N,
|
|
74
|
-
|
|
75
|
-
]),
|
|
74
|
+
$
|
|
75
|
+
]), ne = () => M((e) => Math.min(e + 1, E.length - 1)), re = () => M((e) => Math.max(e - 1, 0)), ie = async () => {
|
|
76
76
|
I(null);
|
|
77
77
|
try {
|
|
78
|
-
let e = await
|
|
78
|
+
let e = await q({ variables: { input: {
|
|
79
79
|
agentId: N.agentId,
|
|
80
80
|
name: N.name,
|
|
81
81
|
description: N.description || void 0,
|
|
@@ -90,7 +90,7 @@ function D() {
|
|
|
90
90
|
} } });
|
|
91
91
|
if (e.error) {
|
|
92
92
|
if (f(e.error)) {
|
|
93
|
-
|
|
93
|
+
B(!0);
|
|
94
94
|
return;
|
|
95
95
|
}
|
|
96
96
|
I(e.error.message);
|
|
@@ -98,26 +98,25 @@ function D() {
|
|
|
98
98
|
}
|
|
99
99
|
let t = e.data?.createTunnel.id;
|
|
100
100
|
if (t) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
} catch {}
|
|
101
|
+
R(!0);
|
|
102
|
+
try {
|
|
103
|
+
await H(t);
|
|
104
|
+
} finally {
|
|
105
|
+
R(!1);
|
|
106
|
+
}
|
|
108
107
|
A(`/vibecontrols/tunnels/${t}`);
|
|
109
108
|
}
|
|
110
109
|
} catch (e) {
|
|
111
110
|
if (f(e)) {
|
|
112
|
-
I(null),
|
|
111
|
+
I(null), B(!0);
|
|
113
112
|
return;
|
|
114
113
|
}
|
|
115
114
|
I(e instanceof Error ? e.message : String(e));
|
|
116
115
|
}
|
|
117
116
|
};
|
|
118
|
-
return
|
|
117
|
+
return W ? /* @__PURE__ */ S(a, { message: "Loading providers" }) : G ? /* @__PURE__ */ S(o, {
|
|
119
118
|
title: "Failed to load providers",
|
|
120
|
-
message:
|
|
119
|
+
message: G.message,
|
|
121
120
|
onRetry: () => window.location.reload()
|
|
122
121
|
}) : /* @__PURE__ */ C("div", {
|
|
123
122
|
className: "p-6 max-w-3xl mx-auto",
|
|
@@ -164,9 +163,9 @@ function D() {
|
|
|
164
163
|
}),
|
|
165
164
|
/* @__PURE__ */ S("div", {
|
|
166
165
|
className: "grid gap-3 sm:grid-cols-2",
|
|
167
|
-
children:
|
|
166
|
+
children: Z.map((e) => /* @__PURE__ */ C("button", {
|
|
168
167
|
type: "button",
|
|
169
|
-
onClick: () =>
|
|
168
|
+
onClick: () => X(e.key),
|
|
170
169
|
className: `rounded-lg border p-4 text-left transition-colors ${N.providerKey === e.key ? "border-action-primary-bg bg-bg-sunken" : "border-border-subtle hover:border-border-default"}`,
|
|
171
170
|
children: [
|
|
172
171
|
/* @__PURE__ */ S("div", {
|
|
@@ -188,12 +187,12 @@ function D() {
|
|
|
188
187
|
label: "Agent",
|
|
189
188
|
children: /* @__PURE__ */ C("select", {
|
|
190
189
|
value: N.agentId,
|
|
191
|
-
onChange: (e) =>
|
|
190
|
+
onChange: (e) => Y("agentId", e.target.value),
|
|
192
191
|
className: "w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary",
|
|
193
192
|
children: [/* @__PURE__ */ S("option", {
|
|
194
193
|
value: "",
|
|
195
194
|
children: "Select agent"
|
|
196
|
-
}), (
|
|
195
|
+
}), (K?.vibecontrolsAgents ?? []).map((e) => /* @__PURE__ */ S("option", {
|
|
197
196
|
value: e.id,
|
|
198
197
|
children: e.name
|
|
199
198
|
}, e.id))]
|
|
@@ -204,7 +203,7 @@ function D() {
|
|
|
204
203
|
children: /* @__PURE__ */ S("input", {
|
|
205
204
|
type: "text",
|
|
206
205
|
value: N.name,
|
|
207
|
-
onChange: (e) =>
|
|
206
|
+
onChange: (e) => Y("name", e.target.value),
|
|
208
207
|
placeholder: "My dev tunnel",
|
|
209
208
|
className: "w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-muted"
|
|
210
209
|
})
|
|
@@ -214,7 +213,7 @@ function D() {
|
|
|
214
213
|
children: /* @__PURE__ */ S("input", {
|
|
215
214
|
type: "text",
|
|
216
215
|
value: N.description,
|
|
217
|
-
onChange: (e) =>
|
|
216
|
+
onChange: (e) => Y("description", e.target.value),
|
|
218
217
|
className: "w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-muted"
|
|
219
218
|
})
|
|
220
219
|
})
|
|
@@ -231,9 +230,9 @@ function D() {
|
|
|
231
230
|
label: "Protocol",
|
|
232
231
|
children: /* @__PURE__ */ S("div", {
|
|
233
232
|
className: "flex gap-2",
|
|
234
|
-
children:
|
|
233
|
+
children: ee.map((e) => /* @__PURE__ */ S("button", {
|
|
235
234
|
type: "button",
|
|
236
|
-
onClick: () =>
|
|
235
|
+
onClick: () => Y("protocol", e),
|
|
237
236
|
className: `rounded-md border px-4 py-2 text-sm ${N.protocol === e ? "border-action-primary-bg bg-bg-sunken text-text-primary" : "border-border-subtle text-text-secondary hover:border-border-default"}`,
|
|
238
237
|
children: e
|
|
239
238
|
}, e))
|
|
@@ -244,7 +243,7 @@ function D() {
|
|
|
244
243
|
children: /* @__PURE__ */ S("input", {
|
|
245
244
|
type: "text",
|
|
246
245
|
value: N.localHost,
|
|
247
|
-
onChange: (e) =>
|
|
246
|
+
onChange: (e) => Y("localHost", e.target.value),
|
|
248
247
|
className: "w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary"
|
|
249
248
|
})
|
|
250
249
|
}),
|
|
@@ -253,7 +252,7 @@ function D() {
|
|
|
253
252
|
children: /* @__PURE__ */ S("input", {
|
|
254
253
|
type: "number",
|
|
255
254
|
value: N.localPort,
|
|
256
|
-
onChange: (e) =>
|
|
255
|
+
onChange: (e) => Y("localPort", parseInt(e.target.value, 10) || 0),
|
|
257
256
|
min: 1,
|
|
258
257
|
max: 65535,
|
|
259
258
|
className: "w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary"
|
|
@@ -273,22 +272,22 @@ function D() {
|
|
|
273
272
|
children: [
|
|
274
273
|
/* @__PURE__ */ S(k, {
|
|
275
274
|
active: N.hostnameKind === "managed",
|
|
276
|
-
onClick: () =>
|
|
277
|
-
disabled: !
|
|
275
|
+
onClick: () => Y("hostnameKind", "managed"),
|
|
276
|
+
disabled: !Q?.supportsManagedSubdomains,
|
|
278
277
|
label: "Managed subdomain",
|
|
279
278
|
hint: "*.vibetunnels.com"
|
|
280
279
|
}),
|
|
281
280
|
/* @__PURE__ */ S(k, {
|
|
282
281
|
active: N.hostnameKind === "custom",
|
|
283
|
-
onClick: () =>
|
|
284
|
-
disabled: !
|
|
282
|
+
onClick: () => Y("hostnameKind", "custom"),
|
|
283
|
+
disabled: !Q?.supportsCustomDomains,
|
|
285
284
|
label: "Custom domain",
|
|
286
285
|
hint: "TXT verification"
|
|
287
286
|
}),
|
|
288
287
|
/* @__PURE__ */ S(k, {
|
|
289
288
|
active: N.hostnameKind === "auto",
|
|
290
|
-
onClick: () =>
|
|
291
|
-
disabled:
|
|
289
|
+
onClick: () => Y("hostnameKind", "auto"),
|
|
290
|
+
disabled: !$,
|
|
292
291
|
label: "Auto",
|
|
293
292
|
hint: "provider default"
|
|
294
293
|
})
|
|
@@ -301,12 +300,12 @@ function D() {
|
|
|
301
300
|
className: "flex gap-2",
|
|
302
301
|
children: [/* @__PURE__ */ S("button", {
|
|
303
302
|
type: "button",
|
|
304
|
-
onClick: () =>
|
|
303
|
+
onClick: () => Y("environment", c.Prod),
|
|
305
304
|
className: `rounded-md border px-4 py-2 text-sm ${N.environment === c.Prod ? "border-action-primary-bg bg-bg-sunken text-text-primary" : "border-border-subtle text-text-secondary hover:border-border-default"}`,
|
|
306
305
|
children: "Prod (vibetunnels.com)"
|
|
307
306
|
}), /* @__PURE__ */ S("button", {
|
|
308
307
|
type: "button",
|
|
309
|
-
onClick: () =>
|
|
308
|
+
onClick: () => Y("environment", c.Alpha),
|
|
310
309
|
className: `rounded-md border px-4 py-2 text-sm ${N.environment === c.Alpha ? "border-action-primary-bg bg-bg-sunken text-text-primary" : "border-border-subtle text-text-secondary hover:border-border-default"}`,
|
|
311
310
|
children: "Alpha"
|
|
312
311
|
})]
|
|
@@ -317,7 +316,7 @@ function D() {
|
|
|
317
316
|
children: /* @__PURE__ */ S("input", {
|
|
318
317
|
type: "text",
|
|
319
318
|
value: N.subdomain,
|
|
320
|
-
onChange: (e) =>
|
|
319
|
+
onChange: (e) => Y("subdomain", e.target.value.toLowerCase()),
|
|
321
320
|
placeholder: "demo",
|
|
322
321
|
className: "w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary"
|
|
323
322
|
})
|
|
@@ -339,7 +338,7 @@ function D() {
|
|
|
339
338
|
children: [/* @__PURE__ */ S("input", {
|
|
340
339
|
type: "text",
|
|
341
340
|
value: N.customDomain,
|
|
342
|
-
onChange: (e) =>
|
|
341
|
+
onChange: (e) => Y("customDomain", e.target.value.toLowerCase()),
|
|
343
342
|
placeholder: "preview.example.com",
|
|
344
343
|
className: "w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary"
|
|
345
344
|
}), /* @__PURE__ */ S("p", {
|
|
@@ -361,7 +360,7 @@ function D() {
|
|
|
361
360
|
children: /* @__PURE__ */ S("input", {
|
|
362
361
|
type: "text",
|
|
363
362
|
value: N.basicAuthUser,
|
|
364
|
-
onChange: (e) =>
|
|
363
|
+
onChange: (e) => Y("basicAuthUser", e.target.value),
|
|
365
364
|
className: "w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary"
|
|
366
365
|
})
|
|
367
366
|
}),
|
|
@@ -370,7 +369,7 @@ function D() {
|
|
|
370
369
|
children: /* @__PURE__ */ S("input", {
|
|
371
370
|
type: "password",
|
|
372
371
|
value: N.basicAuthPassword,
|
|
373
|
-
onChange: (e) =>
|
|
372
|
+
onChange: (e) => Y("basicAuthPassword", e.target.value),
|
|
374
373
|
className: "w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary"
|
|
375
374
|
})
|
|
376
375
|
})
|
|
@@ -453,9 +452,9 @@ function D() {
|
|
|
453
452
|
children: [/* @__PURE__ */ S("h4", {
|
|
454
453
|
className: "mb-2 text-sm font-medium text-text-secondary",
|
|
455
454
|
children: D("vibecontrols.tunnels.tags", "Tags")
|
|
456
|
-
}), /* @__PURE__ */ S(
|
|
455
|
+
}), /* @__PURE__ */ S(p, {
|
|
457
456
|
entityType: "VIBECONTROLS_TUNNEL",
|
|
458
|
-
onDraftChange:
|
|
457
|
+
onDraftChange: V
|
|
459
458
|
})]
|
|
460
459
|
}),
|
|
461
460
|
F && /* @__PURE__ */ S("div", {
|
|
@@ -472,28 +471,28 @@ function D() {
|
|
|
472
471
|
children: [/* @__PURE__ */ S("button", {
|
|
473
472
|
type: "button",
|
|
474
473
|
disabled: j === 0,
|
|
475
|
-
onClick:
|
|
474
|
+
onClick: re,
|
|
476
475
|
className: "rounded-md border border-border-default px-4 py-2 text-sm text-text-primary hover:bg-bg-elevated disabled:opacity-50",
|
|
477
476
|
children: D("vibecontrols.tunnels.back", "Back")
|
|
478
477
|
}), j < E.length - 1 ? /* @__PURE__ */ S("button", {
|
|
479
478
|
type: "button",
|
|
480
|
-
disabled: !
|
|
481
|
-
onClick:
|
|
479
|
+
disabled: !te,
|
|
480
|
+
onClick: ne,
|
|
482
481
|
className: "rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-fg hover:bg-action-primary-bg-hover disabled:opacity-50",
|
|
483
482
|
children: D("vibecontrols.tunnels.next", "Next")
|
|
484
483
|
}) : /* @__PURE__ */ S("button", {
|
|
485
484
|
type: "button",
|
|
486
|
-
disabled:
|
|
487
|
-
onClick: () => void
|
|
485
|
+
disabled: J || L,
|
|
486
|
+
onClick: () => void ie(),
|
|
488
487
|
className: "rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-fg hover:bg-action-primary-bg-hover disabled:opacity-50",
|
|
489
|
-
children:
|
|
488
|
+
children: J ? D("vibecontrols.tunnels.creating", "Creating...") : D("vibecontrols.tunnels.createTunnel", "Create tunnel")
|
|
490
489
|
})]
|
|
491
490
|
}),
|
|
492
491
|
/* @__PURE__ */ S(s, {
|
|
493
|
-
isOpen:
|
|
494
|
-
onClose: () =>
|
|
492
|
+
isOpen: z,
|
|
493
|
+
onClose: () => B(!1),
|
|
495
494
|
onConfirm: () => {
|
|
496
|
-
|
|
495
|
+
B(!1), A("/billing");
|
|
497
496
|
},
|
|
498
497
|
...d("tunnel-subdomain")
|
|
499
498
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TunnelCreateWizard.js","names":[],"sources":["../../../src/components/tunnels/TunnelCreateWizard.tsx"],"sourcesContent":["import { useState, useMemo, useCallback } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { useTr } from '../../shared/hooks/useTr';\n\nimport { PageHeader } from '@/components/shared/PageHeader';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { QuotaExhaustedDialog } from '@/components/shared/QuotaExhaustedDialog';\nimport {\n TunnelEnvironment,\n TunnelProtocol,\n TunnelProviderKey,\n type CreateTunnelInput,\n} from '@/generated/wspace-types';\nimport {\n useCreateTunnelMutation,\n useListTunnelProvidersQuery,\n useListAgentsQuery,\n} from '@/generated/wspace-operations';\nimport { getQuotaDialogContent, isQuotaExhaustedError } from '@/utils/quotaUtils';\nimport { EntityTagPicker } from '@/components/shared/EntityTagPicker';\nimport { useTagEntity } from '@/hooks/useEntityTags';\n\nconst SUBDOMAIN_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;\n\ntype HostnameKind = 'managed' | 'custom' | 'auto';\n\ninterface WizardState {\n providerKey: TunnelProviderKey | '';\n agentId: string;\n name: string;\n description: string;\n protocol: TunnelProtocol;\n localHost: string;\n localPort: number;\n hostnameKind: HostnameKind;\n subdomain: string;\n environment: TunnelEnvironment;\n customDomain: string;\n basicAuthUser: string;\n basicAuthPassword: string;\n}\n\nconst INITIAL_STATE: WizardState = {\n // Default to Vibe Tunnels as the preferred provider; users can still pick\n // Cloudflare Tunnels from the provider list.\n providerKey: TunnelProviderKey.Vibetunnels,\n agentId: '',\n name: '',\n description: '',\n protocol: TunnelProtocol.Http,\n localHost: '127.0.0.1',\n localPort: 3000,\n hostnameKind: 'managed',\n subdomain: '',\n environment: TunnelEnvironment.Prod,\n customDomain: '',\n basicAuthUser: '',\n basicAuthPassword: '',\n};\n\nconst STEPS = [\n { id: 'provider', label: 'Provider' },\n { id: 'target', label: 'Protocol & target' },\n { id: 'hostname', label: 'Hostname' },\n { id: 'policies', label: 'Policies' },\n { id: 'review', label: 'Review' },\n] as const;\n\nexport function TunnelCreateWizard() {\n const tr = useTr();\n const navigate = useNavigate();\n const [step, setStep] = useState(0);\n const [state, setState] = useState<WizardState>(INITIAL_STATE);\n const [submitError, setSubmitError] = useState<string | null>(null);\n const [showQuotaExhausted, setShowQuotaExhausted] = useState(false);\n // Tags selected before the tunnel exists — persisted right after create.\n const [draftTagIds, setDraftTagIds] = useState<string[]>([]);\n const [tagEntity] = useTagEntity();\n\n const {\n data: providersData,\n loading: providersLoading,\n error: providersError,\n } = useListTunnelProvidersQuery();\n const { data: agentsData } = useListAgentsQuery({ variables: { pagination: { limit: 100 } } });\n\n const [createTunnel, { loading: creating }] = useCreateTunnelMutation();\n\n const update = useCallback(<K extends keyof WizardState>(key: K, value: WizardState[K]) => {\n setState((prev) => ({ ...prev, [key]: value }));\n }, []);\n\n const selectProvider = useCallback((providerKey: TunnelProviderKey) => {\n setState((prev) => ({\n ...prev,\n providerKey,\n hostnameKind: providerKey === TunnelProviderKey.Cloudflare ? 'auto' : 'managed',\n }));\n }, []);\n\n // VibeTunnels first, Cloudflare second, everything else alphabetically.\n const orderedProviders = useMemo(\n () =>\n [...(providersData?.tunnelProviders ?? [])].sort((a: { key: string }, b: { key: string }) => {\n const rank = (key: string) => (key === 'VIBETUNNELS' ? 0 : key === 'CLOUDFLARE' ? 1 : 2);\n const diff = rank(a.key) - rank(b.key);\n if (diff !== 0) return diff;\n return a.key.localeCompare(b.key);\n }),\n [providersData]\n );\n\n const selectedProvider = useMemo(\n () => orderedProviders.find((p: { key: string }) => p.key === state.providerKey),\n [orderedProviders, state.providerKey]\n );\n\n const caps = selectedProvider?.capabilities;\n const supportsAutoHostname = state.providerKey === TunnelProviderKey.Cloudflare;\n const protocolOptions = useMemo(() => {\n if (!caps) return [TunnelProtocol.Http];\n const list: TunnelProtocol[] = [];\n if (caps.supportsHttp) list.push(TunnelProtocol.Http);\n if (caps.supportsHttps) list.push(TunnelProtocol.Https);\n if (caps.supportsTcp) list.push(TunnelProtocol.Tcp);\n return list;\n }, [caps]);\n\n const canAdvance = useMemo(() => {\n if (step === 0) return Boolean(state.providerKey && state.agentId && state.name.trim());\n if (step === 1) return state.localPort > 0 && state.localPort < 65536;\n if (step === 2) {\n if (state.hostnameKind === 'managed')\n return Boolean(state.subdomain && SUBDOMAIN_RE.test(state.subdomain));\n if (state.hostnameKind === 'custom') return state.customDomain.trim().includes('.');\n return supportsAutoHostname;\n }\n return true;\n }, [step, state, supportsAutoHostname]);\n\n const next = () => setStep((s) => Math.min(s + 1, STEPS.length - 1));\n const back = () => setStep((s) => Math.max(s - 1, 0));\n\n const submit = async () => {\n setSubmitError(null);\n try {\n const input: CreateTunnelInput = {\n agentId: state.agentId,\n name: state.name,\n description: state.description || undefined,\n providerKey: state.providerKey as TunnelProviderKey,\n protocol: state.protocol,\n localHost: state.localHost,\n localPort: state.localPort,\n environment: state.environment,\n basicAuthUser: state.basicAuthUser || undefined,\n basicAuthPassword: state.basicAuthPassword || undefined,\n managedSubdomain: state.hostnameKind === 'managed' ? state.subdomain : undefined,\n };\n const result = await createTunnel({ variables: { input } });\n\n if (result.error) {\n if (isQuotaExhaustedError(result.error)) {\n setShowQuotaExhausted(true);\n return;\n }\n\n setSubmitError(result.error.message);\n return;\n }\n\n const id = result.data?.createTunnel.id;\n if (id) {\n // Persist any tags chosen during creation. Best-effort: the tunnel\n // already exists, so a tag failure must not block navigation —\n // the detail page picker lets the user retry.\n if (draftTagIds.length > 0) {\n try {\n await Promise.all(\n draftTagIds.map((tagId) =>\n tagEntity({\n variables: {\n input: { entityType: 'VIBECONTROLS_TUNNEL', entityId: id, tagId },\n },\n })\n )\n );\n } catch {\n // Swallow — tunnel creation succeeded; tags can be added later.\n }\n }\n navigate(`/vibecontrols/tunnels/${id}`);\n }\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n setSubmitError(null);\n setShowQuotaExhausted(true);\n return;\n }\n\n setSubmitError(err instanceof Error ? err.message : String(err));\n }\n };\n\n if (providersLoading) return <LoadingState message=\"Loading providers\" />;\n if (providersError) {\n return (\n <ErrorState\n title=\"Failed to load providers\"\n message={providersError.message}\n onRetry={() => window.location.reload()}\n />\n );\n }\n\n return (\n <div className=\"p-6 max-w-3xl mx-auto\">\n <PageHeader\n breadcrumb={[{ label: 'Tunnels', path: '/vibecontrols/tunnels' }, { label: 'New tunnel' }]}\n title={tr('vibecontrols.tunnels.createTitle', 'Create tunnel')}\n description={tr(\n 'vibecontrols.tunnels.createDescription',\n 'Expose a local service through a managed tunnel.'\n )}\n />\n\n <div className=\"mb-6 flex items-center gap-2\">\n {STEPS.map((s, i) => (\n <div\n key={s.id}\n className={`flex-1 h-1.5 rounded-full ${\n i <= step ? 'bg-action-primary-bg' : 'bg-bg-sunken'\n }`}\n aria-label={`Step ${i + 1}: ${s.label}`}\n />\n ))}\n </div>\n\n <AnimatePresence mode=\"wait\">\n <motion.div\n key={step}\n initial={{ opacity: 0, x: 6 }}\n animate={{ opacity: 1, x: 0 }}\n exit={{ opacity: 0, x: -6 }}\n transition={{ duration: 0.15 }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface p-6\"\n >\n {step === 0 && (\n <div className=\"space-y-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Provider</h3>\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {orderedProviders.map(\n (p: {\n id: string;\n key: TunnelProviderKey;\n displayName: string;\n description?: string | null;\n }) => (\n <button\n key={p.id}\n type=\"button\"\n onClick={() => selectProvider(p.key)}\n className={`rounded-lg border p-4 text-left transition-colors ${\n state.providerKey === p.key\n ? 'border-action-primary-bg bg-bg-sunken'\n : 'border-border-subtle hover:border-border-default'\n }`}\n >\n <div className=\"font-semibold text-text-primary\">{p.displayName}</div>\n <div className=\"text-xs text-text-muted mt-1\">{p.key}</div>\n {p.description && (\n <div className=\"text-xs text-text-secondary mt-2\">{p.description}</div>\n )}\n </button>\n )\n )}\n </div>\n <LabeledField label=\"Agent\">\n <select\n value={state.agentId}\n onChange={(e) => update('agentId', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n >\n <option value=\"\">Select agent</option>\n {(agentsData?.vibecontrolsAgents ?? []).map((a: { id: string; name: string }) => (\n <option key={a.id} value={a.id}>\n {a.name}\n </option>\n ))}\n </select>\n </LabeledField>\n <LabeledField label=\"Name\">\n <input\n type=\"text\"\n value={state.name}\n onChange={(e) => update('name', e.target.value)}\n placeholder=\"My dev tunnel\"\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-muted\"\n />\n </LabeledField>\n <LabeledField label=\"Description (optional)\">\n <input\n type=\"text\"\n value={state.description}\n onChange={(e) => update('description', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-muted\"\n />\n </LabeledField>\n </div>\n )}\n\n {step === 1 && (\n <div className=\"space-y-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Protocol & target</h3>\n <LabeledField label=\"Protocol\">\n <div className=\"flex gap-2\">\n {protocolOptions.map((proto) => (\n <button\n key={proto}\n type=\"button\"\n onClick={() => update('protocol', proto)}\n className={`rounded-md border px-4 py-2 text-sm ${\n state.protocol === proto\n ? 'border-action-primary-bg bg-bg-sunken text-text-primary'\n : 'border-border-subtle text-text-secondary hover:border-border-default'\n }`}\n >\n {proto}\n </button>\n ))}\n </div>\n </LabeledField>\n <LabeledField label=\"Local host\">\n <input\n type=\"text\"\n value={state.localHost}\n onChange={(e) => update('localHost', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n <LabeledField label=\"Local port\">\n <input\n type=\"number\"\n value={state.localPort}\n onChange={(e) => update('localPort', parseInt(e.target.value, 10) || 0)}\n min={1}\n max={65535}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n </div>\n )}\n\n {step === 2 && (\n <div className=\"space-y-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Hostname</h3>\n <div className=\"flex gap-2\">\n <HostnameKindButton\n active={state.hostnameKind === 'managed'}\n onClick={() => update('hostnameKind', 'managed')}\n disabled={!caps?.supportsManagedSubdomains}\n label=\"Managed subdomain\"\n hint=\"*.vibetunnels.com\"\n />\n <HostnameKindButton\n active={state.hostnameKind === 'custom'}\n onClick={() => update('hostnameKind', 'custom')}\n disabled={!caps?.supportsCustomDomains}\n label=\"Custom domain\"\n hint=\"TXT verification\"\n />\n <HostnameKindButton\n active={state.hostnameKind === 'auto'}\n onClick={() => update('hostnameKind', 'auto')}\n disabled={!supportsAutoHostname}\n label=\"Auto\"\n hint=\"provider default\"\n />\n </div>\n\n {state.hostnameKind === 'managed' && (\n <>\n <LabeledField label=\"Environment\">\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => update('environment', TunnelEnvironment.Prod)}\n className={`rounded-md border px-4 py-2 text-sm ${\n state.environment === TunnelEnvironment.Prod\n ? 'border-action-primary-bg bg-bg-sunken text-text-primary'\n : 'border-border-subtle text-text-secondary hover:border-border-default'\n }`}\n >\n Prod (vibetunnels.com)\n </button>\n <button\n type=\"button\"\n onClick={() => update('environment', TunnelEnvironment.Alpha)}\n className={`rounded-md border px-4 py-2 text-sm ${\n state.environment === TunnelEnvironment.Alpha\n ? 'border-action-primary-bg bg-bg-sunken text-text-primary'\n : 'border-border-subtle text-text-secondary hover:border-border-default'\n }`}\n >\n Alpha\n </button>\n </div>\n </LabeledField>\n <LabeledField label=\"Subdomain label\">\n <input\n type=\"text\"\n value={state.subdomain}\n onChange={(e) => update('subdomain', e.target.value.toLowerCase())}\n placeholder=\"demo\"\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n {state.subdomain && (\n <p className=\"text-xs text-text-secondary\">\n Will be reserved as{' '}\n <span className=\"font-mono text-text-primary\">\n {state.subdomain}\n {state.environment === TunnelEnvironment.Alpha\n ? '.alpha.vibetunnels.com'\n : '.vibetunnels.com'}\n </span>\n </p>\n )}\n </>\n )}\n\n {state.hostnameKind === 'custom' && (\n <LabeledField label=\"Custom domain\">\n <input\n type=\"text\"\n value={state.customDomain}\n onChange={(e) => update('customDomain', e.target.value.toLowerCase())}\n placeholder=\"preview.example.com\"\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n <p className=\"text-xs text-text-muted mt-2\">\n Custom domains are registered on create but must be verified via a TXT record\n before they can be attached. You'll see the verification challenge in the\n Domains tab after creation.\n </p>\n </LabeledField>\n )}\n </div>\n )}\n\n {step === 3 && (\n <div className=\"space-y-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Policies (optional)</h3>\n <LabeledField label=\"Basic auth user\">\n <input\n type=\"text\"\n value={state.basicAuthUser}\n onChange={(e) => update('basicAuthUser', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n <LabeledField label=\"Basic auth password\">\n <input\n type=\"password\"\n value={state.basicAuthPassword}\n onChange={(e) => update('basicAuthPassword', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n </div>\n )}\n\n {step === 4 && (\n <div className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {tr('vibecontrols.tunnels.review', 'Review')}\n </h3>\n <dl className=\"grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm\">\n <dt className=\"text-text-muted\">Provider</dt>\n <dd className=\"text-text-primary\">{state.providerKey}</dd>\n <dt className=\"text-text-muted\">Name</dt>\n <dd className=\"text-text-primary\">{state.name}</dd>\n <dt className=\"text-text-muted\">Agent</dt>\n <dd className=\"text-text-primary font-mono\">{state.agentId}</dd>\n <dt className=\"text-text-muted\">Protocol</dt>\n <dd className=\"text-text-primary\">{state.protocol}</dd>\n <dt className=\"text-text-muted\">Target</dt>\n <dd className=\"text-text-primary font-mono\">\n {state.localHost}:{state.localPort}\n </dd>\n <dt className=\"text-text-muted\">Hostname</dt>\n <dd className=\"text-text-primary\">\n {state.hostnameKind === 'managed'\n ? `${state.subdomain}.${state.environment === 'ALPHA' ? 'alpha.' : ''}vibetunnels.com`\n : state.hostnameKind === 'custom'\n ? state.customDomain\n : 'auto-assigned'}\n </dd>\n <dt className=\"text-text-muted\">Basic auth</dt>\n <dd className=\"text-text-primary\">{state.basicAuthUser ? 'yes' : 'no'}</dd>\n </dl>\n <div className=\"border-t border-border-subtle pt-3\">\n <h4 className=\"mb-2 text-sm font-medium text-text-secondary\">\n {tr('vibecontrols.tunnels.tags', 'Tags')}\n </h4>\n <EntityTagPicker entityType=\"VIBECONTROLS_TUNNEL\" onDraftChange={setDraftTagIds} />\n </div>\n {submitError && (\n <div className=\"rounded-md bg-status-error-bg p-3 text-sm text-status-error-fg\">\n {submitError}\n </div>\n )}\n </div>\n )}\n </motion.div>\n </AnimatePresence>\n\n <div className=\"flex items-center justify-between mt-4\">\n <button\n type=\"button\"\n disabled={step === 0}\n onClick={back}\n className=\"rounded-md border border-border-default px-4 py-2 text-sm text-text-primary hover:bg-bg-elevated disabled:opacity-50\"\n >\n {tr('vibecontrols.tunnels.back', 'Back')}\n </button>\n {step < STEPS.length - 1 ? (\n <button\n type=\"button\"\n disabled={!canAdvance}\n onClick={next}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-fg hover:bg-action-primary-bg-hover disabled:opacity-50\"\n >\n {tr('vibecontrols.tunnels.next', 'Next')}\n </button>\n ) : (\n <button\n type=\"button\"\n disabled={creating}\n onClick={() => void submit()}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-fg hover:bg-action-primary-bg-hover disabled:opacity-50\"\n >\n {creating\n ? tr('vibecontrols.tunnels.creating', 'Creating...')\n : tr('vibecontrols.tunnels.createTunnel', 'Create tunnel')}\n </button>\n )}\n </div>\n\n <QuotaExhaustedDialog\n isOpen={showQuotaExhausted}\n onClose={() => setShowQuotaExhausted(false)}\n onConfirm={() => {\n setShowQuotaExhausted(false);\n navigate('/billing');\n }}\n {...getQuotaDialogContent('tunnel-subdomain')}\n />\n </div>\n );\n}\n\nfunction LabeledField({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <label className=\"block\">\n <span className=\"text-xs font-medium text-text-secondary\">{label}</span>\n <div className=\"mt-1\">{children}</div>\n </label>\n );\n}\n\nfunction HostnameKindButton({\n active,\n disabled,\n onClick,\n label,\n hint,\n}: {\n active: boolean;\n disabled?: boolean;\n onClick: () => void;\n label: string;\n hint: string;\n}) {\n return (\n <button\n type=\"button\"\n disabled={disabled}\n onClick={onClick}\n className={`flex-1 rounded-md border p-3 text-left transition-colors ${\n active\n ? 'border-action-primary-bg bg-bg-sunken'\n : 'border-border-subtle hover:border-border-default'\n } ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}\n >\n <div className=\"text-sm font-medium text-text-primary\">{label}</div>\n <div className=\"text-xs text-text-muted mt-0.5\">{hint}</div>\n </button>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TunnelCreateWizard.js","names":[],"sources":["../../../src/components/tunnels/TunnelCreateWizard.tsx"],"sourcesContent":["import { useState, useMemo, useCallback } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { useTr } from '../../shared/hooks/useTr';\n\nimport { PageHeader } from '@/components/shared/PageHeader';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { QuotaExhaustedDialog } from '@/components/shared/QuotaExhaustedDialog';\nimport {\n TunnelEnvironment,\n TunnelProtocol,\n TunnelProviderKey,\n type CreateTunnelInput,\n} from '@/generated/wspace-types';\nimport {\n useCreateTunnelMutation,\n useListTunnelProvidersQuery,\n useListAgentsQuery,\n} from '@/generated/wspace-operations';\nimport { getQuotaDialogContent, isQuotaExhaustedError } from '@/utils/quotaUtils';\nimport { EntityTagPicker } from '@/components/shared/EntityTagPicker';\nimport { useDraftEntityTags } from '@/hooks/useDraftEntityTags';\n\nconst SUBDOMAIN_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;\n\ntype HostnameKind = 'managed' | 'custom' | 'auto';\n\ninterface WizardState {\n providerKey: TunnelProviderKey | '';\n agentId: string;\n name: string;\n description: string;\n protocol: TunnelProtocol;\n localHost: string;\n localPort: number;\n hostnameKind: HostnameKind;\n subdomain: string;\n environment: TunnelEnvironment;\n customDomain: string;\n basicAuthUser: string;\n basicAuthPassword: string;\n}\n\nconst INITIAL_STATE: WizardState = {\n // Default to Vibe Tunnels as the preferred provider; users can still pick\n // Cloudflare Tunnels from the provider list.\n providerKey: TunnelProviderKey.Vibetunnels,\n agentId: '',\n name: '',\n description: '',\n protocol: TunnelProtocol.Http,\n localHost: '127.0.0.1',\n localPort: 3000,\n hostnameKind: 'managed',\n subdomain: '',\n environment: TunnelEnvironment.Prod,\n customDomain: '',\n basicAuthUser: '',\n basicAuthPassword: '',\n};\n\nconst STEPS = [\n { id: 'provider', label: 'Provider' },\n { id: 'target', label: 'Protocol & target' },\n { id: 'hostname', label: 'Hostname' },\n { id: 'policies', label: 'Policies' },\n { id: 'review', label: 'Review' },\n] as const;\n\nexport function TunnelCreateWizard() {\n const tr = useTr();\n const navigate = useNavigate();\n const [step, setStep] = useState(0);\n const [state, setState] = useState<WizardState>(INITIAL_STATE);\n const [submitError, setSubmitError] = useState<string | null>(null);\n const [isFlushingTags, setIsFlushingTags] = useState(false);\n const [showQuotaExhausted, setShowQuotaExhausted] = useState(false);\n // Tags selected before the tunnel exists — persisted right after create.\n const { setDraftTagIds, flushTags } = useDraftEntityTags('VIBECONTROLS_TUNNEL');\n\n const {\n data: providersData,\n loading: providersLoading,\n error: providersError,\n } = useListTunnelProvidersQuery();\n const { data: agentsData } = useListAgentsQuery({ variables: { pagination: { limit: 100 } } });\n\n const [createTunnel, { loading: creating }] = useCreateTunnelMutation();\n\n const update = useCallback(<K extends keyof WizardState>(key: K, value: WizardState[K]) => {\n setState((prev) => ({ ...prev, [key]: value }));\n }, []);\n\n const selectProvider = useCallback((providerKey: TunnelProviderKey) => {\n setState((prev) => ({\n ...prev,\n providerKey,\n hostnameKind: providerKey === TunnelProviderKey.Cloudflare ? 'auto' : 'managed',\n }));\n }, []);\n\n // VibeTunnels first, Cloudflare second, everything else alphabetically.\n const orderedProviders = useMemo(\n () =>\n [...(providersData?.tunnelProviders ?? [])].sort((a: { key: string }, b: { key: string }) => {\n const rank = (key: string) => (key === 'VIBETUNNELS' ? 0 : key === 'CLOUDFLARE' ? 1 : 2);\n const diff = rank(a.key) - rank(b.key);\n if (diff !== 0) return diff;\n return a.key.localeCompare(b.key);\n }),\n [providersData]\n );\n\n const selectedProvider = useMemo(\n () => orderedProviders.find((p: { key: string }) => p.key === state.providerKey),\n [orderedProviders, state.providerKey]\n );\n\n const caps = selectedProvider?.capabilities;\n const supportsAutoHostname = state.providerKey === TunnelProviderKey.Cloudflare;\n const protocolOptions = useMemo(() => {\n if (!caps) return [TunnelProtocol.Http];\n const list: TunnelProtocol[] = [];\n if (caps.supportsHttp) list.push(TunnelProtocol.Http);\n if (caps.supportsHttps) list.push(TunnelProtocol.Https);\n if (caps.supportsTcp) list.push(TunnelProtocol.Tcp);\n return list;\n }, [caps]);\n\n const canAdvance = useMemo(() => {\n if (step === 0) return Boolean(state.providerKey && state.agentId && state.name.trim());\n if (step === 1) return state.localPort > 0 && state.localPort < 65536;\n if (step === 2) {\n if (state.hostnameKind === 'managed')\n return Boolean(state.subdomain && SUBDOMAIN_RE.test(state.subdomain));\n if (state.hostnameKind === 'custom') return state.customDomain.trim().includes('.');\n return supportsAutoHostname;\n }\n return true;\n }, [step, state, supportsAutoHostname]);\n\n const next = () => setStep((s) => Math.min(s + 1, STEPS.length - 1));\n const back = () => setStep((s) => Math.max(s - 1, 0));\n\n const submit = async () => {\n setSubmitError(null);\n try {\n const input: CreateTunnelInput = {\n agentId: state.agentId,\n name: state.name,\n description: state.description || undefined,\n providerKey: state.providerKey as TunnelProviderKey,\n protocol: state.protocol,\n localHost: state.localHost,\n localPort: state.localPort,\n environment: state.environment,\n basicAuthUser: state.basicAuthUser || undefined,\n basicAuthPassword: state.basicAuthPassword || undefined,\n managedSubdomain: state.hostnameKind === 'managed' ? state.subdomain : undefined,\n };\n const result = await createTunnel({ variables: { input } });\n\n if (result.error) {\n if (isQuotaExhaustedError(result.error)) {\n setShowQuotaExhausted(true);\n return;\n }\n\n setSubmitError(result.error.message);\n return;\n }\n\n const id = result.data?.createTunnel.id;\n if (id) {\n setIsFlushingTags(true);\n try {\n await flushTags(id);\n } finally {\n setIsFlushingTags(false);\n }\n navigate(`/vibecontrols/tunnels/${id}`);\n }\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n setSubmitError(null);\n setShowQuotaExhausted(true);\n return;\n }\n\n setSubmitError(err instanceof Error ? err.message : String(err));\n }\n };\n\n if (providersLoading) return <LoadingState message=\"Loading providers\" />;\n if (providersError) {\n return (\n <ErrorState\n title=\"Failed to load providers\"\n message={providersError.message}\n onRetry={() => window.location.reload()}\n />\n );\n }\n\n return (\n <div className=\"p-6 max-w-3xl mx-auto\">\n <PageHeader\n breadcrumb={[{ label: 'Tunnels', path: '/vibecontrols/tunnels' }, { label: 'New tunnel' }]}\n title={tr('vibecontrols.tunnels.createTitle', 'Create tunnel')}\n description={tr(\n 'vibecontrols.tunnels.createDescription',\n 'Expose a local service through a managed tunnel.'\n )}\n />\n\n <div className=\"mb-6 flex items-center gap-2\">\n {STEPS.map((s, i) => (\n <div\n key={s.id}\n className={`flex-1 h-1.5 rounded-full ${\n i <= step ? 'bg-action-primary-bg' : 'bg-bg-sunken'\n }`}\n aria-label={`Step ${i + 1}: ${s.label}`}\n />\n ))}\n </div>\n\n <AnimatePresence mode=\"wait\">\n <motion.div\n key={step}\n initial={{ opacity: 0, x: 6 }}\n animate={{ opacity: 1, x: 0 }}\n exit={{ opacity: 0, x: -6 }}\n transition={{ duration: 0.15 }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface p-6\"\n >\n {step === 0 && (\n <div className=\"space-y-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Provider</h3>\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {orderedProviders.map(\n (p: {\n id: string;\n key: TunnelProviderKey;\n displayName: string;\n description?: string | null;\n }) => (\n <button\n key={p.id}\n type=\"button\"\n onClick={() => selectProvider(p.key)}\n className={`rounded-lg border p-4 text-left transition-colors ${\n state.providerKey === p.key\n ? 'border-action-primary-bg bg-bg-sunken'\n : 'border-border-subtle hover:border-border-default'\n }`}\n >\n <div className=\"font-semibold text-text-primary\">{p.displayName}</div>\n <div className=\"text-xs text-text-muted mt-1\">{p.key}</div>\n {p.description && (\n <div className=\"text-xs text-text-secondary mt-2\">{p.description}</div>\n )}\n </button>\n )\n )}\n </div>\n <LabeledField label=\"Agent\">\n <select\n value={state.agentId}\n onChange={(e) => update('agentId', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n >\n <option value=\"\">Select agent</option>\n {(agentsData?.vibecontrolsAgents ?? []).map((a: { id: string; name: string }) => (\n <option key={a.id} value={a.id}>\n {a.name}\n </option>\n ))}\n </select>\n </LabeledField>\n <LabeledField label=\"Name\">\n <input\n type=\"text\"\n value={state.name}\n onChange={(e) => update('name', e.target.value)}\n placeholder=\"My dev tunnel\"\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-muted\"\n />\n </LabeledField>\n <LabeledField label=\"Description (optional)\">\n <input\n type=\"text\"\n value={state.description}\n onChange={(e) => update('description', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-muted\"\n />\n </LabeledField>\n </div>\n )}\n\n {step === 1 && (\n <div className=\"space-y-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Protocol & target</h3>\n <LabeledField label=\"Protocol\">\n <div className=\"flex gap-2\">\n {protocolOptions.map((proto) => (\n <button\n key={proto}\n type=\"button\"\n onClick={() => update('protocol', proto)}\n className={`rounded-md border px-4 py-2 text-sm ${\n state.protocol === proto\n ? 'border-action-primary-bg bg-bg-sunken text-text-primary'\n : 'border-border-subtle text-text-secondary hover:border-border-default'\n }`}\n >\n {proto}\n </button>\n ))}\n </div>\n </LabeledField>\n <LabeledField label=\"Local host\">\n <input\n type=\"text\"\n value={state.localHost}\n onChange={(e) => update('localHost', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n <LabeledField label=\"Local port\">\n <input\n type=\"number\"\n value={state.localPort}\n onChange={(e) => update('localPort', parseInt(e.target.value, 10) || 0)}\n min={1}\n max={65535}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n </div>\n )}\n\n {step === 2 && (\n <div className=\"space-y-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Hostname</h3>\n <div className=\"flex gap-2\">\n <HostnameKindButton\n active={state.hostnameKind === 'managed'}\n onClick={() => update('hostnameKind', 'managed')}\n disabled={!caps?.supportsManagedSubdomains}\n label=\"Managed subdomain\"\n hint=\"*.vibetunnels.com\"\n />\n <HostnameKindButton\n active={state.hostnameKind === 'custom'}\n onClick={() => update('hostnameKind', 'custom')}\n disabled={!caps?.supportsCustomDomains}\n label=\"Custom domain\"\n hint=\"TXT verification\"\n />\n <HostnameKindButton\n active={state.hostnameKind === 'auto'}\n onClick={() => update('hostnameKind', 'auto')}\n disabled={!supportsAutoHostname}\n label=\"Auto\"\n hint=\"provider default\"\n />\n </div>\n\n {state.hostnameKind === 'managed' && (\n <>\n <LabeledField label=\"Environment\">\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => update('environment', TunnelEnvironment.Prod)}\n className={`rounded-md border px-4 py-2 text-sm ${\n state.environment === TunnelEnvironment.Prod\n ? 'border-action-primary-bg bg-bg-sunken text-text-primary'\n : 'border-border-subtle text-text-secondary hover:border-border-default'\n }`}\n >\n Prod (vibetunnels.com)\n </button>\n <button\n type=\"button\"\n onClick={() => update('environment', TunnelEnvironment.Alpha)}\n className={`rounded-md border px-4 py-2 text-sm ${\n state.environment === TunnelEnvironment.Alpha\n ? 'border-action-primary-bg bg-bg-sunken text-text-primary'\n : 'border-border-subtle text-text-secondary hover:border-border-default'\n }`}\n >\n Alpha\n </button>\n </div>\n </LabeledField>\n <LabeledField label=\"Subdomain label\">\n <input\n type=\"text\"\n value={state.subdomain}\n onChange={(e) => update('subdomain', e.target.value.toLowerCase())}\n placeholder=\"demo\"\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n {state.subdomain && (\n <p className=\"text-xs text-text-secondary\">\n Will be reserved as{' '}\n <span className=\"font-mono text-text-primary\">\n {state.subdomain}\n {state.environment === TunnelEnvironment.Alpha\n ? '.alpha.vibetunnels.com'\n : '.vibetunnels.com'}\n </span>\n </p>\n )}\n </>\n )}\n\n {state.hostnameKind === 'custom' && (\n <LabeledField label=\"Custom domain\">\n <input\n type=\"text\"\n value={state.customDomain}\n onChange={(e) => update('customDomain', e.target.value.toLowerCase())}\n placeholder=\"preview.example.com\"\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n <p className=\"text-xs text-text-muted mt-2\">\n Custom domains are registered on create but must be verified via a TXT record\n before they can be attached. You'll see the verification challenge in the\n Domains tab after creation.\n </p>\n </LabeledField>\n )}\n </div>\n )}\n\n {step === 3 && (\n <div className=\"space-y-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Policies (optional)</h3>\n <LabeledField label=\"Basic auth user\">\n <input\n type=\"text\"\n value={state.basicAuthUser}\n onChange={(e) => update('basicAuthUser', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n <LabeledField label=\"Basic auth password\">\n <input\n type=\"password\"\n value={state.basicAuthPassword}\n onChange={(e) => update('basicAuthPassword', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n </div>\n )}\n\n {step === 4 && (\n <div className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {tr('vibecontrols.tunnels.review', 'Review')}\n </h3>\n <dl className=\"grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm\">\n <dt className=\"text-text-muted\">Provider</dt>\n <dd className=\"text-text-primary\">{state.providerKey}</dd>\n <dt className=\"text-text-muted\">Name</dt>\n <dd className=\"text-text-primary\">{state.name}</dd>\n <dt className=\"text-text-muted\">Agent</dt>\n <dd className=\"text-text-primary font-mono\">{state.agentId}</dd>\n <dt className=\"text-text-muted\">Protocol</dt>\n <dd className=\"text-text-primary\">{state.protocol}</dd>\n <dt className=\"text-text-muted\">Target</dt>\n <dd className=\"text-text-primary font-mono\">\n {state.localHost}:{state.localPort}\n </dd>\n <dt className=\"text-text-muted\">Hostname</dt>\n <dd className=\"text-text-primary\">\n {state.hostnameKind === 'managed'\n ? `${state.subdomain}.${state.environment === 'ALPHA' ? 'alpha.' : ''}vibetunnels.com`\n : state.hostnameKind === 'custom'\n ? state.customDomain\n : 'auto-assigned'}\n </dd>\n <dt className=\"text-text-muted\">Basic auth</dt>\n <dd className=\"text-text-primary\">{state.basicAuthUser ? 'yes' : 'no'}</dd>\n </dl>\n <div className=\"border-t border-border-subtle pt-3\">\n <h4 className=\"mb-2 text-sm font-medium text-text-secondary\">\n {tr('vibecontrols.tunnels.tags', 'Tags')}\n </h4>\n <EntityTagPicker entityType=\"VIBECONTROLS_TUNNEL\" onDraftChange={setDraftTagIds} />\n </div>\n {submitError && (\n <div className=\"rounded-md bg-status-error-bg p-3 text-sm text-status-error-fg\">\n {submitError}\n </div>\n )}\n </div>\n )}\n </motion.div>\n </AnimatePresence>\n\n <div className=\"flex items-center justify-between mt-4\">\n <button\n type=\"button\"\n disabled={step === 0}\n onClick={back}\n className=\"rounded-md border border-border-default px-4 py-2 text-sm text-text-primary hover:bg-bg-elevated disabled:opacity-50\"\n >\n {tr('vibecontrols.tunnels.back', 'Back')}\n </button>\n {step < STEPS.length - 1 ? (\n <button\n type=\"button\"\n disabled={!canAdvance}\n onClick={next}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-fg hover:bg-action-primary-bg-hover disabled:opacity-50\"\n >\n {tr('vibecontrols.tunnels.next', 'Next')}\n </button>\n ) : (\n <button\n type=\"button\"\n disabled={creating || isFlushingTags}\n onClick={() => void submit()}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-fg hover:bg-action-primary-bg-hover disabled:opacity-50\"\n >\n {creating\n ? tr('vibecontrols.tunnels.creating', 'Creating...')\n : tr('vibecontrols.tunnels.createTunnel', 'Create tunnel')}\n </button>\n )}\n </div>\n\n <QuotaExhaustedDialog\n isOpen={showQuotaExhausted}\n onClose={() => setShowQuotaExhausted(false)}\n onConfirm={() => {\n setShowQuotaExhausted(false);\n navigate('/billing');\n }}\n {...getQuotaDialogContent('tunnel-subdomain')}\n />\n </div>\n );\n}\n\nfunction LabeledField({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <label className=\"block\">\n <span className=\"text-xs font-medium text-text-secondary\">{label}</span>\n <div className=\"mt-1\">{children}</div>\n </label>\n );\n}\n\nfunction HostnameKindButton({\n active,\n disabled,\n onClick,\n label,\n hint,\n}: {\n active: boolean;\n disabled?: boolean;\n onClick: () => void;\n label: string;\n hint: string;\n}) {\n return (\n <button\n type=\"button\"\n disabled={disabled}\n onClick={onClick}\n className={`flex-1 rounded-md border p-3 text-left transition-colors ${\n active\n ? 'border-action-primary-bg bg-bg-sunken'\n : 'border-border-subtle hover:border-border-default'\n } ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}\n >\n <div className=\"text-sm font-medium text-text-primary\">{label}</div>\n <div className=\"text-xs text-text-muted mt-0.5\">{hint}</div>\n </button>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useTagEntity as e } from "./useEntityTags.js";
|
|
2
|
+
import { useCallback as t, useState as n } from "react";
|
|
3
|
+
//#region src/hooks/useDraftEntityTags.ts
|
|
4
|
+
function r(r) {
|
|
5
|
+
let [i, a] = n([]), [o] = e();
|
|
6
|
+
return {
|
|
7
|
+
draftTagIds: i,
|
|
8
|
+
setDraftTagIds: a,
|
|
9
|
+
flushTags: t(async (e) => {
|
|
10
|
+
if (!(!e || i.length === 0)) try {
|
|
11
|
+
await Promise.allSettled(i.map((t) => o({ variables: { input: {
|
|
12
|
+
entityType: r,
|
|
13
|
+
entityId: e,
|
|
14
|
+
tagId: t
|
|
15
|
+
} } })));
|
|
16
|
+
} finally {
|
|
17
|
+
a([]);
|
|
18
|
+
}
|
|
19
|
+
}, [
|
|
20
|
+
i,
|
|
21
|
+
r,
|
|
22
|
+
o
|
|
23
|
+
])
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { r as useDraftEntityTags };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=useDraftEntityTags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDraftEntityTags.js","names":[],"sources":["../../src/hooks/useDraftEntityTags.ts"],"sourcesContent":["import { useState, useCallback } from 'react';\n\nimport { useTagEntity } from './useEntityTags';\n\nexport type VibecontrolsEntityType =\n | 'VIBECONTROLS_NOTE'\n | 'VIBECONTROLS_VIBE'\n | 'VIBECONTROLS_SESSION'\n | 'VIBECONTROLS_AGENT'\n | 'VIBECONTROLS_TARGET'\n | 'VIBECONTROLS_VIBEDECK'\n | 'VIBECONTROLS_DOCSSITE'\n | 'VIBECONTROLS_DOCSPAGE'\n | 'VIBECONTROLS_TUNNEL'\n | 'VIBECONTROLS_TUNNELDOMAIN'\n | 'VIBECONTROLS_CATALOG'\n | 'VIBECONTROLS_COMPONENT'\n | 'VIBECONTROLS_TEMPLATE'\n | 'VIBECONTROLS_WEBHOOK'\n | 'VIBECONTROLS_SCORECARD'\n | 'VIBECONTROLS_GITOPSPROVIDER'\n | 'VIBECONTROLS_CONFIGURATION'\n | 'VIBECONTROLS_AIPROMPT'\n | 'VIBECONTROLS_UISESSION'\n | 'VIBECONTROLS_ACTION';\n\nexport function useDraftEntityTags(entityType: VibecontrolsEntityType) {\n const [draftTagIds, setDraftTagIds] = useState<string[]>([]);\n const [tagEntity] = useTagEntity();\n\n const flushTags = useCallback(\n async (entityId: string): Promise<void> => {\n if (!entityId || draftTagIds.length === 0) return;\n try {\n await Promise.allSettled(\n draftTagIds.map((tagId) =>\n tagEntity({ variables: { input: { entityType, entityId, tagId } } })\n )\n );\n } finally {\n setDraftTagIds([]);\n }\n },\n [draftTagIds, entityType, tagEntity]\n );\n\n return { draftTagIds, setDraftTagIds, flushTags };\n}\n"],"mappings":";;;AA0BA,SAAgB,EAAmB,GAAoC;CACrE,IAAM,CAAC,GAAa,KAAkB,EAAmB,EAAE,CAAC,EACtD,CAAC,KAAa,GAAc;AAkBlC,QAAO;EAAE;EAAa;EAAgB,WAhBpB,EAChB,OAAO,MAAoC;AACrC,UAAC,KAAY,EAAY,WAAW,GACxC,KAAI;AACF,UAAM,QAAQ,WACZ,EAAY,KAAK,MACf,EAAU,EAAE,WAAW,EAAE,OAAO;KAAE;KAAY;KAAU;KAAO,EAAE,EAAE,CAAC,CACrE,CACF;aACO;AACR,MAAe,EAAE,CAAC;;KAGtB;GAAC;GAAa;GAAY;GAAU,CACrC;EAEgD"}
|