@burdenoff/microfe-vibecontrols 2026.613.1 → 2026.618.1
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/AgentConnectionRecovery.js +100 -94
- package/dist/components/agents/AgentConnectionRecovery.js.map +1 -1
- package/dist/components/agents/AgentMaintenanceTab.js +177 -160
- package/dist/components/agents/AgentMaintenanceTab.js.map +1 -1
- package/dist/components/agents/OpenInBrowserEditorButton.js +102 -94
- package/dist/components/agents/OpenInBrowserEditorButton.js.map +1 -1
- package/dist/components/assistant/AssistantChatArea.js +1 -0
- package/dist/components/assistant/AssistantChatArea.js.map +1 -1
- package/dist/components/assistant/AssistantModeSelector.js +20 -19
- package/dist/components/assistant/AssistantModeSelector.js.map +1 -1
- package/dist/components/tunnels/TunnelCard.js +3 -3
- package/dist/components/tunnels/TunnelCard.js.map +1 -1
- package/dist/components/tunnels/TunnelPublicAccessPanel.js +36 -36
- package/dist/components/tunnels/TunnelPublicAccessPanel.js.map +1 -1
- package/dist/config/assistantUsecases.js +11 -0
- package/dist/config/assistantUsecases.js.map +1 -1
- package/dist/constants/permissions.js +38 -6
- package/dist/constants/permissions.js.map +1 -1
- package/dist/index.js +4 -3
- package/dist/pages/CatalogDetailPage.js +155 -155
- package/dist/pages/CatalogDetailPage.js.map +1 -1
- package/dist/pages/CatalogPage.js +110 -109
- package/dist/pages/CatalogPage.js.map +1 -1
- package/dist/pages/ComponentDetailPage.js +338 -338
- package/dist/pages/ComponentDetailPage.js.map +1 -1
- package/dist/pages/ComponentsPage.js +104 -103
- package/dist/pages/ComponentsPage.js.map +1 -1
- package/dist/pages/TargetDetailsPage.js +140 -132
- package/dist/pages/TargetDetailsPage.js.map +1 -1
- package/dist/pages/TemplateDetailPage.js +201 -199
- package/dist/pages/TemplateDetailPage.js.map +1 -1
- package/dist/pages/TemplatesPage.js +62 -61
- package/dist/pages/TemplatesPage.js.map +1 -1
- package/dist/pages/VibeDetailsPage.js +568 -555
- package/dist/pages/VibeDetailsPage.js.map +1 -1
- package/dist/pages/ai/AIBookmarksPage.js +104 -89
- package/dist/pages/ai/AIBookmarksPage.js.map +1 -1
- package/dist/pages/ai/AIPage.js +116 -107
- package/dist/pages/ai/AIPage.js.map +1 -1
- package/dist/pages/ai/AISessionDetailPage.js +46 -34
- package/dist/pages/ai/AISessionDetailPage.js.map +1 -1
- package/dist/pages/ai/AgentManagerPage.js +112 -100
- package/dist/pages/ai/AgentManagerPage.js.map +1 -1
- package/dist/pages/tunnels/TunnelDetailsPage.js +177 -153
- package/dist/pages/tunnels/TunnelDetailsPage.js.map +1 -1
- package/dist/pages/tunnels/TunnelsTab.js +119 -111
- package/dist/pages/tunnels/TunnelsTab.js.map +1 -1
- package/dist/pages/tunnels/tabs/TunnelDomainsTab.js +24 -24
- package/dist/pages/tunnels/tabs/TunnelDomainsTab.js.map +1 -1
- package/dist/pages/tunnels/tabs/TunnelOverviewTab.js +9 -8
- package/dist/pages/tunnels/tabs/TunnelOverviewTab.js.map +1 -1
- package/dist/pages/tunnels/tabs/TunnelSessionsTab.js +13 -13
- package/dist/pages/tunnels/tabs/TunnelSessionsTab.js.map +1 -1
- package/dist/pages/tunnels/tabs/TunnelSettingsTab.js +19 -15
- package/dist/pages/tunnels/tabs/TunnelSettingsTab.js.map +1 -1
- package/dist/services/assistantApi.js +9 -3
- package/dist/services/assistantApi.js.map +1 -1
- package/dist/services/createSandboxAssistantTransport.js +120 -0
- package/dist/services/createSandboxAssistantTransport.js.map +1 -0
- package/dist/store/assistantStore.js +1 -1
- package/dist/store/assistantStore.js.map +1 -1
- package/package.json +5 -1
|
@@ -1,67 +1,72 @@
|
|
|
1
|
-
import { useDeleteTunnelMutation as e, useListAgentsQuery as t, useListTunnelsQuery as n, useStartTunnelMutation as r, useStopTunnelMutation as
|
|
2
|
-
import { useTr as
|
|
3
|
-
import { ConfirmDialog as
|
|
4
|
-
import { QuotaExhaustedDialog as
|
|
5
|
-
import { getQuotaDialogContent as
|
|
6
|
-
import { useVibeControls as
|
|
7
|
-
import { PageHeader as
|
|
8
|
-
import { LoadingState as
|
|
9
|
-
import { ErrorState as
|
|
10
|
-
import { EmptyState as
|
|
11
|
-
import { SearchInput as
|
|
12
|
-
import { FilterDropdown as
|
|
1
|
+
import { useDeleteTunnelMutation as e, useListAgentsQuery as t, useListTunnelsQuery as n, useStartTunnelMutation as r, useStopTunnelMutation as ee } from "../../generated/wspace-operations.js";
|
|
2
|
+
import { useTr as te } from "../../shared/hooks/useTr.js";
|
|
3
|
+
import { ConfirmDialog as ne } from "../../components/shared/ConfirmDialog.js";
|
|
4
|
+
import { QuotaExhaustedDialog as i } from "../../components/shared/QuotaExhaustedDialog.js";
|
|
5
|
+
import { getQuotaDialogContent as re, isQuotaExhaustedError as a, markQuotaHandledLocally as o } from "../../utils/quotaUtils.js";
|
|
6
|
+
import { useVibeControls as ie } from "../../providers/VibeControlsProvider.js";
|
|
7
|
+
import { PageHeader as s } from "../../components/shared/PageHeader.js";
|
|
8
|
+
import { LoadingState as c } from "../../components/shared/LoadingState.js";
|
|
9
|
+
import { ErrorState as l } from "../../components/shared/ErrorState.js";
|
|
10
|
+
import { EmptyState as u } from "../../components/shared/EmptyState.js";
|
|
11
|
+
import { SearchInput as d } from "../../components/shared/SearchInput.js";
|
|
12
|
+
import { FilterDropdown as f } from "../../components/shared/FilterDropdown.js";
|
|
13
|
+
import { VIBE_PERMISSIONS as p } from "../../constants/permissions.js";
|
|
13
14
|
import { TunnelCard as m } from "../../components/tunnels/TunnelCard.js";
|
|
14
|
-
import { useEffect as
|
|
15
|
-
import { Link as
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
15
|
+
import { useEffect as ae, useMemo as oe, useState as h } from "react";
|
|
16
|
+
import { Link as g, useNavigate as se } from "react-router-dom";
|
|
17
|
+
import { usePermissions as ce } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
18
|
+
import { motion as le } from "framer-motion";
|
|
19
|
+
import { jsx as _, jsxs as v } from "react/jsx-runtime";
|
|
20
|
+
import { Button as y } from "@burdenoff/fe-libs/ui";
|
|
21
|
+
import { useEventBus as b } from "@burdenoff/fe-libs/shared/events";
|
|
22
|
+
import { AccessDenied as x } from "@burdenoff/fe-libs/shared/components";
|
|
20
23
|
//#region src/pages/tunnels/TunnelsTab.tsx
|
|
21
|
-
var
|
|
22
|
-
function
|
|
23
|
-
let T =
|
|
24
|
+
var S = 4e3;
|
|
25
|
+
function C() {
|
|
26
|
+
let C = te(), { hasPermission: w, isLoading: T } = ce(), E = w(p.TUNNEL_READ), D = w(p.TUNNEL_CREATE), O = w(p.TUNNEL_EXECUTE), ue = w(p.TUNNEL_DELETE), de = w(p.AGENT_READ), k = se(), A = b(), { workspaceId: j } = ie(), [M, N] = h(""), [P, F] = h("all"), [I, L] = h("all"), [R, z] = h(null), [B, V] = h(!1), [H, U] = h(/* @__PURE__ */ new Set()), { data: W, loading: fe, error: G, refetch: K, startPolling: q, stopPolling: J } = n({
|
|
24
27
|
variables: {
|
|
25
|
-
filter:
|
|
28
|
+
filter: oe(() => {
|
|
26
29
|
let e = {};
|
|
27
|
-
return
|
|
30
|
+
return P !== "all" && (e.providerKey = P), I !== "all" && (e.status = [I]), M.trim() && (e.search = M.trim()), e;
|
|
28
31
|
}, [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
P,
|
|
33
|
+
I,
|
|
34
|
+
M
|
|
32
35
|
]),
|
|
33
36
|
pagination: { limit: 50 }
|
|
34
37
|
},
|
|
35
|
-
fetchPolicy: "cache-and-network"
|
|
36
|
-
|
|
38
|
+
fetchPolicy: "cache-and-network",
|
|
39
|
+
skip: T || !E
|
|
40
|
+
}), { data: pe } = t({
|
|
37
41
|
variables: { pagination: { limit: 1 } },
|
|
42
|
+
skip: T || !de,
|
|
38
43
|
fetchPolicy: "cache-first"
|
|
39
|
-
}),
|
|
40
|
-
if (
|
|
44
|
+
}), Y = (pe?.vibecontrolsAgents?.length ?? 0) > 0, X = W?.tunnels?.items ?? [], me = (e) => {
|
|
45
|
+
if (A.emitCustom("vibecontrols.tunnel.create_clicked", { workspaceId: j ?? void 0 }), !Y) {
|
|
41
46
|
e.preventDefault();
|
|
42
47
|
let t = {
|
|
43
|
-
message:
|
|
48
|
+
message: C("vibecontrols.tunnels.list.createAgentFirst", "Create an agent first to use tunnels"),
|
|
44
49
|
variant: "info"
|
|
45
50
|
};
|
|
46
|
-
window.dispatchEvent(new CustomEvent("vibecontrols:toast", { detail: t })),
|
|
51
|
+
window.dispatchEvent(new CustomEvent("vibecontrols:toast", { detail: t })), k("/vibecontrols/agents");
|
|
47
52
|
}
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
}, Z = H.size > 0 || X.some((e) => e.status === "PROVISIONING" || e.status === "DELETING" || e.status === "ACTIVE" && !e.primaryDomain?.hostname);
|
|
54
|
+
ae(() => {
|
|
55
|
+
Z ? q(S) : J();
|
|
51
56
|
}, [
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
Z,
|
|
58
|
+
q,
|
|
59
|
+
J
|
|
55
60
|
]);
|
|
56
|
-
let
|
|
61
|
+
let Q = (e) => U((t) => new Set([...t, e])), $ = (e) => U((t) => {
|
|
57
62
|
let n = new Set(t);
|
|
58
63
|
return n.delete(e), n;
|
|
59
|
-
}), [
|
|
60
|
-
let t =
|
|
64
|
+
}), [he] = r(), ge = async (e) => {
|
|
65
|
+
let t = X.find((t) => t.id === e);
|
|
61
66
|
if (t) {
|
|
62
|
-
|
|
67
|
+
Q(e);
|
|
63
68
|
try {
|
|
64
|
-
let n = await
|
|
69
|
+
let n = await he({
|
|
65
70
|
variables: { id: e },
|
|
66
71
|
optimisticResponse: { startTunnel: {
|
|
67
72
|
...t,
|
|
@@ -69,22 +74,22 @@ function T() {
|
|
|
69
74
|
__typename: "Tunnel"
|
|
70
75
|
} }
|
|
71
76
|
});
|
|
72
|
-
n.error &&
|
|
73
|
-
workspaceId:
|
|
77
|
+
n.error && a(n.error) ? (o(), V(!0), A.emitCustom("vibecontrols.quota.tunnel_limit_hit", { workspaceId: j ?? void 0 })) : n.error || A.emitCustom("vibecontrols.tunnel.started", {
|
|
78
|
+
workspaceId: j ?? void 0,
|
|
74
79
|
tunnelId: e
|
|
75
80
|
});
|
|
76
81
|
} catch (e) {
|
|
77
|
-
|
|
82
|
+
a(e) ? (o(), V(!0), A.emitCustom("vibecontrols.quota.tunnel_limit_hit", { workspaceId: j ?? void 0 })) : console.error("[TunnelsTab] startTunnel failed", e);
|
|
78
83
|
} finally {
|
|
79
|
-
|
|
84
|
+
$(e);
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
|
-
}, [
|
|
83
|
-
let t =
|
|
87
|
+
}, [_e] = ee(), ve = async (e) => {
|
|
88
|
+
let t = X.find((t) => t.id === e);
|
|
84
89
|
if (t) {
|
|
85
|
-
|
|
90
|
+
Q(e);
|
|
86
91
|
try {
|
|
87
|
-
await
|
|
92
|
+
await _e({
|
|
88
93
|
variables: { id: e },
|
|
89
94
|
optimisticResponse: { stopTunnel: {
|
|
90
95
|
...t,
|
|
@@ -93,43 +98,43 @@ function T() {
|
|
|
93
98
|
} }
|
|
94
99
|
});
|
|
95
100
|
} finally {
|
|
96
|
-
|
|
101
|
+
$(e);
|
|
97
102
|
}
|
|
98
103
|
}
|
|
99
|
-
}, [
|
|
100
|
-
|
|
104
|
+
}, [ye, { loading: be }] = e({ onCompleted: () => {
|
|
105
|
+
z(null), K();
|
|
101
106
|
} });
|
|
102
|
-
return /* @__PURE__ */
|
|
107
|
+
return T ? /* @__PURE__ */ _(c, { message: C("common.loadingPermissions", "Loading permissions…") }) : E ? /* @__PURE__ */ v("div", {
|
|
103
108
|
className: "p-6 max-w-7xl mx-auto",
|
|
104
109
|
children: [
|
|
105
|
-
/* @__PURE__ */
|
|
106
|
-
title:
|
|
107
|
-
description:
|
|
108
|
-
actions: /* @__PURE__ */
|
|
110
|
+
/* @__PURE__ */ _(s, {
|
|
111
|
+
title: C("vibecontrols.tunnels.list.title", "Tunnels"),
|
|
112
|
+
description: C("vibecontrols.tunnels.list.description", "Expose local services on *.vibetunnels.com and custom domains."),
|
|
113
|
+
actions: D ? /* @__PURE__ */ _(y, {
|
|
109
114
|
variant: "default",
|
|
110
115
|
asChild: !0,
|
|
111
|
-
children: /* @__PURE__ */
|
|
116
|
+
children: /* @__PURE__ */ _(g, {
|
|
112
117
|
to: "/vibecontrols/tunnels/new",
|
|
113
|
-
onClick:
|
|
114
|
-
children:
|
|
118
|
+
onClick: me,
|
|
119
|
+
children: C("vibecontrols.tunnels.list.newTunnel", "New tunnel")
|
|
115
120
|
})
|
|
116
|
-
})
|
|
121
|
+
}) : void 0
|
|
117
122
|
}),
|
|
118
|
-
/* @__PURE__ */
|
|
123
|
+
/* @__PURE__ */ v("div", {
|
|
119
124
|
className: "flex flex-col sm:flex-row gap-3 mb-4",
|
|
120
125
|
children: [
|
|
121
|
-
/* @__PURE__ */
|
|
126
|
+
/* @__PURE__ */ _("div", {
|
|
122
127
|
className: "flex-1",
|
|
123
|
-
children: /* @__PURE__ */
|
|
124
|
-
value:
|
|
125
|
-
onChange:
|
|
128
|
+
children: /* @__PURE__ */ _(d, {
|
|
129
|
+
value: M,
|
|
130
|
+
onChange: N,
|
|
126
131
|
placeholder: "Search tunnels by name or slug"
|
|
127
132
|
})
|
|
128
133
|
}),
|
|
129
|
-
/* @__PURE__ */
|
|
134
|
+
/* @__PURE__ */ _(f, {
|
|
130
135
|
label: "Provider",
|
|
131
|
-
value:
|
|
132
|
-
onChange:
|
|
136
|
+
value: P,
|
|
137
|
+
onChange: F,
|
|
133
138
|
options: [
|
|
134
139
|
{
|
|
135
140
|
value: "all",
|
|
@@ -145,10 +150,10 @@ function T() {
|
|
|
145
150
|
}
|
|
146
151
|
]
|
|
147
152
|
}),
|
|
148
|
-
/* @__PURE__ */
|
|
153
|
+
/* @__PURE__ */ _(f, {
|
|
149
154
|
label: "Status",
|
|
150
|
-
value:
|
|
151
|
-
onChange:
|
|
155
|
+
value: I,
|
|
156
|
+
onChange: L,
|
|
152
157
|
options: [
|
|
153
158
|
{
|
|
154
159
|
value: "all",
|
|
@@ -178,70 +183,73 @@ function T() {
|
|
|
178
183
|
})
|
|
179
184
|
]
|
|
180
185
|
}),
|
|
181
|
-
|
|
182
|
-
|
|
186
|
+
fe && !W && /* @__PURE__ */ _(c, { message: "Loading tunnels" }),
|
|
187
|
+
G && /* @__PURE__ */ _(l, {
|
|
183
188
|
title: "Failed to load tunnels",
|
|
184
|
-
message:
|
|
185
|
-
onRetry: () =>
|
|
189
|
+
message: G.message,
|
|
190
|
+
onRetry: () => K()
|
|
186
191
|
}),
|
|
187
|
-
|
|
188
|
-
title:
|
|
189
|
-
description:
|
|
190
|
-
action: /* @__PURE__ */
|
|
192
|
+
W && X.length === 0 && !Y && /* @__PURE__ */ _(u, {
|
|
193
|
+
title: C("vibecontrols.tunnels.list.noAgentTitle", "Create an agent first"),
|
|
194
|
+
description: C("vibecontrols.tunnels.list.noAgentDescription", "Tunnels need a VibeControls agent to forward traffic. Install + register an agent, then come back to create your first tunnel."),
|
|
195
|
+
action: /* @__PURE__ */ _(y, {
|
|
191
196
|
variant: "default",
|
|
192
|
-
onClick: () =>
|
|
193
|
-
children:
|
|
197
|
+
onClick: () => k("/vibecontrols/agents"),
|
|
198
|
+
children: C("vibecontrols.tunnels.list.setupAgent", "Set up an agent")
|
|
194
199
|
})
|
|
195
200
|
}),
|
|
196
|
-
|
|
197
|
-
title:
|
|
198
|
-
description:
|
|
199
|
-
action: /* @__PURE__ */
|
|
201
|
+
W && X.length === 0 && Y && /* @__PURE__ */ _(u, {
|
|
202
|
+
title: C("vibecontrols.tunnels.list.emptyTitle", "No tunnels yet"),
|
|
203
|
+
description: C("vibecontrols.tunnels.list.emptyDescription", "Create your first tunnel to expose a local service."),
|
|
204
|
+
action: D ? /* @__PURE__ */ _(y, {
|
|
200
205
|
variant: "default",
|
|
201
|
-
onClick: () =>
|
|
202
|
-
children:
|
|
203
|
-
})
|
|
206
|
+
onClick: () => k("/vibecontrols/tunnels/new"),
|
|
207
|
+
children: C("vibecontrols.tunnels.list.createTunnel", "Create tunnel")
|
|
208
|
+
}) : void 0
|
|
204
209
|
}),
|
|
205
|
-
|
|
210
|
+
X.length > 0 && /* @__PURE__ */ _(le.div, {
|
|
206
211
|
initial: { opacity: 0 },
|
|
207
212
|
animate: { opacity: 1 },
|
|
208
213
|
className: "space-y-3",
|
|
209
|
-
children:
|
|
214
|
+
children: X.map((e) => /* @__PURE__ */ _(m, {
|
|
210
215
|
tunnel: e,
|
|
211
|
-
isPending:
|
|
212
|
-
onOpen: (e) =>
|
|
213
|
-
onStart: (e) => void
|
|
214
|
-
onStop: (e) => void
|
|
215
|
-
onDelete: (e, t) =>
|
|
216
|
+
isPending: H.has(e.id),
|
|
217
|
+
onOpen: (e) => k(`/vibecontrols/tunnels/${e}`),
|
|
218
|
+
onStart: O ? (e) => void ge(e) : void 0,
|
|
219
|
+
onStop: O ? (e) => void ve(e) : void 0,
|
|
220
|
+
onDelete: ue ? (e, t) => z({
|
|
216
221
|
id: e,
|
|
217
222
|
name: t
|
|
218
|
-
})
|
|
223
|
+
}) : void 0
|
|
219
224
|
}, e.id))
|
|
220
225
|
}),
|
|
221
|
-
/* @__PURE__ */
|
|
222
|
-
isOpen:
|
|
223
|
-
onClose: () =>
|
|
226
|
+
/* @__PURE__ */ _(ne, {
|
|
227
|
+
isOpen: R !== null,
|
|
228
|
+
onClose: () => z(null),
|
|
224
229
|
onConfirm: () => {
|
|
225
|
-
|
|
230
|
+
R && ye({ variables: { id: R.id } });
|
|
226
231
|
},
|
|
227
232
|
title: "Delete tunnel",
|
|
228
|
-
message:
|
|
233
|
+
message: R ? `Delete "${R.name}"? This will stop the tunnel and remove all its sessions and domains.` : "",
|
|
229
234
|
confirmLabel: "Delete",
|
|
230
235
|
variant: "danger",
|
|
231
|
-
isLoading:
|
|
236
|
+
isLoading: be
|
|
232
237
|
}),
|
|
233
|
-
/* @__PURE__ */
|
|
234
|
-
isOpen:
|
|
235
|
-
onClose: () =>
|
|
238
|
+
/* @__PURE__ */ _(i, {
|
|
239
|
+
isOpen: B,
|
|
240
|
+
onClose: () => V(!1),
|
|
236
241
|
onConfirm: () => {
|
|
237
|
-
|
|
242
|
+
V(!1), k("/billing/plans");
|
|
238
243
|
},
|
|
239
|
-
...
|
|
244
|
+
...re("tunnel-start")
|
|
240
245
|
})
|
|
241
246
|
]
|
|
247
|
+
}) : /* @__PURE__ */ _(x, {
|
|
248
|
+
title: C("vibecontrols.tunnels.list.denied.title", "Access Denied"),
|
|
249
|
+
description: C("vibecontrols.tunnels.list.denied.description", "You do not have permission to view tunnels.")
|
|
242
250
|
});
|
|
243
251
|
}
|
|
244
252
|
//#endregion
|
|
245
|
-
export {
|
|
253
|
+
export { C as TunnelsTab };
|
|
246
254
|
|
|
247
255
|
//# sourceMappingURL=TunnelsTab.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TunnelsTab.js","names":[],"sources":["../../../src/pages/tunnels/TunnelsTab.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { Link, useNavigate } from 'react-router-dom';\nimport { motion } from 'framer-motion';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { useVibeControls } from '@/providers/VibeControlsProvider';\n\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { PageHeader } from '@/components/shared/PageHeader';\nimport { SearchInput } from '@/components/shared/SearchInput';\nimport { FilterDropdown } from '@/components/shared/FilterDropdown';\nimport { ConfirmDialog } from '@/components/shared/ConfirmDialog';\nimport { QuotaExhaustedDialog } from '@/components/shared/QuotaExhaustedDialog';\nimport { EmptyState } from '@/components/shared/EmptyState';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { TunnelCard } from '@/components/tunnels/TunnelCard';\nimport type { TunnelStatus, TunnelProviderKey } from '@/generated/wspace-types';\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n useListTunnelsQuery,\n useListAgentsQuery,\n useStartTunnelMutation,\n useStopTunnelMutation,\n useDeleteTunnelMutation,\n type ListTunnelsQuery,\n} from '@/generated/wspace-operations';\nimport {\n isQuotaExhaustedError,\n markQuotaHandledLocally,\n getQuotaDialogContent,\n} from '@/utils/quotaUtils';\n\ntype TunnelRow = NonNullable<ListTunnelsQuery['tunnels']>['items'][number];\n\nconst TRANSITION_POLL_MS = 4000;\n\nexport function TunnelsTab() {\n const tr = useTr();\n const navigate = useNavigate();\n const bus = useEventBus();\n const { workspaceId } = useVibeControls();\n const [search, setSearch] = useState('');\n const [providerFilter, setProviderFilter] = useState<string>('all');\n const [statusFilter, setStatusFilter] = useState<string>('all');\n const [pendingDelete, setPendingDelete] = useState<{ id: string; name: string } | null>(null);\n const [showQuotaExhausted, setShowQuotaExhausted] = useState(false);\n const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());\n\n const filter = useMemo(() => {\n const f: Record<string, unknown> = {};\n if (providerFilter !== 'all') {\n f.providerKey = providerFilter as TunnelProviderKey;\n }\n if (statusFilter !== 'all') {\n f.status = [statusFilter as TunnelStatus];\n }\n if (search.trim()) {\n f.search = search.trim();\n }\n return f;\n }, [providerFilter, statusFilter, search]);\n\n const { data, loading, error, refetch, startPolling, stopPolling } = useListTunnelsQuery({\n variables: { filter, pagination: { limit: 50 } },\n fetchPolicy: 'cache-and-network',\n });\n\n const { data: agentsData } = useListAgentsQuery({\n variables: { pagination: { limit: 1 } },\n fetchPolicy: 'cache-first',\n });\n const hasAnyAgent = (agentsData?.vibecontrolsAgents?.length ?? 0) > 0;\n\n const tunnels: TunnelRow[] = data?.tunnels?.items ?? [];\n\n const handleNewTunnelClick = (event: React.MouseEvent) => {\n bus.emitCustom('vibecontrols.tunnel.create_clicked', { workspaceId: workspaceId ?? undefined });\n if (!hasAnyAgent) {\n event.preventDefault();\n // Lightweight toast via window event — falls back to console if no host listener.\n const detail = {\n message: tr(\n 'vibecontrols.tunnels.list.createAgentFirst',\n 'Create an agent first to use tunnels'\n ),\n variant: 'info' as const,\n };\n window.dispatchEvent(new CustomEvent('vibecontrols:toast', { detail }));\n navigate('/vibecontrols/agents');\n }\n };\n\n // Poll while any tunnel is in a transient state OR is ACTIVE but has no\n // domain yet (Cloudflare assigns the URL ~5-15 s after start returns).\n const needsPoll =\n pendingIds.size > 0 ||\n tunnels.some(\n (t) =>\n t.status === 'PROVISIONING' ||\n t.status === 'DELETING' ||\n (t.status === 'ACTIVE' && !t.primaryDomain?.hostname)\n );\n useEffect(() => {\n if (needsPoll) {\n startPolling(TRANSITION_POLL_MS);\n } else {\n stopPolling();\n }\n }, [needsPoll, startPolling, stopPolling]);\n\n const addPending = (id: string) => setPendingIds((prev) => new Set([...prev, id]));\n const removePending = (id: string) =>\n setPendingIds((prev) => {\n const next = new Set(prev);\n next.delete(id);\n return next;\n });\n\n const [startTunnel] = useStartTunnelMutation();\n\n const handleStartTunnel = async (id: string) => {\n const existing = tunnels.find((t) => t.id === id);\n if (!existing) return;\n addPending(id);\n try {\n const result = await startTunnel({\n variables: { id },\n optimisticResponse: {\n startTunnel: {\n ...existing,\n status: 'PROVISIONING' as TunnelStatus,\n __typename: 'Tunnel',\n },\n },\n });\n if (result.error && isQuotaExhaustedError(result.error)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n bus.emitCustom('vibecontrols.quota.tunnel_limit_hit', {\n workspaceId: workspaceId ?? undefined,\n });\n } else if (!result.error) {\n bus.emitCustom('vibecontrols.tunnel.started', {\n workspaceId: workspaceId ?? undefined,\n tunnelId: id,\n });\n }\n } catch (err) {\n // Only open the quota modal for actual quota errors — pre-existing\n // bug spotted on the BOFF-2625 review: any tunnel-start failure\n // (network / validation / auth) was opening the quota dialog.\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n bus.emitCustom('vibecontrols.quota.tunnel_limit_hit', {\n workspaceId: workspaceId ?? undefined,\n });\n } else {\n console.error('[TunnelsTab] startTunnel failed', err);\n }\n } finally {\n removePending(id);\n }\n };\n\n const [stopTunnel] = useStopTunnelMutation();\n\n const handleStopTunnel = async (id: string) => {\n const existing = tunnels.find((t) => t.id === id);\n if (!existing) return;\n addPending(id);\n try {\n await stopTunnel({\n variables: { id },\n optimisticResponse: {\n stopTunnel: { ...existing, status: 'STOPPED' as TunnelStatus, __typename: 'Tunnel' },\n },\n });\n } finally {\n removePending(id);\n }\n };\n\n const [deleteTunnel, { loading: deleting }] = useDeleteTunnelMutation({\n onCompleted: () => {\n setPendingDelete(null);\n void refetch();\n },\n });\n\n return (\n <div className=\"p-6 max-w-7xl mx-auto\">\n <PageHeader\n title={tr('vibecontrols.tunnels.list.title', 'Tunnels')}\n description={tr(\n 'vibecontrols.tunnels.list.description',\n 'Expose local services on *.vibetunnels.com and custom domains.'\n )}\n actions={\n <Button variant=\"default\" asChild>\n <Link to=\"/vibecontrols/tunnels/new\" onClick={handleNewTunnelClick}>\n {tr('vibecontrols.tunnels.list.newTunnel', 'New tunnel')}\n </Link>\n </Button>\n }\n />\n\n <div className=\"flex flex-col sm:flex-row gap-3 mb-4\">\n <div className=\"flex-1\">\n <SearchInput\n value={search}\n onChange={setSearch}\n placeholder=\"Search tunnels by name or slug\"\n />\n </div>\n <FilterDropdown\n label=\"Provider\"\n value={providerFilter}\n onChange={setProviderFilter}\n options={[\n { value: 'all', label: 'All providers' },\n { value: 'VIBETUNNELS', label: 'Vibe Tunnels (default)' },\n { value: 'CLOUDFLARE', label: 'Cloudflare Tunnels' },\n ]}\n />\n <FilterDropdown\n label=\"Status\"\n value={statusFilter}\n onChange={setStatusFilter}\n options={[\n { value: 'all', label: 'All statuses' },\n { value: 'ACTIVE', label: 'Active' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'PROVISIONING', label: 'Provisioning' },\n { value: 'STOPPED', label: 'Stopped' },\n { value: 'ERROR', label: 'Error' },\n ]}\n />\n </div>\n\n {loading && !data && <LoadingState message=\"Loading tunnels\" />}\n {error && (\n <ErrorState\n title=\"Failed to load tunnels\"\n message={error.message}\n onRetry={() => refetch()}\n />\n )}\n\n {data && tunnels.length === 0 && !hasAnyAgent && (\n <EmptyState\n title={tr('vibecontrols.tunnels.list.noAgentTitle', 'Create an agent first')}\n description={tr(\n 'vibecontrols.tunnels.list.noAgentDescription',\n 'Tunnels need a VibeControls agent to forward traffic. Install + register an agent, then come back to create your first tunnel.'\n )}\n action={\n <Button variant=\"default\" onClick={() => navigate('/vibecontrols/agents')}>\n {tr('vibecontrols.tunnels.list.setupAgent', 'Set up an agent')}\n </Button>\n }\n />\n )}\n\n {data && tunnels.length === 0 && hasAnyAgent && (\n <EmptyState\n title={tr('vibecontrols.tunnels.list.emptyTitle', 'No tunnels yet')}\n description={tr(\n 'vibecontrols.tunnels.list.emptyDescription',\n 'Create your first tunnel to expose a local service.'\n )}\n action={\n <Button variant=\"default\" onClick={() => navigate('/vibecontrols/tunnels/new')}>\n {tr('vibecontrols.tunnels.list.createTunnel', 'Create tunnel')}\n </Button>\n }\n />\n )}\n\n {tunnels.length > 0 && (\n <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className=\"space-y-3\">\n {tunnels.map((tunnel: TunnelRow) => (\n <TunnelCard\n key={tunnel.id}\n tunnel={tunnel}\n isPending={pendingIds.has(tunnel.id)}\n onOpen={(id) => navigate(`/vibecontrols/tunnels/${id}`)}\n onStart={(id) => void handleStartTunnel(id)}\n onStop={(id) => void handleStopTunnel(id)}\n onDelete={(id, name) => setPendingDelete({ id, name })}\n />\n ))}\n </motion.div>\n )}\n\n <ConfirmDialog\n isOpen={pendingDelete !== null}\n onClose={() => setPendingDelete(null)}\n onConfirm={() => {\n if (pendingDelete) {\n void deleteTunnel({ variables: { id: pendingDelete.id } });\n }\n }}\n title=\"Delete tunnel\"\n message={\n pendingDelete\n ? `Delete \"${pendingDelete.name}\"? This will stop the tunnel and remove all its sessions and domains.`\n : ''\n }\n confirmLabel=\"Delete\"\n variant=\"danger\"\n isLoading={deleting}\n />\n\n <QuotaExhaustedDialog\n isOpen={showQuotaExhausted}\n onClose={() => setShowQuotaExhausted(false)}\n onConfirm={() => {\n setShowQuotaExhausted(false);\n navigate('/billing/plans');\n }}\n {...getQuotaDialogContent('tunnel-start')}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TunnelsTab.js","names":[],"sources":["../../../src/pages/tunnels/TunnelsTab.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { Link, useNavigate } from 'react-router-dom';\nimport { motion } from 'framer-motion';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport { useVibeControls } from '@/providers/VibeControlsProvider';\n\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { PageHeader } from '@/components/shared/PageHeader';\nimport { SearchInput } from '@/components/shared/SearchInput';\nimport { FilterDropdown } from '@/components/shared/FilterDropdown';\nimport { ConfirmDialog } from '@/components/shared/ConfirmDialog';\nimport { QuotaExhaustedDialog } from '@/components/shared/QuotaExhaustedDialog';\nimport { EmptyState } from '@/components/shared/EmptyState';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { TunnelCard } from '@/components/tunnels/TunnelCard';\nimport type { TunnelStatus, TunnelProviderKey } from '@/generated/wspace-types';\nimport { AccessDenied } from '@burdenoff/fe-libs/shared/components';\nimport { usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { VIBE_PERMISSIONS } from '@/constants/permissions';\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n useListTunnelsQuery,\n useListAgentsQuery,\n useStartTunnelMutation,\n useStopTunnelMutation,\n useDeleteTunnelMutation,\n type ListTunnelsQuery,\n} from '@/generated/wspace-operations';\nimport {\n isQuotaExhaustedError,\n markQuotaHandledLocally,\n getQuotaDialogContent,\n} from '@/utils/quotaUtils';\n\ntype TunnelRow = NonNullable<ListTunnelsQuery['tunnels']>['items'][number];\n\nconst TRANSITION_POLL_MS = 4000;\n\nexport function TunnelsTab() {\n const tr = useTr();\n // Tunnel reads are gated on `tunnel:read`; creation on `tunnel:create`;\n // start/stop on `tunnel:execute`; deletion on `tunnel:delete`.\n const { hasPermission, isLoading: permissionsLoading } = usePermissions();\n const canRead = hasPermission(VIBE_PERMISSIONS.TUNNEL_READ);\n const canCreate = hasPermission(VIBE_PERMISSIONS.TUNNEL_CREATE);\n const canExecute = hasPermission(VIBE_PERMISSIONS.TUNNEL_EXECUTE);\n const canDelete = hasPermission(VIBE_PERMISSIONS.TUNNEL_DELETE);\n const canReadAgents = hasPermission(VIBE_PERMISSIONS.AGENT_READ);\n const navigate = useNavigate();\n const bus = useEventBus();\n const { workspaceId } = useVibeControls();\n const [search, setSearch] = useState('');\n const [providerFilter, setProviderFilter] = useState<string>('all');\n const [statusFilter, setStatusFilter] = useState<string>('all');\n const [pendingDelete, setPendingDelete] = useState<{ id: string; name: string } | null>(null);\n const [showQuotaExhausted, setShowQuotaExhausted] = useState(false);\n const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());\n\n const filter = useMemo(() => {\n const f: Record<string, unknown> = {};\n if (providerFilter !== 'all') {\n f.providerKey = providerFilter as TunnelProviderKey;\n }\n if (statusFilter !== 'all') {\n f.status = [statusFilter as TunnelStatus];\n }\n if (search.trim()) {\n f.search = search.trim();\n }\n return f;\n }, [providerFilter, statusFilter, search]);\n\n const { data, loading, error, refetch, startPolling, stopPolling } = useListTunnelsQuery({\n variables: { filter, pagination: { limit: 50 } },\n fetchPolicy: 'cache-and-network',\n skip: permissionsLoading || !canRead,\n });\n\n const { data: agentsData } = useListAgentsQuery({\n variables: { pagination: { limit: 1 } },\n skip: permissionsLoading || !canReadAgents,\n fetchPolicy: 'cache-first',\n });\n const hasAnyAgent = (agentsData?.vibecontrolsAgents?.length ?? 0) > 0;\n\n const tunnels: TunnelRow[] = data?.tunnels?.items ?? [];\n\n const handleNewTunnelClick = (event: React.MouseEvent) => {\n bus.emitCustom('vibecontrols.tunnel.create_clicked', { workspaceId: workspaceId ?? undefined });\n if (!hasAnyAgent) {\n event.preventDefault();\n // Lightweight toast via window event — falls back to console if no host listener.\n const detail = {\n message: tr(\n 'vibecontrols.tunnels.list.createAgentFirst',\n 'Create an agent first to use tunnels'\n ),\n variant: 'info' as const,\n };\n window.dispatchEvent(new CustomEvent('vibecontrols:toast', { detail }));\n navigate('/vibecontrols/agents');\n }\n };\n\n // Poll while any tunnel is in a transient state OR is ACTIVE but has no\n // domain yet (Cloudflare assigns the URL ~5-15 s after start returns).\n const needsPoll =\n pendingIds.size > 0 ||\n tunnels.some(\n (t) =>\n t.status === 'PROVISIONING' ||\n t.status === 'DELETING' ||\n (t.status === 'ACTIVE' && !t.primaryDomain?.hostname)\n );\n useEffect(() => {\n if (needsPoll) {\n startPolling(TRANSITION_POLL_MS);\n } else {\n stopPolling();\n }\n }, [needsPoll, startPolling, stopPolling]);\n\n const addPending = (id: string) => setPendingIds((prev) => new Set([...prev, id]));\n const removePending = (id: string) =>\n setPendingIds((prev) => {\n const next = new Set(prev);\n next.delete(id);\n return next;\n });\n\n const [startTunnel] = useStartTunnelMutation();\n\n const handleStartTunnel = async (id: string) => {\n const existing = tunnels.find((t) => t.id === id);\n if (!existing) return;\n addPending(id);\n try {\n const result = await startTunnel({\n variables: { id },\n optimisticResponse: {\n startTunnel: {\n ...existing,\n status: 'PROVISIONING' as TunnelStatus,\n __typename: 'Tunnel',\n },\n },\n });\n if (result.error && isQuotaExhaustedError(result.error)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n bus.emitCustom('vibecontrols.quota.tunnel_limit_hit', {\n workspaceId: workspaceId ?? undefined,\n });\n } else if (!result.error) {\n bus.emitCustom('vibecontrols.tunnel.started', {\n workspaceId: workspaceId ?? undefined,\n tunnelId: id,\n });\n }\n } catch (err) {\n // Only open the quota modal for actual quota errors — pre-existing\n // bug spotted on the BOFF-2625 review: any tunnel-start failure\n // (network / validation / auth) was opening the quota dialog.\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setShowQuotaExhausted(true);\n bus.emitCustom('vibecontrols.quota.tunnel_limit_hit', {\n workspaceId: workspaceId ?? undefined,\n });\n } else {\n console.error('[TunnelsTab] startTunnel failed', err);\n }\n } finally {\n removePending(id);\n }\n };\n\n const [stopTunnel] = useStopTunnelMutation();\n\n const handleStopTunnel = async (id: string) => {\n const existing = tunnels.find((t) => t.id === id);\n if (!existing) return;\n addPending(id);\n try {\n await stopTunnel({\n variables: { id },\n optimisticResponse: {\n stopTunnel: { ...existing, status: 'STOPPED' as TunnelStatus, __typename: 'Tunnel' },\n },\n });\n } finally {\n removePending(id);\n }\n };\n\n const [deleteTunnel, { loading: deleting }] = useDeleteTunnelMutation({\n onCompleted: () => {\n setPendingDelete(null);\n void refetch();\n },\n });\n\n if (permissionsLoading) {\n return <LoadingState message={tr('common.loadingPermissions', 'Loading permissions…')} />;\n }\n\n if (!canRead) {\n return (\n <AccessDenied\n title={tr('vibecontrols.tunnels.list.denied.title', 'Access Denied')}\n description={tr(\n 'vibecontrols.tunnels.list.denied.description',\n 'You do not have permission to view tunnels.'\n )}\n />\n );\n }\n\n return (\n <div className=\"p-6 max-w-7xl mx-auto\">\n <PageHeader\n title={tr('vibecontrols.tunnels.list.title', 'Tunnels')}\n description={tr(\n 'vibecontrols.tunnels.list.description',\n 'Expose local services on *.vibetunnels.com and custom domains.'\n )}\n actions={\n canCreate ? (\n <Button variant=\"default\" asChild>\n <Link to=\"/vibecontrols/tunnels/new\" onClick={handleNewTunnelClick}>\n {tr('vibecontrols.tunnels.list.newTunnel', 'New tunnel')}\n </Link>\n </Button>\n ) : undefined\n }\n />\n\n <div className=\"flex flex-col sm:flex-row gap-3 mb-4\">\n <div className=\"flex-1\">\n <SearchInput\n value={search}\n onChange={setSearch}\n placeholder=\"Search tunnels by name or slug\"\n />\n </div>\n <FilterDropdown\n label=\"Provider\"\n value={providerFilter}\n onChange={setProviderFilter}\n options={[\n { value: 'all', label: 'All providers' },\n { value: 'VIBETUNNELS', label: 'Vibe Tunnels (default)' },\n { value: 'CLOUDFLARE', label: 'Cloudflare Tunnels' },\n ]}\n />\n <FilterDropdown\n label=\"Status\"\n value={statusFilter}\n onChange={setStatusFilter}\n options={[\n { value: 'all', label: 'All statuses' },\n { value: 'ACTIVE', label: 'Active' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'PROVISIONING', label: 'Provisioning' },\n { value: 'STOPPED', label: 'Stopped' },\n { value: 'ERROR', label: 'Error' },\n ]}\n />\n </div>\n\n {loading && !data && <LoadingState message=\"Loading tunnels\" />}\n {error && (\n <ErrorState\n title=\"Failed to load tunnels\"\n message={error.message}\n onRetry={() => refetch()}\n />\n )}\n\n {data && tunnels.length === 0 && !hasAnyAgent && (\n <EmptyState\n title={tr('vibecontrols.tunnels.list.noAgentTitle', 'Create an agent first')}\n description={tr(\n 'vibecontrols.tunnels.list.noAgentDescription',\n 'Tunnels need a VibeControls agent to forward traffic. Install + register an agent, then come back to create your first tunnel.'\n )}\n action={\n <Button variant=\"default\" onClick={() => navigate('/vibecontrols/agents')}>\n {tr('vibecontrols.tunnels.list.setupAgent', 'Set up an agent')}\n </Button>\n }\n />\n )}\n\n {data && tunnels.length === 0 && hasAnyAgent && (\n <EmptyState\n title={tr('vibecontrols.tunnels.list.emptyTitle', 'No tunnels yet')}\n description={tr(\n 'vibecontrols.tunnels.list.emptyDescription',\n 'Create your first tunnel to expose a local service.'\n )}\n action={\n canCreate ? (\n <Button variant=\"default\" onClick={() => navigate('/vibecontrols/tunnels/new')}>\n {tr('vibecontrols.tunnels.list.createTunnel', 'Create tunnel')}\n </Button>\n ) : undefined\n }\n />\n )}\n\n {tunnels.length > 0 && (\n <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className=\"space-y-3\">\n {tunnels.map((tunnel: TunnelRow) => (\n <TunnelCard\n key={tunnel.id}\n tunnel={tunnel}\n isPending={pendingIds.has(tunnel.id)}\n onOpen={(id) => navigate(`/vibecontrols/tunnels/${id}`)}\n onStart={canExecute ? (id) => void handleStartTunnel(id) : undefined}\n onStop={canExecute ? (id) => void handleStopTunnel(id) : undefined}\n onDelete={canDelete ? (id, name) => setPendingDelete({ id, name }) : undefined}\n />\n ))}\n </motion.div>\n )}\n\n <ConfirmDialog\n isOpen={pendingDelete !== null}\n onClose={() => setPendingDelete(null)}\n onConfirm={() => {\n if (pendingDelete) {\n void deleteTunnel({ variables: { id: pendingDelete.id } });\n }\n }}\n title=\"Delete tunnel\"\n message={\n pendingDelete\n ? `Delete \"${pendingDelete.name}\"? This will stop the tunnel and remove all its sessions and domains.`\n : ''\n }\n confirmLabel=\"Delete\"\n variant=\"danger\"\n isLoading={deleting}\n />\n\n <QuotaExhaustedDialog\n isOpen={showQuotaExhausted}\n onClose={() => setShowQuotaExhausted(false)}\n onConfirm={() => {\n setShowQuotaExhausted(false);\n navigate('/billing/plans');\n }}\n {...getQuotaDialogContent('tunnel-start')}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -5,27 +5,27 @@ import { DomainVerificationBadge as s } from "../../../components/tunnels/Domain
|
|
|
5
5
|
import { Fragment as c, useState as l } from "react";
|
|
6
6
|
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
7
7
|
//#region src/pages/tunnels/tabs/TunnelDomainsTab.tsx
|
|
8
|
-
function f({ tunnel: f }) {
|
|
9
|
-
let
|
|
8
|
+
function f({ tunnel: f, canCreate: p, canUpdate: m }) {
|
|
9
|
+
let h = a(), { refetch: g } = n({ variables: { id: f.id } }), [_, v] = l(""), [y, b] = l(null), [x, { loading: S }] = r({
|
|
10
10
|
onCompleted: async (e) => {
|
|
11
|
-
await
|
|
11
|
+
await g(), e.registerCustomTunnelDomain.id && await C({ variables: {
|
|
12
12
|
tunnelId: f.id,
|
|
13
13
|
domainId: e.registerCustomTunnelDomain.id
|
|
14
|
-
} }),
|
|
14
|
+
} }), v(""), b(null);
|
|
15
15
|
},
|
|
16
|
-
onError: (e) =>
|
|
17
|
-
}), [
|
|
18
|
-
let e =
|
|
19
|
-
e && await
|
|
20
|
-
},
|
|
16
|
+
onError: (e) => b(e.message)
|
|
17
|
+
}), [C] = e({ onCompleted: () => void g() }), [w] = t({ onCompleted: () => void g() }), [T] = i({ onCompleted: () => void g() }), E = async () => {
|
|
18
|
+
let e = _.trim().toLowerCase();
|
|
19
|
+
e && await x({ variables: { input: { hostname: e } } });
|
|
20
|
+
}, D = f.domains ?? [];
|
|
21
21
|
return /* @__PURE__ */ d("div", {
|
|
22
22
|
className: "max-w-4xl",
|
|
23
|
-
children: [/* @__PURE__ */ d("section", {
|
|
23
|
+
children: [p && /* @__PURE__ */ d("section", {
|
|
24
24
|
className: "rounded-lg border border-border-subtle bg-bg-surface p-4 mb-6",
|
|
25
25
|
children: [
|
|
26
26
|
/* @__PURE__ */ u("h3", {
|
|
27
27
|
className: "text-sm font-semibold text-text-primary mb-3",
|
|
28
|
-
children:
|
|
28
|
+
children: h("vibecontrols.tunnels.domains.attachCustomDomain", "Attach a custom domain")
|
|
29
29
|
}),
|
|
30
30
|
/* @__PURE__ */ u("p", {
|
|
31
31
|
className: "text-xs text-text-secondary mb-3",
|
|
@@ -36,26 +36,26 @@ function f({ tunnel: f }) {
|
|
|
36
36
|
children: [/* @__PURE__ */ u("input", {
|
|
37
37
|
"aria-label": "preview.example.com",
|
|
38
38
|
type: "text",
|
|
39
|
-
value:
|
|
40
|
-
onChange: (e) =>
|
|
39
|
+
value: _,
|
|
40
|
+
onChange: (e) => v(e.target.value),
|
|
41
41
|
placeholder: "preview.example.com",
|
|
42
42
|
className: "flex-1 rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-border-focus focus:outline-none"
|
|
43
43
|
}), /* @__PURE__ */ u("button", {
|
|
44
44
|
type: "button",
|
|
45
|
-
disabled:
|
|
46
|
-
onClick: () => void
|
|
45
|
+
disabled: S || !_.trim(),
|
|
46
|
+
onClick: () => void E(),
|
|
47
47
|
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",
|
|
48
|
-
children:
|
|
48
|
+
children: S ? h("vibecontrols.tunnels.domains.adding", "Adding...") : h("vibecontrols.tunnels.domains.addDomain", "Add domain")
|
|
49
49
|
})]
|
|
50
50
|
}),
|
|
51
|
-
|
|
51
|
+
y && /* @__PURE__ */ u("p", {
|
|
52
52
|
className: "mt-2 text-xs text-status-error-fg",
|
|
53
|
-
children:
|
|
53
|
+
children: y
|
|
54
54
|
})
|
|
55
55
|
]
|
|
56
56
|
}), /* @__PURE__ */ u("div", {
|
|
57
57
|
className: "rounded-lg border border-border-subtle bg-bg-surface overflow-x-auto",
|
|
58
|
-
children:
|
|
58
|
+
children: D.length === 0 ? /* @__PURE__ */ u("div", {
|
|
59
59
|
className: "p-4 text-sm text-text-muted",
|
|
60
60
|
children: "No domains attached. Add one above or reserve a *.vibetunnels.com subdomain from the Tunnels tab."
|
|
61
61
|
}) : /* @__PURE__ */ d("table", {
|
|
@@ -82,7 +82,7 @@ function f({ tunnel: f }) {
|
|
|
82
82
|
]
|
|
83
83
|
}) }), /* @__PURE__ */ u("tbody", {
|
|
84
84
|
className: "divide-y divide-border-subtle",
|
|
85
|
-
children:
|
|
85
|
+
children: D.map((e) => /* @__PURE__ */ d(c, { children: [/* @__PURE__ */ d("tr", {
|
|
86
86
|
className: "align-middle",
|
|
87
87
|
children: [
|
|
88
88
|
/* @__PURE__ */ u("td", {
|
|
@@ -107,14 +107,14 @@ function f({ tunnel: f }) {
|
|
|
107
107
|
className: "px-4 py-3 whitespace-nowrap text-right",
|
|
108
108
|
children: /* @__PURE__ */ d("div", {
|
|
109
109
|
className: "flex gap-2 justify-end",
|
|
110
|
-
children: [e.verificationStatus === o.Pending && /* @__PURE__ */ u("button", {
|
|
110
|
+
children: [m && e.verificationStatus === o.Pending && /* @__PURE__ */ u("button", {
|
|
111
111
|
type: "button",
|
|
112
|
-
onClick: () => void
|
|
112
|
+
onClick: () => void T({ variables: { domainId: e.id } }),
|
|
113
113
|
className: "rounded-md border border-border-default px-3 py-1 text-xs text-text-primary hover:bg-bg-elevated",
|
|
114
114
|
children: "Verify"
|
|
115
|
-
}), /* @__PURE__ */ u("button", {
|
|
115
|
+
}), m && /* @__PURE__ */ u("button", {
|
|
116
116
|
type: "button",
|
|
117
|
-
onClick: () => void
|
|
117
|
+
onClick: () => void w({ variables: {
|
|
118
118
|
tunnelId: f.id,
|
|
119
119
|
domainId: e.id
|
|
120
120
|
} }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TunnelDomainsTab.js","names":[],"sources":["../../../../src/pages/tunnels/tabs/TunnelDomainsTab.tsx"],"sourcesContent":["import { Fragment, useState } from 'react';\nimport type { GetTunnelQuery } from '@/generated/wspace-operations';\nimport { useTr } from '../../../shared/hooks/useTr';\nimport {\n useRegisterCustomTunnelDomainMutation,\n useAttachTunnelDomainMutation,\n useDetachTunnelDomainMutation,\n useVerifyTunnelDomainMutation,\n useGetTunnelQuery,\n} from '@/generated/wspace-operations';\nimport { TunnelDomainVerificationStatus } from '@/generated/wspace-types';\nimport { DomainVerificationBadge } from '@/components/tunnels/DomainVerificationBadge';\n\ntype TunnelDetails = NonNullable<GetTunnelQuery['tunnel']>;\n\nexport function TunnelDomainsTab({ tunnel }: { tunnel: TunnelDetails }) {\n const tr = useTr();\n const { refetch } = useGetTunnelQuery({ variables: { id: tunnel.id } });\n const [newHostname, setNewHostname] = useState('');\n const [addError, setAddError] = useState<string | null>(null);\n\n const [registerDomain, { loading: adding }] = useRegisterCustomTunnelDomainMutation({\n onCompleted: async (result) => {\n await refetch();\n if (result.registerCustomTunnelDomain.id) {\n await attachDomain({\n variables: {\n tunnelId: tunnel.id,\n domainId: result.registerCustomTunnelDomain.id,\n },\n });\n }\n setNewHostname('');\n setAddError(null);\n },\n onError: (err) => setAddError(err.message),\n });\n const [attachDomain] = useAttachTunnelDomainMutation({\n onCompleted: () => void refetch(),\n });\n const [detachDomain] = useDetachTunnelDomainMutation({\n onCompleted: () => void refetch(),\n });\n const [verifyDomain] = useVerifyTunnelDomainMutation({\n onCompleted: () => void refetch(),\n });\n\n const handleAdd = async () => {\n const trimmed = newHostname.trim().toLowerCase();\n if (!trimmed) return;\n await registerDomain({ variables: { input: { hostname: trimmed } } });\n };\n\n const domains = tunnel.domains ?? [];\n\n return (\n <div className=\"max-w-4xl\">\n <section className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 mb-6\">\n <h3 className=\"text-sm font-semibold text-text-primary mb-3\">\n {tr('vibecontrols.tunnels.domains.attachCustomDomain', 'Attach a custom domain')}\n </h3>\n <p className=\"text-xs text-text-secondary mb-3\">\n Register a user-owned FQDN. You'll be asked to create a TXT record to prove ownership\n before the domain can be attached.\n </p>\n <div className=\"flex gap-2\">\n <input\n aria-label=\"preview.example.com\"\n type=\"text\"\n value={newHostname}\n onChange={(e) => setNewHostname(e.target.value)}\n placeholder=\"preview.example.com\"\n className=\"flex-1 rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-border-focus focus:outline-none\"\n />\n <button\n type=\"button\"\n disabled={adding || !newHostname.trim()}\n onClick={() => void handleAdd()}\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 {adding\n ? tr('vibecontrols.tunnels.domains.adding', 'Adding...')\n : tr('vibecontrols.tunnels.domains.addDomain', 'Add domain')}\n </button>\n </div>\n {addError && <p className=\"mt-2 text-xs text-status-error-fg\">{addError}</p>}\n </section>\n\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface overflow-x-auto\">\n {domains.length === 0 ? (\n <div className=\"p-4 text-sm text-text-muted\">\n No domains attached. Add one above or reserve a *.vibetunnels.com subdomain from the\n Tunnels tab.\n </div>\n ) : (\n <table className=\"min-w-[480px] w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border-subtle bg-bg-sunken\">\n <th className=\"px-4 py-2 text-left text-xs font-medium text-text-muted\">Domain</th>\n <th className=\"px-4 py-2 text-left text-xs font-medium text-text-muted whitespace-nowrap\">\n Status\n </th>\n <th className=\"px-4 py-2 text-left text-xs font-medium text-text-muted whitespace-nowrap\">\n Type\n </th>\n <th className=\"px-4 py-2 text-right text-xs font-medium text-text-muted whitespace-nowrap\">\n Actions\n </th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-subtle\">\n {domains.map((domain) => (\n <Fragment key={domain.id}>\n <tr className=\"align-middle\">\n <td className=\"px-4 py-3\">\n <div className=\"font-mono text-sm text-text-primary break-all min-w-[140px]\">\n {domain.hostname}\n </div>\n </td>\n <td className=\"px-4 py-3 whitespace-nowrap\">\n <DomainVerificationBadge status={domain.verificationStatus} />\n </td>\n <td className=\"px-4 py-3 text-xs text-text-muted whitespace-nowrap\">\n {domain.type}\n {domain.isPrimary && <span className=\"ml-1\">• primary</span>}\n </td>\n <td className=\"px-4 py-3 whitespace-nowrap text-right\">\n <div className=\"flex gap-2 justify-end\">\n {domain.verificationStatus === TunnelDomainVerificationStatus.Pending && (\n <button\n type=\"button\"\n onClick={() =>\n void verifyDomain({ variables: { domainId: domain.id } })\n }\n className=\"rounded-md border border-border-default px-3 py-1 text-xs text-text-primary hover:bg-bg-elevated\"\n >\n Verify\n </button>\n )}\n <button\n type=\"button\"\n onClick={() =>\n void detachDomain({\n variables: { tunnelId: tunnel.id, domainId: domain.id },\n })\n }\n className=\"rounded-md border border-border-default px-3 py-1 text-xs text-text-primary hover:bg-bg-elevated\"\n >\n Detach\n </button>\n </div>\n </td>\n </tr>\n {domain.verificationStatus === TunnelDomainVerificationStatus.Pending &&\n domain.challengeRecordName &&\n domain.challengeToken && (\n <tr>\n <td colSpan={4} className=\"px-4 pb-3 pt-0\">\n <div className=\"rounded-md bg-bg-sunken p-2 text-xs text-text-secondary\">\n <div>Create this TXT record to verify:</div>\n <div className=\"font-mono text-text-primary mt-1 break-all\">\n {domain.challengeRecordName} IN TXT "{domain.challengeToken}\n "\n </div>\n </div>\n </td>\n </tr>\n )}\n </Fragment>\n ))}\n </tbody>\n </table>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TunnelDomainsTab.js","names":[],"sources":["../../../../src/pages/tunnels/tabs/TunnelDomainsTab.tsx"],"sourcesContent":["import { Fragment, useState } from 'react';\nimport type { GetTunnelQuery } from '@/generated/wspace-operations';\nimport { useTr } from '../../../shared/hooks/useTr';\nimport {\n useRegisterCustomTunnelDomainMutation,\n useAttachTunnelDomainMutation,\n useDetachTunnelDomainMutation,\n useVerifyTunnelDomainMutation,\n useGetTunnelQuery,\n} from '@/generated/wspace-operations';\nimport { TunnelDomainVerificationStatus } from '@/generated/wspace-types';\nimport { DomainVerificationBadge } from '@/components/tunnels/DomainVerificationBadge';\n\ntype TunnelDetails = NonNullable<GetTunnelQuery['tunnel']>;\n\nexport function TunnelDomainsTab({\n tunnel,\n canCreate,\n canUpdate,\n}: {\n tunnel: TunnelDetails;\n canCreate: boolean;\n canUpdate: boolean;\n}) {\n const tr = useTr();\n const { refetch } = useGetTunnelQuery({ variables: { id: tunnel.id } });\n const [newHostname, setNewHostname] = useState('');\n const [addError, setAddError] = useState<string | null>(null);\n\n const [registerDomain, { loading: adding }] = useRegisterCustomTunnelDomainMutation({\n onCompleted: async (result) => {\n await refetch();\n if (result.registerCustomTunnelDomain.id) {\n await attachDomain({\n variables: {\n tunnelId: tunnel.id,\n domainId: result.registerCustomTunnelDomain.id,\n },\n });\n }\n setNewHostname('');\n setAddError(null);\n },\n onError: (err) => setAddError(err.message),\n });\n const [attachDomain] = useAttachTunnelDomainMutation({\n onCompleted: () => void refetch(),\n });\n const [detachDomain] = useDetachTunnelDomainMutation({\n onCompleted: () => void refetch(),\n });\n const [verifyDomain] = useVerifyTunnelDomainMutation({\n onCompleted: () => void refetch(),\n });\n\n const handleAdd = async () => {\n const trimmed = newHostname.trim().toLowerCase();\n if (!trimmed) return;\n await registerDomain({ variables: { input: { hostname: trimmed } } });\n };\n\n const domains = tunnel.domains ?? [];\n\n return (\n <div className=\"max-w-4xl\">\n {canCreate && (\n <section className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 mb-6\">\n <h3 className=\"text-sm font-semibold text-text-primary mb-3\">\n {tr('vibecontrols.tunnels.domains.attachCustomDomain', 'Attach a custom domain')}\n </h3>\n <p className=\"text-xs text-text-secondary mb-3\">\n Register a user-owned FQDN. You'll be asked to create a TXT record to prove ownership\n before the domain can be attached.\n </p>\n <div className=\"flex gap-2\">\n <input\n aria-label=\"preview.example.com\"\n type=\"text\"\n value={newHostname}\n onChange={(e) => setNewHostname(e.target.value)}\n placeholder=\"preview.example.com\"\n className=\"flex-1 rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:border-border-focus focus:outline-none\"\n />\n <button\n type=\"button\"\n disabled={adding || !newHostname.trim()}\n onClick={() => void handleAdd()}\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 {adding\n ? tr('vibecontrols.tunnels.domains.adding', 'Adding...')\n : tr('vibecontrols.tunnels.domains.addDomain', 'Add domain')}\n </button>\n </div>\n {addError && <p className=\"mt-2 text-xs text-status-error-fg\">{addError}</p>}\n </section>\n )}\n\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface overflow-x-auto\">\n {domains.length === 0 ? (\n <div className=\"p-4 text-sm text-text-muted\">\n No domains attached. Add one above or reserve a *.vibetunnels.com subdomain from the\n Tunnels tab.\n </div>\n ) : (\n <table className=\"min-w-[480px] w-full text-sm\">\n <thead>\n <tr className=\"border-b border-border-subtle bg-bg-sunken\">\n <th className=\"px-4 py-2 text-left text-xs font-medium text-text-muted\">Domain</th>\n <th className=\"px-4 py-2 text-left text-xs font-medium text-text-muted whitespace-nowrap\">\n Status\n </th>\n <th className=\"px-4 py-2 text-left text-xs font-medium text-text-muted whitespace-nowrap\">\n Type\n </th>\n <th className=\"px-4 py-2 text-right text-xs font-medium text-text-muted whitespace-nowrap\">\n Actions\n </th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-subtle\">\n {domains.map((domain) => (\n <Fragment key={domain.id}>\n <tr className=\"align-middle\">\n <td className=\"px-4 py-3\">\n <div className=\"font-mono text-sm text-text-primary break-all min-w-[140px]\">\n {domain.hostname}\n </div>\n </td>\n <td className=\"px-4 py-3 whitespace-nowrap\">\n <DomainVerificationBadge status={domain.verificationStatus} />\n </td>\n <td className=\"px-4 py-3 text-xs text-text-muted whitespace-nowrap\">\n {domain.type}\n {domain.isPrimary && <span className=\"ml-1\">• primary</span>}\n </td>\n <td className=\"px-4 py-3 whitespace-nowrap text-right\">\n <div className=\"flex gap-2 justify-end\">\n {canUpdate &&\n domain.verificationStatus === TunnelDomainVerificationStatus.Pending && (\n <button\n type=\"button\"\n onClick={() =>\n void verifyDomain({ variables: { domainId: domain.id } })\n }\n className=\"rounded-md border border-border-default px-3 py-1 text-xs text-text-primary hover:bg-bg-elevated\"\n >\n Verify\n </button>\n )}\n {canUpdate && (\n <button\n type=\"button\"\n onClick={() =>\n void detachDomain({\n variables: { tunnelId: tunnel.id, domainId: domain.id },\n })\n }\n className=\"rounded-md border border-border-default px-3 py-1 text-xs text-text-primary hover:bg-bg-elevated\"\n >\n Detach\n </button>\n )}\n </div>\n </td>\n </tr>\n {domain.verificationStatus === TunnelDomainVerificationStatus.Pending &&\n domain.challengeRecordName &&\n domain.challengeToken && (\n <tr>\n <td colSpan={4} className=\"px-4 pb-3 pt-0\">\n <div className=\"rounded-md bg-bg-sunken p-2 text-xs text-text-secondary\">\n <div>Create this TXT record to verify:</div>\n <div className=\"font-mono text-text-primary mt-1 break-all\">\n {domain.challengeRecordName} IN TXT "{domain.challengeToken}\n "\n </div>\n </div>\n </td>\n </tr>\n )}\n </Fragment>\n ))}\n </tbody>\n </table>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|