@growthub/cli 0.14.9 → 0.14.11

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.
Files changed (61) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/callback/route.js +35 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/failure/route.js +35 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/schedule/route.js +423 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/connect/route.js +78 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/credentials/route.js +276 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/products/[productId]/resources/route.js +173 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/products/sync/route.js +347 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/sync/route.js +293 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/connect/route.js +7 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/sync/route.js +7 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/sync/route.js +197 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/apps/route.js +1 -1
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/patch/preflight/route.js +38 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +3 -20
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-api-record/route.js +3 -20
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflow/publish/route.js +407 -290
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflows/[providerId]/route.js +209 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceAddOnsMarketplace.jsx +806 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +141 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/CeoCockpit.jsx +15 -3
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +42 -5
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +5 -1
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +86 -20
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ScheduleCockpit.jsx +363 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +8 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +322 -1
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +2 -2
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/add-ons-client.jsx +197 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/page.jsx +23 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +1 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +734 -61
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +15 -10
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +2 -7
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +29 -19
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +8 -4
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/schedule-cockpit-console.js +287 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/scheduler-orchestration.js +449 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-secrets.js +77 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-readiness.js +583 -0
  40. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-callback.js +63 -0
  41. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-scheduler.js +519 -0
  42. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-ons.js +957 -0
  43. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-app-readiness.js +212 -0
  44. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +607 -63
  45. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-contract-compliance.js +168 -0
  46. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +21 -0
  47. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-operator-auth.js +32 -0
  48. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-patch-impact.js +133 -0
  49. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-provenance-lineage.js +214 -0
  50. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-stale-surfaces.js +217 -0
  51. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-workflow-impact.js +170 -0
  52. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/provider.png +0 -0
  53. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/qstash.png +0 -0
  54. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/redis.png +0 -0
  55. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/search.png +0 -0
  56. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/vector.png +0 -0
  57. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/scripts/scheduler-ingress-smoke.mjs +26 -0
  58. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +6 -0
  59. package/assets/worker-kits/growthub-custom-workspace-starter-v1/skills/governed-workspace-mutation/SKILL.md +3 -1
  60. package/dist/index.js +3024 -4191
  61. package/package.json +1 -1
@@ -0,0 +1,806 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import {
5
+ CheckCircle2,
6
+ ChevronDown,
7
+ Database,
8
+ ExternalLink,
9
+ Headphones,
10
+ PlugZap,
11
+ Search,
12
+ Server,
13
+ Settings,
14
+ X,
15
+ } from "lucide-react";
16
+ import {
17
+ MARKETPLACE_PROVIDERS,
18
+ findMarketplaceProviderRow,
19
+ findInstalledWorkspaceAddOns,
20
+ findWorkspaceAddOnRows,
21
+ getMarketplaceProduct,
22
+ } from "@/lib/workspace-add-ons";
23
+
24
+ function AddOnsSurface({
25
+ onConnectProvider,
26
+ onSyncProvider,
27
+ onSaveProviderCredentials,
28
+ onSyncProduct,
29
+ onCustomSetup,
30
+ installing = false,
31
+ activeAction = "",
32
+ errorMessage = "",
33
+ setupMessage = "",
34
+ envSignals = {},
35
+ workspaceConfig = {},
36
+ onClose,
37
+ shell = "page",
38
+ }) {
39
+ const [activePath, setActivePath] = useState("plugins");
40
+ const [selectedProvider, setSelectedProvider] = useState("");
41
+ const [installDrawer, setInstallDrawer] = useState("");
42
+ const [manageDrawer, setManageDrawer] = useState("");
43
+ const [region, setRegion] = useState("us-east-1");
44
+ const [plan, setPlan] = useState("free");
45
+ const [resourceOptions, setResourceOptions] = useState([]);
46
+ const [selectedResourceId, setSelectedResourceId] = useState("");
47
+ const [resourceLoading, setResourceLoading] = useState(false);
48
+ const [resourceMessage, setResourceMessage] = useState("");
49
+ const [installMode, setInstallMode] = useState("existing");
50
+ const [providerCredentialValues, setProviderCredentialValues] = useState({});
51
+ const persistenceAdapters = Array.isArray(envSignals.persistenceAdapters) ? envSignals.persistenceAdapters : [];
52
+ const installed = useMemo(() => findInstalledWorkspaceAddOns(workspaceConfig), [workspaceConfig]);
53
+ const selectedMarketplaceProvider = MARKETPLACE_PROVIDERS.find((provider) => provider.providerId === selectedProvider) || null;
54
+ const providerProductReadiness = envSignals.providerProductReadiness || {};
55
+ const productReadiness = selectedMarketplaceProvider && envSignals.providerProductReadiness?.[selectedMarketplaceProvider.providerId]
56
+ ? envSignals.providerProductReadiness[selectedMarketplaceProvider.providerId]
57
+ : [];
58
+ const providerRows = useMemo(() => Object.fromEntries(MARKETPLACE_PROVIDERS.map((provider) => [provider.providerId, findMarketplaceProviderRow(workspaceConfig, provider.providerId)])), [workspaceConfig]);
59
+ const providerRow = selectedMarketplaceProvider ? providerRows[selectedMarketplaceProvider.providerId] : null;
60
+ const providerConnected = Boolean(providerRow?.isConnectedProvider);
61
+ const providerVerified = Boolean(providerRow?.isVerifiedProvider);
62
+ const providerSetupStarted = Boolean(providerRow?.isSetupPendingProvider);
63
+ const providerSetupOpen = providerSetupStarted || Boolean(setupMessage);
64
+ const providerSetupMessage = setupMessage || providerRow?.lastResponse || "";
65
+ const providerSetupFields = Array.isArray(selectedMarketplaceProvider?.accountSetupFields)
66
+ ? selectedMarketplaceProvider.accountSetupFields
67
+ : [];
68
+ const providerSetupNeedsCredentials = providerSetupOpen && providerSetupFields.length > 0;
69
+ const providerSetupReady = providerSetupFields.every((field) => {
70
+ if (!field?.required) return true;
71
+ return Boolean(String(providerCredentialValues[field.id] || "").trim());
72
+ });
73
+ const allAddOnRows = useMemo(() => findWorkspaceAddOnRows(workspaceConfig), [workspaceConfig]);
74
+ const providerProducts = selectedMarketplaceProvider?.products || [];
75
+ const installedProviderRows = selectedMarketplaceProvider
76
+ ? installed.filter((row) => providerProducts.some((product) => product.productId === row.productId || product.integrationId === row.integrationId))
77
+ : [];
78
+ const installedIds = new Set(installedProviderRows.map((row) => String(row.productId || "").trim()));
79
+ const activeProduct = selectedMarketplaceProvider ? getMarketplaceProduct(selectedMarketplaceProvider.providerId, installDrawer) : null;
80
+ const managedProduct = selectedMarketplaceProvider ? getMarketplaceProduct(selectedMarketplaceProvider.providerId, manageDrawer) : null;
81
+ const managedSavedRow = managedProduct
82
+ ? allAddOnRows.find((row) => row.productId === managedProduct.productId || row.integrationId === managedProduct.integrationId)
83
+ : null;
84
+ const createResourceDividerLabel = activeProduct?.resourceDiscovery?.createDividerLabel || "";
85
+ const hasExistingResources = providerConnected && resourceOptions.length > 0;
86
+ const showCreateNewOptions = !hasExistingResources || installMode === "new";
87
+ const activeReadiness = productReadiness.find((item) => item.productId === activeProduct?.productId) || null;
88
+ const activeSavedRow = allAddOnRows.find((row) => row.productId === activeProduct?.productId) || null;
89
+ const productInstalled = Boolean(activeSavedRow?.isVerifiedAddOn);
90
+ const providerAccountLabel = providerRow?.Name || selectedMarketplaceProvider?.label || "Provider account";
91
+ const providerAccountRef = providerRow?.authRef || selectedMarketplaceProvider?.authRef || selectedMarketplaceProvider?.providerId || "";
92
+ const providerAccountOptions = useMemo(() => {
93
+ const raw = providerRow?.providerAccountOptions;
94
+ if (Array.isArray(raw)) return raw;
95
+ if (typeof raw !== "string" || !raw.trim()) return [];
96
+ try {
97
+ const parsed = JSON.parse(raw);
98
+ return Array.isArray(parsed) ? parsed : [];
99
+ } catch {
100
+ return [];
101
+ }
102
+ }, [providerRow]);
103
+ const selectedProviderAccountId = providerRow?.selectedProviderAccountId || providerAccountOptions[0]?.id || "";
104
+ const regionOptions = activeProduct?.regionOptions || [];
105
+ const selectedRegion = regionOptions.find((option) => option.id === region) || regionOptions[0] || { id: region, label: region };
106
+ const readyAdapters = persistenceAdapters.filter((adapter) => adapter.configured);
107
+ const customProviderReady = readyAdapters.length > 0;
108
+ const currentSectionLabel = activePath === "custom" ? "Custom" : "Plugins";
109
+ const selectedProviderLabel = selectedProvider === "custom" ? "Custom Plugin" : (selectedMarketplaceProvider?.label || "Provider");
110
+ const canSyncProduct = Boolean(onSyncProduct);
111
+ const canConnectProvider = Boolean(onConnectProvider);
112
+ const canSyncProvider = Boolean(onSyncProvider);
113
+ const canSaveProviderCredentials = Boolean(onSaveProviderCredentials);
114
+ const inModal = shell === "modal";
115
+ const details = [
116
+ ["Installed products", String(installed.filter((row) => !selectedMarketplaceProvider || providerProducts.some((product) => product.productId === row.productId)).length)],
117
+ ["Developer", selectedMarketplaceProvider?.developer || "Provider"],
118
+ ["Website", selectedMarketplaceProvider?.websiteUrl || ""],
119
+ ["Documentation", "Read"],
120
+ ["Terms", "Read"],
121
+ ["Privacy Policy", "Read"],
122
+ ["Support", selectedMarketplaceProvider?.supportUrl ? "Open support" : ""],
123
+ ];
124
+
125
+ function syncProduct() {
126
+ if (!selectedMarketplaceProvider || !activeProduct) return;
127
+ const selectedResource = resourceOptions.find((item) => item.id === selectedResourceId) || null;
128
+ onSyncProduct?.({
129
+ providerId: selectedMarketplaceProvider.providerId,
130
+ productId: activeProduct.productId,
131
+ region: installMode === "existing" ? selectedResource?.region || region : region,
132
+ plan: installMode === "existing" ? selectedResource?.type || plan : plan,
133
+ selectedResourceId: installMode === "existing" ? selectedResource?.id || "" : "",
134
+ selectedResourceLabel: installMode === "existing" ? selectedResource?.label || "" : "",
135
+ selectedResourceSource: installMode === "existing" ? selectedResource?.source || "" : "",
136
+ });
137
+ }
138
+
139
+ function syncManagedProduct() {
140
+ if (!selectedMarketplaceProvider || !managedProduct) return;
141
+ onSyncProduct?.({
142
+ providerId: selectedMarketplaceProvider.providerId,
143
+ productId: managedProduct.productId,
144
+ region: managedSavedRow?.region || region,
145
+ plan: managedSavedRow?.plan || plan,
146
+ selectedResourceId: managedSavedRow?.selectedResourceId || "",
147
+ selectedResourceLabel: managedSavedRow?.selectedResourceLabel || "",
148
+ selectedResourceSource: managedSavedRow?.selectedResourceSource || "",
149
+ });
150
+ }
151
+
152
+ function syncProvider() {
153
+ if (!selectedMarketplaceProvider) return;
154
+ onSyncProvider?.({ providerId: selectedMarketplaceProvider.providerId });
155
+ }
156
+
157
+ function connectProvider() {
158
+ if (!selectedMarketplaceProvider) return;
159
+ const setupUrl = selectedMarketplaceProvider.accountSetupUrl || selectedMarketplaceProvider.consoleUrl;
160
+ if (setupUrl) window.open(setupUrl, `${selectedMarketplaceProvider.providerId}-provider-setup`, "popup,width=1160,height=820");
161
+ onConnectProvider?.({ providerId: selectedMarketplaceProvider.providerId, openedExternally: Boolean(setupUrl) });
162
+ }
163
+
164
+ function saveProviderCredentials() {
165
+ if (!selectedMarketplaceProvider) return;
166
+ onSaveProviderCredentials?.({
167
+ providerId: selectedMarketplaceProvider.providerId,
168
+ credentials: providerCredentialValues,
169
+ });
170
+ }
171
+
172
+ function updateProviderCredential(fieldId, value) {
173
+ setProviderCredentialValues((current) => ({ ...current, [fieldId]: value }));
174
+ }
175
+
176
+ function openProvider(providerId) {
177
+ setSelectedProvider(providerId);
178
+ setInstallDrawer("");
179
+ setManageDrawer("");
180
+ }
181
+
182
+ function closeProvider() {
183
+ setSelectedProvider("");
184
+ setInstallDrawer("");
185
+ setManageDrawer("");
186
+ }
187
+
188
+ function switchPath(path) {
189
+ setActivePath(path);
190
+ setSelectedProvider("");
191
+ setInstallDrawer("");
192
+ setManageDrawer("");
193
+ }
194
+
195
+ function ProductIcon({ product, provider = false }) {
196
+ const src = provider ? selectedMarketplaceProvider?.iconSrc : product?.iconSrc;
197
+ return (
198
+ <span className={`dm-marketplace-product-icon ${provider ? "is-provider" : (product?.iconClass || "is-provider")}`}>
199
+ {src ? <img src={src} alt="" aria-hidden="true" /> : <PlugZap size={18} />}
200
+ </span>
201
+ );
202
+ }
203
+
204
+ useEffect(() => {
205
+ if (!selectedMarketplaceProvider || !activeProduct || !providerConnected) {
206
+ setResourceOptions([]);
207
+ setSelectedResourceId("");
208
+ setResourceMessage("");
209
+ return undefined;
210
+ }
211
+ let cancelled = false;
212
+ async function loadResources() {
213
+ setResourceLoading(true);
214
+ setResourceMessage("");
215
+ try {
216
+ const response = await fetch(`/api/workspace/add-ons/providers/${encodeURIComponent(selectedMarketplaceProvider.providerId)}/products/${encodeURIComponent(activeProduct.productId)}/resources`, {
217
+ method: "GET",
218
+ });
219
+ const payload = await response.json().catch(() => ({}));
220
+ if (cancelled) return;
221
+ if (!response.ok) {
222
+ setResourceOptions([]);
223
+ setSelectedResourceId("");
224
+ setResourceMessage(payload?.error || "No provider resources were returned for this product.");
225
+ return;
226
+ }
227
+ const resources = Array.isArray(payload.resources) ? payload.resources : [];
228
+ setResourceOptions(resources);
229
+ setSelectedResourceId(resources[0]?.id || "");
230
+ setInstallMode(resources.length ? "existing" : "new");
231
+ setResourceMessage(resources.length ? "" : "No existing provider resources were returned for this product.");
232
+ } catch (error) {
233
+ if (!cancelled) {
234
+ setResourceOptions([]);
235
+ setSelectedResourceId("");
236
+ setResourceMessage(error?.message || "Provider resources could not be loaded.");
237
+ }
238
+ } finally {
239
+ if (!cancelled) setResourceLoading(false);
240
+ }
241
+ }
242
+ loadResources();
243
+ return () => {
244
+ cancelled = true;
245
+ };
246
+ }, [selectedMarketplaceProvider?.providerId, activeProduct?.productId, providerConnected]);
247
+
248
+ useEffect(() => {
249
+ setProviderCredentialValues({});
250
+ }, [selectedMarketplaceProvider?.providerId]);
251
+
252
+ return (
253
+ <section className={inModal ? "dm-marketplace-modal" : "dm-marketplace-page"} role={inModal ? "dialog" : undefined} aria-modal={inModal ? "true" : undefined} aria-labelledby="workspace-marketplace-title">
254
+ <header className="dm-marketplace-header">
255
+ <div>
256
+ <nav className="dm-marketplace-breadcrumbs" aria-label="Add-ons breadcrumbs">
257
+ <span>Workspace Marketplace</span>
258
+ <ChevronDown size={12} aria-hidden="true" />
259
+ {selectedProvider ? <button type="button" onClick={closeProvider}>{currentSectionLabel}</button> : <span>{currentSectionLabel}</span>}
260
+ {selectedProvider ? (
261
+ <>
262
+ <ChevronDown size={12} aria-hidden="true" />
263
+ <span>{selectedProviderLabel}</span>
264
+ <ChevronDown size={12} aria-hidden="true" />
265
+ <strong>Installation</strong>
266
+ </>
267
+ ) : null}
268
+ </nav>
269
+ {selectedProvider ? (
270
+ <div className="dm-marketplace-provider-title">
271
+ {selectedProvider === "custom" ? <span className="dm-marketplace-product-icon is-custom"><Database size={18} /></span> : <ProductIcon provider />}
272
+ <div>
273
+ <h2 id="workspace-marketplace-title">{selectedProviderLabel}</h2>
274
+ <p>{selectedProvider === "custom" ? "Governed custom plugins and thin adapters" : "Serverless DB (Redis, Vector, Queue, Search)"}</p>
275
+ </div>
276
+ </div>
277
+ ) : (
278
+ <div>
279
+ <h2 id="workspace-marketplace-title">{currentSectionLabel}</h2>
280
+ <p className="dm-marketplace-subtitle">{activePath === "custom" ? "Register custom providers at the workspace level, then normalize each plugin through governed workspace objects." : "Install provider plugins at the workspace level, then configure products inside each provider."}</p>
281
+ </div>
282
+ )}
283
+ </div>
284
+ {selectedMarketplaceProvider ? <div className="dm-marketplace-provider-actions">
285
+ <button type="button" className="dm-btn-outline">Build in <PlugZap size={13} /></button>
286
+ {selectedMarketplaceProvider.supportUrl ? <a className="dm-btn-outline" href={selectedMarketplaceProvider.supportUrl} target="_blank" rel="noreferrer">Support <Headphones size={13} /></a> : null}
287
+ {providerConnected && installed.some((row) => providerProducts.some((product) => product.productId === row.productId)) && selectedMarketplaceProvider.consoleUrl ? <a className="dm-btn-primary-sm" href={selectedMarketplaceProvider.consoleUrl} target="_blank" rel="noreferrer">Open provider <ExternalLink size={13} /></a> : null}
288
+ </div> : null}
289
+ {onClose ? (
290
+ <button type="button" className="dm-workflow-icon-btn" aria-label="Close Workspace Marketplace" onClick={onClose}>
291
+ <X size={14} />
292
+ </button>
293
+ ) : null}
294
+ </header>
295
+ <div className="dm-marketplace-layout">
296
+ <aside className="dm-marketplace-sidebar" aria-label="Marketplace sections">
297
+ <button type="button" className={activePath === "plugins" ? "is-active" : ""} onClick={() => switchPath("plugins")}>
298
+ <PlugZap size={14} /> Plugins
299
+ </button>
300
+ <button type="button" className={activePath === "custom" ? "is-active" : ""} onClick={() => switchPath("custom")}>
301
+ <Database size={14} /> Custom
302
+ </button>
303
+ {selectedProvider ? <div className="dm-marketplace-setup-nav" aria-label="Install path">
304
+ <span>Install path</span>
305
+ <ol>
306
+ <li className={installDrawer ? "is-active" : ""}>Install</li>
307
+ <li>Setup</li>
308
+ <li>Login/Auth</li>
309
+ <li>Sync</li>
310
+ </ol>
311
+ </div> : null}
312
+ </aside>
313
+ <div className="dm-marketplace-content">
314
+ {errorMessage ? <div className="dm-marketplace-error" role="alert">{errorMessage}</div> : null}
315
+ {activePath === "plugins" ? (
316
+ <>
317
+ {!selectedProvider ? (
318
+ <div className="dm-marketplace-search-row">
319
+ <div className="dm-marketplace-search"><Search size={14} /><span>Search plugins</span></div>
320
+ <button type="button" className="dm-marketplace-filter">Filter by <ChevronDown size={13} /></button>
321
+ <button type="button" className="dm-marketplace-filter">Sort by <ChevronDown size={13} /></button>
322
+ </div>
323
+ ) : null}
324
+
325
+ {!selectedProvider ? (
326
+ <section className="dm-marketplace-products" aria-label="Plugin providers">
327
+ <h3>Plugin Providers</h3>
328
+ <div className="dm-marketplace-provider-grid">
329
+ {MARKETPLACE_PROVIDERS.map((provider) => {
330
+ const row = providerRows[provider.providerId];
331
+ const connected = Boolean(row?.isConnectedProvider);
332
+ const verified = Boolean(row?.isVerifiedProvider);
333
+ const setupStarted = Boolean(row?.isSetupPendingProvider);
334
+ const installedCount = installed.filter((installedRow) => provider.products.some((product) => product.productId === installedRow.productId)).length;
335
+ const stateLabel = verified ? "Verified" : setupStarted ? "Setup opened" : "Provider setup required";
336
+ return (
337
+ <button type="button" className="dm-marketplace-provider-card" key={provider.providerId} onClick={() => openProvider(provider.providerId)}>
338
+ <span className="dm-marketplace-product-icon is-provider">
339
+ {provider.iconSrc ? <img src={provider.iconSrc} alt="" aria-hidden="true" /> : <PlugZap size={18} />}
340
+ </span>
341
+ <div>
342
+ <strong>{provider.label}</strong>
343
+ <p>{provider.providerProductsLabel || provider.description}</p>
344
+ <small>{connected ? `${stateLabel} · ${installedCount} installed product${installedCount === 1 ? "" : "s"}` : stateLabel}</small>
345
+ </div>
346
+ <span className="dm-btn-outline">{connected ? "Manage" : setupStarted ? "Continue setup" : "Install"}</span>
347
+ </button>
348
+ );
349
+ })}
350
+ </div>
351
+ </section>
352
+ ) : (
353
+ <div className="dm-marketplace-provider-layout">
354
+ <div className="dm-marketplace-provider-main">
355
+ {!providerConnected ? (
356
+ <section className="dm-marketplace-install-card" aria-label={`${selectedMarketplaceProvider?.label || "Provider"} setup`}>
357
+ <div className="dm-marketplace-product-head">
358
+ <ProductIcon provider />
359
+ <div>
360
+ <h3>Install {selectedMarketplaceProvider?.label || "provider"}</h3>
361
+ <p>Connect the provider account once before installing products for this workspace.</p>
362
+ </div>
363
+ <span className="dm-db-status"><span />Account setup</span>
364
+ </div>
365
+ <div className="dm-marketplace-config">
366
+ <p className="dm-marketplace-section-title"><Server size={14} /> Provider Account</p>
367
+ <div className="dm-marketplace-env is-setup">
368
+ <span>Provider account</span>
369
+ <code>{selectedMarketplaceProvider?.authRef || selectedMarketplaceProvider?.providerId || "provider-auth"}</code>
370
+ </div>
371
+ <p className="dm-cockpit-step-hint">Connect the provider account for this workspace. Product setup unlocks after the provider account is available to the workspace.</p>
372
+ {providerSetupMessage ? <p className="dm-cockpit-step-hint">{providerSetupMessage}</p> : null}
373
+ {providerSetupNeedsCredentials ? (
374
+ <div className="dm-marketplace-credential-grid">
375
+ {providerSetupFields.map((field) => (
376
+ <label className="dm-marketplace-field" key={field.id}>
377
+ <span>{field.label || field.id}</span>
378
+ <input
379
+ value={providerCredentialValues[field.id] || ""}
380
+ onChange={(event) => updateProviderCredential(field.id, event.target.value)}
381
+ type={field.type || "text"}
382
+ autoComplete={field.autocomplete || "off"}
383
+ placeholder={field.placeholder || ""}
384
+ />
385
+ </label>
386
+ ))}
387
+ </div>
388
+ ) : null}
389
+ </div>
390
+ <div className="dm-marketplace-provision-steps">
391
+ <div className="is-active dm-marketplace-step-action-row">
392
+ <span>Install provider</span>
393
+ <button type="button" className="dm-btn-primary-sm" disabled={installing || !canConnectProvider} onClick={connectProvider}>
394
+ {activeAction === "connect" ? "Opening..." : `Set up ${selectedMarketplaceProvider?.label || "provider"}`}
395
+ </button>
396
+ </div>
397
+ {providerSetupNeedsCredentials ? <div className="dm-marketplace-step-action-row">
398
+ <span>Account setup</span>
399
+ <button type="button" className="dm-btn-primary-sm" disabled={installing || !canSaveProviderCredentials || !providerSetupReady} onClick={saveProviderCredentials}>
400
+ {activeAction === "save-provider" ? "Verifying..." : "Verify and save account"}
401
+ </button>
402
+ </div> : <div>Account setup</div>}
403
+ <div>{activeAction === "sync-provider" ? "Syncing account" : "Account sync"}</div>
404
+ <div>Product marketplace</div>
405
+ </div>
406
+ </section>
407
+ ) : null}
408
+
409
+ {providerConnected && installedProviderRows.length ? (
410
+ <section className="dm-marketplace-products" aria-label="Installed Products">
411
+ <h3>Installed Products</h3>
412
+ <div className="dm-marketplace-product-grid">
413
+ {installedProviderRows.map((row) => {
414
+ const product = getMarketplaceProduct(selectedMarketplaceProvider.providerId, row.productId) || getMarketplaceProduct(selectedMarketplaceProvider.providerId, row.integrationId);
415
+ if (!product) return null;
416
+ return (
417
+ <article className="dm-marketplace-product-card" key={row.integrationId}>
418
+ <ProductIcon product={product} />
419
+ <div>
420
+ <strong>{row.Name || product.label}</strong>
421
+ <p>{row.selectedResourceLabel || row.status || "draft"} / {row.syncCheckedAt || row.lastTested || "not synced"}</p>
422
+ </div>
423
+ <div className="dm-marketplace-card-actions">
424
+ <button type="button" className="dm-workflow-icon-btn dm-marketplace-gear" aria-label={`Manage ${product.label}`} onClick={() => {
425
+ setInstallDrawer("");
426
+ setManageDrawer(product.productId);
427
+ }}>
428
+ <Settings size={14} />
429
+ </button>
430
+ <span className="dm-db-status ok"><span />Installed</span>
431
+ </div>
432
+ </article>
433
+ );})}
434
+ </div>
435
+ </section>
436
+ ) : null}
437
+
438
+ {providerConnected ? <section className="dm-marketplace-products" aria-label="More Products">
439
+ <h3>More Products</h3>
440
+ <div className="dm-marketplace-product-grid">
441
+ {providerProducts.map((product) => {
442
+ const readiness = productReadiness.find((item) => item.productId === product.productId);
443
+ const isInstalled = installedIds.has(product.productId);
444
+ return (
445
+ <article className="dm-marketplace-product-card" key={product.productId}>
446
+ <ProductIcon product={product} />
447
+ <div>
448
+ <strong>{product.label}</strong>
449
+ <p>{product.subtitle}</p>
450
+ <small>{product.plans}</small>
451
+ <small>{readiness?.configured ? "Product refs ready" : "Set up product refs"}</small>
452
+ </div>
453
+ <button
454
+ type="button"
455
+ className="dm-btn-outline"
456
+ onClick={() => {
457
+ if (isInstalled) {
458
+ setInstallDrawer("");
459
+ setManageDrawer(product.productId);
460
+ } else {
461
+ setManageDrawer("");
462
+ setInstallDrawer(product.productId);
463
+ }
464
+ }}
465
+ >
466
+ {isInstalled ? "Manage" : "Install"}
467
+ </button>
468
+ </article>
469
+ );
470
+ })}
471
+ </div>
472
+ </section> : null}
473
+
474
+ <section className="dm-marketplace-overview" aria-label="Overview">
475
+ <h3>Overview</h3>
476
+ <p>{providerConnected ? "Install provider products into workflow, data, and retrieval surfaces through the shared API Registry." : "Install the provider account first. Product installation stays locked until the account is linked."}</p>
477
+ </section>
478
+ </div>
479
+ <aside className="dm-marketplace-details" aria-label="Provider details">
480
+ <h3>Details</h3>
481
+ {details.map(([label, value]) => (
482
+ <div key={label}>
483
+ <span>{label}</span>
484
+ {label === "Website" && selectedMarketplaceProvider?.websiteUrl ? <a href={selectedMarketplaceProvider.websiteUrl} target="_blank" rel="noreferrer">{value}</a>
485
+ : label === "Documentation" && selectedMarketplaceProvider?.docsUrl ? <a href={selectedMarketplaceProvider.docsUrl} target="_blank" rel="noreferrer">{value} <ExternalLink size={11} /></a>
486
+ : label === "Terms" && selectedMarketplaceProvider?.termsUrl ? <a href={selectedMarketplaceProvider.termsUrl} target="_blank" rel="noreferrer">{value} <ExternalLink size={11} /></a>
487
+ : label === "Privacy Policy" && selectedMarketplaceProvider?.privacyUrl ? <a href={selectedMarketplaceProvider.privacyUrl} target="_blank" rel="noreferrer">{value} <ExternalLink size={11} /></a>
488
+ : label === "Support" && selectedMarketplaceProvider?.supportUrl ? <a href={selectedMarketplaceProvider.supportUrl} target="_blank" rel="noreferrer">{value} <ExternalLink size={11} /></a>
489
+ : <strong>{value}</strong>}
490
+ </div>
491
+ ))}
492
+ </aside>
493
+ </div>
494
+ )}
495
+ </>
496
+ ) : (
497
+ <>
498
+ {!selectedProvider ? (
499
+ <>
500
+ <div className="dm-marketplace-search-row">
501
+ <div className="dm-marketplace-search"><Search size={14} /><span>Search custom providers</span></div>
502
+ <button type="button" className="dm-marketplace-filter">Filter by <ChevronDown size={13} /></button>
503
+ <button type="button" className="dm-marketplace-filter">Sort by <ChevronDown size={13} /></button>
504
+ </div>
505
+ <section className="dm-marketplace-products" aria-label="Custom plugin providers">
506
+ <h3>Custom Providers</h3>
507
+ <div className="dm-marketplace-provider-grid">
508
+ <button type="button" className="dm-marketplace-provider-card" onClick={() => openProvider("custom")}>
509
+ <span className="dm-marketplace-product-icon is-custom"><Database size={18} /></span>
510
+ <div>
511
+ <strong>Custom Plugin</strong>
512
+ <p>Register owned workers, functions, APIs, and schedulers through the governed API Registry.</p>
513
+ <small>{customProviderReady ? `${readyAdapters.length} adapter${readyAdapters.length === 1 ? "" : "s"} ready` : "Provider setup required"}</small>
514
+ </div>
515
+ <span className="dm-btn-outline">{customProviderReady ? "Open" : "Install"}</span>
516
+ </button>
517
+ </div>
518
+ </section>
519
+ </>
520
+ ) : (
521
+ <div className="dm-marketplace-provider-layout">
522
+ <div className="dm-marketplace-provider-main">
523
+ <section className="dm-marketplace-install-card" aria-label="Custom provider setup">
524
+ <div className="dm-marketplace-product-head">
525
+ <span className="dm-marketplace-product-icon is-custom"><Database size={18} /></span>
526
+ <div>
527
+ <h3>Install Custom Plugin provider</h3>
528
+ <p>Set the governed adapter and object path once before adding custom workers, functions, APIs, or schedulers.</p>
529
+ </div>
530
+ <span className={customProviderReady ? "dm-db-status ok" : "dm-db-status"}><span />{customProviderReady ? "Ready" : "Account setup"}</span>
531
+ </div>
532
+ <div className="dm-marketplace-config">
533
+ <p className="dm-marketplace-section-title"><Server size={14} /> Provider Account</p>
534
+ <div className={customProviderReady ? "dm-marketplace-env is-ready" : "dm-marketplace-env is-setup"}>
535
+ <span>Governed adapter binding</span>
536
+ <code>{customProviderReady ? readyAdapters.map((adapter) => adapter.label).join(", ") : "Configure API Registry / workspace object normalization"}</code>
537
+ </div>
538
+ <div className="dm-marketplace-adapters">
539
+ {persistenceAdapters.map((adapter) => (
540
+ <div key={adapter.id} className={adapter.configured ? "dm-marketplace-adapter is-ready" : "dm-marketplace-adapter"}>
541
+ <div>
542
+ <strong>{adapter.label}</strong>
543
+ <span>{adapter.mode}</span>
544
+ </div>
545
+ <code>{adapter.configured ? "ready" : "setup required"}</code>
546
+ </div>
547
+ ))}
548
+ {!persistenceAdapters.length ? <p className="dm-cockpit-step-desc">No adapter signal returned yet. Reopen after env-status responds.</p> : null}
549
+ </div>
550
+ <p className="dm-cockpit-step-hint">Custom plugins enter through the existing governed API/Webhooks path so credentials stay server-side and workspace config stores only refs, object bindings, and registry metadata.</p>
551
+ </div>
552
+ <div className="dm-marketplace-provision-steps">
553
+ <div className="is-active">Install provider</div>
554
+ <div>Account setup</div>
555
+ <div>Governed object</div>
556
+ <div>Plugin products</div>
557
+ </div>
558
+ <footer className="dm-marketplace-actions">
559
+ <button type="button" className="dm-btn-primary-sm" disabled={installing || !onCustomSetup} onClick={onCustomSetup}>
560
+ Configure custom
561
+ </button>
562
+ </footer>
563
+ </section>
564
+
565
+ {customProviderReady ? (
566
+ <section className="dm-marketplace-products" aria-label="Custom plugin products">
567
+ <h3>Plugin Products</h3>
568
+ <div className="dm-marketplace-product-grid">
569
+ <article className="dm-marketplace-product-card">
570
+ <span className="dm-marketplace-product-icon is-custom"><Database size={18} /></span>
571
+ <div>
572
+ <strong>Add custom plugin</strong>
573
+ <p>Normalize an owned worker, edge function, API, or scheduler into the workspace governance model.</p>
574
+ <small>API Registry, source records, business object binding</small>
575
+ </div>
576
+ <button type="button" className="dm-btn-outline" disabled={installing || !onCustomSetup} onClick={onCustomSetup}>
577
+ Configure
578
+ </button>
579
+ </article>
580
+ </div>
581
+ </section>
582
+ ) : null}
583
+ </div>
584
+ <aside className="dm-marketplace-details" aria-label="Custom provider details">
585
+ <h3>Details</h3>
586
+ <div><span>Provider</span><strong>Custom</strong></div>
587
+ <div><span>Installed products</span><strong>0</strong></div>
588
+ <div><span>Governance</span><strong>API Registry</strong></div>
589
+ <div><span>Secrets</span><strong>Server-side refs</strong></div>
590
+ <div><span>Objects</span><strong>Workspace Data Model</strong></div>
591
+ </aside>
592
+ </div>
593
+ )}
594
+ </>
595
+ )}
596
+ </div>
597
+ </div>
598
+
599
+ {manageDrawer && managedProduct ? (
600
+ <div className="dm-marketplace-install-drawer" role="dialog" aria-modal="true" aria-label={`Manage ${managedProduct.label}`}>
601
+ <section className="dm-marketplace-install-card">
602
+ <header className="dm-marketplace-drawer-head">
603
+ <h3>Manage Product</h3>
604
+ <button type="button" className="dm-workflow-icon-btn" aria-label="Close manage drawer" onClick={() => setManageDrawer("")}>
605
+ <X size={14} />
606
+ </button>
607
+ </header>
608
+ <div className="dm-marketplace-product-head">
609
+ <ProductIcon product={managedProduct} />
610
+ <div>
611
+ <h3>{managedSavedRow?.Name || managedProduct.label}</h3>
612
+ <p>{managedProduct.subtitle || "Installed workspace add-on"}</p>
613
+ </div>
614
+ </div>
615
+ <div className="dm-marketplace-config">
616
+ <p className="dm-marketplace-section-title"><Server size={14} /> Installed Binding</p>
617
+ <div className="dm-marketplace-config-summary">
618
+ <div><span>Status</span><code>{managedSavedRow?.isVerifiedAddOn ? "verified" : managedSavedRow?.status || "setup required"}</code></div>
619
+ <div><span>Provider</span><code>{selectedMarketplaceProvider?.label || "Provider"}</code></div>
620
+ <div><span>Product</span><code>{managedProduct.shortLabel || managedProduct.label}</code></div>
621
+ <div><span>Resource</span><code>{managedSavedRow?.selectedResourceLabel || managedSavedRow?.selectedResourceId || "No existing resource binding stored"}</code></div>
622
+ <div><span>Resource source</span><code>{managedSavedRow?.selectedResourceSource || "not stored"}</code></div>
623
+ <div><span>Region</span><code>{managedSavedRow?.region || "not stored"}</code></div>
624
+ <div><span>Plan</span><code>{managedSavedRow?.plan || "not stored"}</code></div>
625
+ <div><span>Auth ref</span><code>{managedSavedRow?.authRef || managedProduct.authRef}</code></div>
626
+ <div><span>Resolved env</span><code>{managedSavedRow?.resolvedEnv || "not resolved"}</code></div>
627
+ <div><span>Last sync</span><code>{managedSavedRow?.syncCheckedAt || managedSavedRow?.lastTested || "not synced"}</code></div>
628
+ <div><span>Proof</span><code>{managedSavedRow?.syncProof || "no provider proof stored"}</code></div>
629
+ </div>
630
+ <p className="dm-cockpit-step-hint">This installed product row is the workspace binding used by workflow canvas upgrades and activation.</p>
631
+ </div>
632
+ <footer className="dm-marketplace-actions">
633
+ {managedProduct.consoleUrl ? <a className="dm-btn-outline dm-marketplace-console-link" href={managedProduct.consoleUrl} target="_blank" rel="noreferrer">
634
+ Open provider <ExternalLink size={13} />
635
+ </a> : null}
636
+ <button type="button" className="dm-btn-outline" onClick={() => setManageDrawer("")}>Close</button>
637
+ <button type="button" className="dm-btn-primary-sm" disabled={installing || !providerConnected || !canSyncProduct || !managedSavedRow} onClick={syncManagedProduct}>
638
+ {activeAction === "sync-product" ? "Resyncing..." : "Resync product"}
639
+ </button>
640
+ </footer>
641
+ </section>
642
+ </div>
643
+ ) : null}
644
+
645
+ {installDrawer && activeProduct ? (
646
+ <div className="dm-marketplace-install-drawer" role="dialog" aria-modal="true" aria-label={`Install ${activeProduct.label}`}>
647
+ <section className="dm-marketplace-install-card">
648
+ <header className="dm-marketplace-drawer-head">
649
+ <h3>Install Integration</h3>
650
+ <button type="button" className="dm-workflow-icon-btn" aria-label="Close install drawer" onClick={() => setInstallDrawer("")}>
651
+ <X size={14} />
652
+ </button>
653
+ </header>
654
+ <div className="dm-marketplace-product-head">
655
+ <ProductIcon product={activeProduct} />
656
+ <div>
657
+ <h3>{activeProduct.label}</h3>
658
+ <p>{activeProduct.subtitle || "Workspace add-on"}</p>
659
+ </div>
660
+ </div>
661
+ <div className="dm-marketplace-config">
662
+ <p className="dm-marketplace-section-title"><Server size={14} /> Configuration and Plan</p>
663
+ {regionOptions.length ? (
664
+ <>
665
+ <label className="dm-marketplace-field">
666
+ <span>Primary Region</span>
667
+ <select value={region} onChange={(event) => setRegion(event.target.value)}>
668
+ {regionOptions.map((option) => <option key={option.id} value={option.id}>{option.label}</option>)}
669
+ </select>
670
+ </label>
671
+ <p className="dm-cockpit-step-hint">Select the deployment region for this product.</p>
672
+ </>
673
+ ) : (
674
+ <p className="dm-cockpit-step-hint">Sync registers the workspace row after the provider account is connected.</p>
675
+ )}
676
+ <label className="dm-marketplace-toggle">
677
+ <input type="checkbox" disabled />
678
+ <span>Prod Pack (+$200 per month)</span>
679
+ </label>
680
+ <p className="dm-cockpit-step-hint">Recommended for production. Configure paid plan changes in the provider account.</p>
681
+ {providerConnected ? (
682
+ providerAccountOptions.length ? (
683
+ <label className="dm-marketplace-field">
684
+ <span>Connected {selectedMarketplaceProvider?.label || "provider"} account</span>
685
+ <select value={selectedProviderAccountId} onChange={() => {}}>
686
+ {providerAccountOptions.map((account) => (
687
+ <option key={account.id} value={account.id}>
688
+ {account.label}{account.role ? ` - ${account.role}` : ""}{account.plan ? ` - ${account.plan}` : ""}
689
+ </option>
690
+ ))}
691
+ </select>
692
+ </label>
693
+ ) : (
694
+ <div className="dm-marketplace-account-unavailable">
695
+ <strong>Provider account details unavailable</strong>
696
+ <span>Account lookup needs this provider's account API credentials in this runtime. Product install still validates the selected product server-side.</span>
697
+ </div>
698
+ )
699
+ ) : null}
700
+ {providerConnected ? (
701
+ <div className="dm-marketplace-install-choice">
702
+ <div>
703
+ <strong>Use existing provider resource</strong>
704
+ <span>Select an existing account resource and bind it to this workspace.</span>
705
+ </div>
706
+ <label className="dm-marketplace-field">
707
+ <span>{resourceLoading ? "Loading existing resources" : "Existing provider resource"}</span>
708
+ <select value={selectedResourceId} onChange={(event) => {
709
+ setSelectedResourceId(event.target.value);
710
+ setInstallMode("existing");
711
+ }} disabled={resourceLoading || !resourceOptions.length}>
712
+ {resourceOptions.length ? resourceOptions.map((resource) => (
713
+ <option key={resource.id} value={resource.id}>
714
+ {resource.label}{resource.region ? ` - ${resource.region}` : ""}
715
+ </option>
716
+ )) : <option value="">No existing resource found</option>}
717
+ </select>
718
+ </label>
719
+ </div>
720
+ ) : null}
721
+ {providerConnected && resourceMessage ? <p className="dm-cockpit-step-hint">{resourceMessage}</p> : null}
722
+ {providerConnected && createResourceDividerLabel ? (
723
+ <div className="dm-marketplace-resource-divider" role="separator" aria-label={createResourceDividerLabel}>
724
+ <span>{createResourceDividerLabel}</span>
725
+ </div>
726
+ ) : null}
727
+ {showCreateNewOptions ? (
728
+ <div className="dm-marketplace-plan-list" aria-label="New resource plan">
729
+ {[
730
+ ["free", "Free", "Perfect for prototypes and hobby projects."],
731
+ ["payg", "Pay As You Go", "For use cases with bursting traffic."],
732
+ ["fixed-1m", "Fixed", "For businesses with consistent high-capacity loads."],
733
+ ].map(([id, label, desc]) => (
734
+ <button key={id} type="button" className={plan === id ? "dm-marketplace-plan is-selected" : "dm-marketplace-plan"} onClick={() => {
735
+ setInstallMode("new");
736
+ setPlan(id);
737
+ }}>
738
+ <span><b>{label}</b> {desc}</span>
739
+ {plan === id ? <CheckCircle2 size={15} /> : <span className="dm-marketplace-radio" />}
740
+ </button>
741
+ ))}
742
+ </div>
743
+ ) : (
744
+ <button type="button" className="dm-btn-outline dm-marketplace-create-new-btn" onClick={() => setInstallMode("new")}>
745
+ Configure new resource
746
+ </button>
747
+ )}
748
+ <div className={activeReadiness?.configured ? "dm-marketplace-env is-ready" : "dm-marketplace-env is-setup"}>
749
+ <span>{activeSavedRow?.isVerifiedAddOn ? "Product installed" : providerConnected ? "Ready to install product" : "Provider setup required"}</span>
750
+ <code>{installMode === "existing" && selectedResourceId ? (resourceOptions.find((item) => item.id === selectedResourceId)?.label || selectedResourceId) : regionOptions.length ? `${selectedRegion.label} / ${plan}` : `${activeProduct.shortLabel || activeProduct.label} / ${plan}`}</code>
751
+ </div>
752
+ {activeSavedRow ? (
753
+ <div className="dm-marketplace-config-summary">
754
+ <div><span>Status</span><code>{activeSavedRow.isVerifiedAddOn ? "verified" : "setup required"}</code></div>
755
+ <div><span>Auth ref</span><code>{activeSavedRow.authRef || activeProduct.authRef}</code></div>
756
+ <div><span>Base URL</span><code>{activeSavedRow.baseUrl || "pending provider sync"}</code></div>
757
+ <div><span>Last sync</span><code>{activeSavedRow.syncCheckedAt || activeSavedRow.lastTested || "not synced"}</code></div>
758
+ <div><span>Proof</span><code>{activeSavedRow.syncProof || "no provider proof stored"}</code></div>
759
+ </div>
760
+ ) : null}
761
+ <div className="dm-cockpit-step-hint">
762
+ {providerConnected
763
+ ? "Install calls the product sync route, validates the product credentials server-side, and writes the product API Registry row into workspace config."
764
+ : "Set up the provider account first. Return here after provider setup, then Sync provider to unlock product install."}
765
+ </div>
766
+ </div>
767
+ <div className="dm-marketplace-provision-steps">
768
+ <div className={`${providerConnected ? "is-complete" : "is-active"} dm-marketplace-step-action-row`}>
769
+ <span>Provider account</span>
770
+ {!providerConnected ? (
771
+ <button type="button" className="dm-btn-primary-sm" disabled={installing || !canConnectProvider} onClick={connectProvider}>
772
+ {activeAction === "connect" ? "Opening..." : "Set up provider account"}
773
+ </button>
774
+ ) : null}
775
+ </div>
776
+ <div className={productInstalled ? "is-complete" : providerConnected ? "is-active" : ""}>Product install</div>
777
+ </div>
778
+ <footer className="dm-marketplace-actions">
779
+ {activeSavedRow?.isVerifiedAddOn && activeProduct.consoleUrl ? <a className="dm-btn-outline dm-marketplace-console-link" href={activeProduct.consoleUrl} target="_blank" rel="noreferrer">
780
+ Open provider <ExternalLink size={13} />
781
+ </a> : null}
782
+ <button type="button" className="dm-btn-outline" onClick={() => setInstallDrawer("")}>Cancel</button>
783
+ <button type="button" className="dm-btn-primary-sm" disabled={installing || !providerConnected || !canSyncProduct} onClick={syncProduct}>
784
+ {activeAction === "sync-product" ? "Installing..." : activeSavedRow ? "Resync product" : "Install product"}
785
+ </button>
786
+ </footer>
787
+ </section>
788
+ </div>
789
+ ) : null}
790
+ </section>
791
+ );
792
+ }
793
+
794
+ function WorkspaceAddOnsMarketplace(props) {
795
+ if (props.open === false) return null;
796
+ if (props.shell === "modal") {
797
+ return <div className="dm-marketplace-backdrop" role="presentation">
798
+ <AddOnsSurface {...props} />
799
+ </div>;
800
+ }
801
+ return <AddOnsSurface {...props} />;
802
+ }
803
+
804
+ export {
805
+ WorkspaceAddOnsMarketplace
806
+ };