@ekanos/harness 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +879 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.js +11 -0
- package/dist/config.d.ts +18 -0
- package/dist/config.js +34 -0
- package/dist/internal/components/ask-assistant-bridge.d.ts +10 -0
- package/dist/internal/components/ask-assistant-bridge.js +50 -0
- package/dist/internal/components/dashboard-grid.d.ts +45 -0
- package/dist/internal/components/dashboard-grid.js +84 -0
- package/dist/internal/components/dev-toolbar.d.ts +15 -0
- package/dist/internal/components/dev-toolbar.js +155 -0
- package/dist/internal/components/harness-providers.d.ts +16 -0
- package/dist/internal/components/harness-providers.js +83 -0
- package/dist/internal/components/harness-widget-provider.d.ts +60 -0
- package/dist/internal/components/harness-widget-provider.js +84 -0
- package/dist/internal/components/i18n-provider.d.ts +9 -0
- package/dist/internal/components/i18n-provider.js +9 -0
- package/dist/internal/components/row-groups.d.ts +23 -0
- package/dist/internal/components/row-groups.js +37 -0
- package/dist/internal/components/surface-nav.d.ts +4 -0
- package/dist/internal/components/surface-nav.js +70 -0
- package/dist/internal/components/viewport-frame.d.ts +14 -0
- package/dist/internal/components/viewport-frame.js +27 -0
- package/dist/internal/components/widget-boundary.d.ts +25 -0
- package/dist/internal/components/widget-boundary.js +44 -0
- package/dist/internal/components/widget-surface.d.ts +20 -0
- package/dist/internal/components/widget-surface.js +76 -0
- package/dist/internal/lib/fonts.d.ts +2 -0
- package/dist/internal/lib/fonts.js +22 -0
- package/dist/internal/lib/harness-fetch-interceptor.d.ts +89 -0
- package/dist/internal/lib/harness-fetch-interceptor.js +101 -0
- package/dist/internal/lib/harness-live-fetch.d.ts +66 -0
- package/dist/internal/lib/harness-live-fetch.js +121 -0
- package/dist/internal/lib/harness-query-client.d.ts +43 -0
- package/dist/internal/lib/harness-query-client.js +103 -0
- package/dist/internal/lib/http-fixtures.d.ts +145 -0
- package/dist/internal/lib/http-fixtures.js +256 -0
- package/dist/internal/lib/i18n.d.ts +2 -0
- package/dist/internal/lib/i18n.js +17 -0
- package/dist/internal/lib/redact.d.ts +33 -0
- package/dist/internal/lib/redact.js +43 -0
- package/dist/internal/lib/toolbar-context.d.ts +59 -0
- package/dist/internal/lib/toolbar-context.js +124 -0
- package/dist/internal/registry-context.d.ts +27 -0
- package/dist/internal/registry-context.js +51 -0
- package/dist/internal/routes/activation-page.d.ts +33 -0
- package/dist/internal/routes/activation-page.js +242 -0
- package/dist/internal/routes/index-page.d.ts +13 -0
- package/dist/internal/routes/index-page.js +62 -0
- package/dist/internal/routes/integration-layout.d.ts +9 -0
- package/dist/internal/routes/integration-layout.js +84 -0
- package/dist/internal/routes/root-layout.d.ts +34 -0
- package/dist/internal/routes/root-layout.js +39 -0
- package/dist/internal/routes/single-widget-page.d.ts +6 -0
- package/dist/internal/routes/single-widget-page.js +30 -0
- package/dist/internal/routes/tile-page.d.ts +1 -0
- package/dist/internal/routes/tile-page.js +88 -0
- package/dist/internal/routes/triggers-page.d.ts +1 -0
- package/dist/internal/routes/triggers-page.js +386 -0
- package/dist/internal/routes/widgets-page.d.ts +1 -0
- package/dist/internal/routes/widgets-page.js +19 -0
- package/dist/internal/surfaces.d.ts +25 -0
- package/dist/internal/surfaces.js +29 -0
- package/dist/mocks/team-account-workspace.d.ts +78 -0
- package/dist/mocks/team-account-workspace.js +86 -0
- package/dist/registry.d.ts +418 -0
- package/dist/registry.js +82 -0
- package/dist/routes.d.ts +24 -0
- package/dist/routes.js +15 -0
- package/dist/styles.css +236 -0
- package/package.json +101 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { notFound } from "next/navigation";
|
|
5
|
+
import { Alert, AlertDescription, AlertTitle } from "@ekanos/ui/alert";
|
|
6
|
+
import { Badge } from "@ekanos/ui/badge";
|
|
7
|
+
import { Button } from "@ekanos/ui/button";
|
|
8
|
+
import {
|
|
9
|
+
Dialog,
|
|
10
|
+
DialogContent,
|
|
11
|
+
DialogDescription,
|
|
12
|
+
DialogHeader,
|
|
13
|
+
DialogTitle
|
|
14
|
+
} from "@ekanos/ui/dialog";
|
|
15
|
+
import { HARNESS_ACCOUNT_ID } from "../../registry.js";
|
|
16
|
+
import { hasSensitiveValues, redactSensitive } from "../lib/redact.js";
|
|
17
|
+
import { useHarnessIntegration } from "../registry-context.js";
|
|
18
|
+
function ActivationPage() {
|
|
19
|
+
const [flow, setFlow] = useState({ open: true, connected: false, result: null, revealed: false });
|
|
20
|
+
const integration = useHarnessIntegration();
|
|
21
|
+
if (!integration) notFound();
|
|
22
|
+
const ActivationForm = integration.activationForm;
|
|
23
|
+
const definition = integration.definition;
|
|
24
|
+
const capabilities = definition?.capabilities ?? [];
|
|
25
|
+
const permissions = definition?.permissions ?? [];
|
|
26
|
+
const summary = definition?.description ?? integration.description;
|
|
27
|
+
return /* @__PURE__ */ jsxs(
|
|
28
|
+
"main",
|
|
29
|
+
{
|
|
30
|
+
className: "mx-auto flex w-full max-w-3xl flex-col gap-6 px-6 py-8",
|
|
31
|
+
"data-test": "activation",
|
|
32
|
+
children: [
|
|
33
|
+
/* @__PURE__ */ jsxs(Alert, { children: [
|
|
34
|
+
/* @__PURE__ */ jsx(AlertTitle, { children: "Submissions go nowhere" }),
|
|
35
|
+
/* @__PURE__ */ jsx(AlertDescription, { children: /* @__PURE__ */ jsxs("p", { children: [
|
|
36
|
+
"The harness has no backend. Submitting shows you the",
|
|
37
|
+
" ",
|
|
38
|
+
/* @__PURE__ */ jsx("code", { children: "ActivationResult" }),
|
|
39
|
+
" your form handed back instead of posting it \u2014 which is the part worth checking, since that payload is what the real activation action would persist. The dialog, the capability and permission sections and the connected state are the real ones, rendered from your ",
|
|
40
|
+
/* @__PURE__ */ jsx("code", { children: "defineIntegration()" }),
|
|
41
|
+
" ",
|
|
42
|
+
"declaration."
|
|
43
|
+
] }) })
|
|
44
|
+
] }),
|
|
45
|
+
/* @__PURE__ */ jsxs("div", { className: "bg-card flex flex-wrap items-center justify-between gap-3 rounded-lg border p-5", children: [
|
|
46
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
47
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: flow.connected ? "Connected" : "Not connected" }),
|
|
48
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: flow.connected ? "The dialog now shows the manage view, whose only action is Disconnect." : "The dialog shows the scope disclosure and your activation form." })
|
|
49
|
+
] }),
|
|
50
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
51
|
+
/* @__PURE__ */ jsx(
|
|
52
|
+
Button,
|
|
53
|
+
{
|
|
54
|
+
variant: "outline",
|
|
55
|
+
size: "sm",
|
|
56
|
+
onClick: () => setFlow((prev) => ({ ...prev, open: true })),
|
|
57
|
+
"data-test": "activation-open-dialog",
|
|
58
|
+
children: "Open connect dialog"
|
|
59
|
+
}
|
|
60
|
+
),
|
|
61
|
+
/* @__PURE__ */ jsx(
|
|
62
|
+
Button,
|
|
63
|
+
{
|
|
64
|
+
variant: "ghost",
|
|
65
|
+
size: "sm",
|
|
66
|
+
onClick: () => setFlow({
|
|
67
|
+
open: false,
|
|
68
|
+
connected: false,
|
|
69
|
+
result: null,
|
|
70
|
+
revealed: false
|
|
71
|
+
}),
|
|
72
|
+
children: "Reset flow"
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
] })
|
|
76
|
+
] }),
|
|
77
|
+
ActivationForm ? /* @__PURE__ */ jsx(
|
|
78
|
+
Dialog,
|
|
79
|
+
{
|
|
80
|
+
open: flow.open,
|
|
81
|
+
onOpenChange: (next) => setFlow((prev) => ({ ...prev, open: next })),
|
|
82
|
+
children: /* @__PURE__ */ jsxs(DialogContent, { "data-test": "integration-connect-dialog", children: [
|
|
83
|
+
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
84
|
+
/* @__PURE__ */ jsx("span", { className: "font-heading bg-muted text-muted-foreground flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-base font-semibold", children: integration.name.trim().charAt(0).toUpperCase() || "?" }),
|
|
85
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
86
|
+
/* @__PURE__ */ jsx(DialogTitle, { children: integration.name }),
|
|
87
|
+
/* @__PURE__ */ jsx(DialogDescription, { children: summary })
|
|
88
|
+
] })
|
|
89
|
+
] }) }),
|
|
90
|
+
/* @__PURE__ */ jsxs("div", { className: "flex max-h-[70vh] flex-col gap-5 overflow-y-auto", children: [
|
|
91
|
+
/* @__PURE__ */ jsx(CapabilityList, { capabilities }),
|
|
92
|
+
/* @__PURE__ */ jsx(PermissionList, { permissions }),
|
|
93
|
+
flow.connected ? /* @__PURE__ */ jsx(
|
|
94
|
+
ConnectedState,
|
|
95
|
+
{
|
|
96
|
+
name: integration.name,
|
|
97
|
+
onDisconnect: () => setFlow({
|
|
98
|
+
open: true,
|
|
99
|
+
connected: false,
|
|
100
|
+
result: null,
|
|
101
|
+
revealed: false
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
) : (
|
|
105
|
+
/*
|
|
106
|
+
`inline` is REQUIRED. Without it `BaseActivationForm` renders
|
|
107
|
+
its own Dialog, which inside this one means nested dialogs —
|
|
108
|
+
exactly the constraint the real host comments on.
|
|
109
|
+
*/
|
|
110
|
+
/* @__PURE__ */ jsx(
|
|
111
|
+
ActivationForm,
|
|
112
|
+
{
|
|
113
|
+
accountId: HARNESS_ACCOUNT_ID,
|
|
114
|
+
productSlug: integration.slug,
|
|
115
|
+
inline: true,
|
|
116
|
+
onSuccess: (result) => setFlow({
|
|
117
|
+
open: false,
|
|
118
|
+
connected: true,
|
|
119
|
+
result: result ?? null,
|
|
120
|
+
revealed: false
|
|
121
|
+
}),
|
|
122
|
+
onCancel: () => setFlow((prev) => ({ ...prev, open: false }))
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
] })
|
|
127
|
+
] })
|
|
128
|
+
}
|
|
129
|
+
) : /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "This integration registers no activation form." }),
|
|
130
|
+
capabilities.length === 0 && permissions.length === 0 ? /* @__PURE__ */ jsxs(Alert, { children: [
|
|
131
|
+
/* @__PURE__ */ jsx(AlertTitle, { children: "No capabilities or permissions declared" }),
|
|
132
|
+
/* @__PURE__ */ jsx(AlertDescription, { children: /* @__PURE__ */ jsxs("p", { children: [
|
|
133
|
+
"The connect dialog renders \u201CWhat it does\u201D from",
|
|
134
|
+
" ",
|
|
135
|
+
/* @__PURE__ */ jsx("code", { children: "capabilities" }),
|
|
136
|
+
" and \u201CWhat it can access\u201D from ",
|
|
137
|
+
/* @__PURE__ */ jsx("code", { children: "permissions" }),
|
|
138
|
+
". This integration declares neither, so a user consents to it with no idea what it reaches \u2014 add both to ",
|
|
139
|
+
/* @__PURE__ */ jsx("code", { children: "defineIntegration()" }),
|
|
140
|
+
"."
|
|
141
|
+
] }) })
|
|
142
|
+
] }) : null,
|
|
143
|
+
flow.result ? /* @__PURE__ */ jsxs("div", { className: "bg-card flex flex-col gap-3 rounded-lg border p-6", children: [
|
|
144
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
|
|
145
|
+
/* @__PURE__ */ jsx("h2", { className: "font-heading text-lg tracking-tight", children: "Submitted payload" }),
|
|
146
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
147
|
+
hasSensitiveValues(flow.result) ? /* @__PURE__ */ jsx(
|
|
148
|
+
Button,
|
|
149
|
+
{
|
|
150
|
+
variant: "ghost",
|
|
151
|
+
size: "sm",
|
|
152
|
+
onClick: () => setFlow((prev) => ({ ...prev, revealed: !prev.revealed })),
|
|
153
|
+
"data-test": "activation-reveal-payload",
|
|
154
|
+
children: flow.revealed ? "Hide values" : "Reveal values"
|
|
155
|
+
}
|
|
156
|
+
) : null,
|
|
157
|
+
/* @__PURE__ */ jsx(
|
|
158
|
+
Button,
|
|
159
|
+
{
|
|
160
|
+
variant: "ghost",
|
|
161
|
+
size: "sm",
|
|
162
|
+
onClick: () => setFlow((prev) => ({
|
|
163
|
+
...prev,
|
|
164
|
+
result: null,
|
|
165
|
+
revealed: false
|
|
166
|
+
})),
|
|
167
|
+
children: "Clear"
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
] })
|
|
171
|
+
] }),
|
|
172
|
+
/* @__PURE__ */ jsx("pre", { className: "bg-muted overflow-x-auto rounded-md p-3 text-xs", children: JSON.stringify(
|
|
173
|
+
flow.revealed ? flow.result : redactSensitive(flow.result),
|
|
174
|
+
null,
|
|
175
|
+
2
|
|
176
|
+
) })
|
|
177
|
+
] }) : null
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
function CapabilityList({
|
|
183
|
+
capabilities
|
|
184
|
+
}) {
|
|
185
|
+
if (capabilities.length === 0) return null;
|
|
186
|
+
return /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-2", children: [
|
|
187
|
+
/* @__PURE__ */ jsx("h3", { className: "text-muted-foreground text-xs font-semibold tracking-wider uppercase", children: "What it does" }),
|
|
188
|
+
/* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-1.5", children: capabilities.map((capability) => /* @__PURE__ */ jsxs("li", { className: "text-foreground text-sm", children: [
|
|
189
|
+
capability.label,
|
|
190
|
+
capability.description ? /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground", children: [
|
|
191
|
+
" ",
|
|
192
|
+
"\u2014 ",
|
|
193
|
+
capability.description
|
|
194
|
+
] }) : null
|
|
195
|
+
] }, capability.label)) })
|
|
196
|
+
] });
|
|
197
|
+
}
|
|
198
|
+
function PermissionList({
|
|
199
|
+
permissions
|
|
200
|
+
}) {
|
|
201
|
+
if (permissions.length === 0) return null;
|
|
202
|
+
return /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-2", children: [
|
|
203
|
+
/* @__PURE__ */ jsx("h3", { className: "text-muted-foreground text-xs font-semibold tracking-wider uppercase", children: "What it can access" }),
|
|
204
|
+
/* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-2", children: permissions.map((permission) => /* @__PURE__ */ jsxs("li", { className: "flex flex-col gap-0.5", children: [
|
|
205
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
206
|
+
/* @__PURE__ */ jsx("span", { className: "text-foreground text-sm font-medium", children: permission.label }),
|
|
207
|
+
/* @__PURE__ */ jsx(
|
|
208
|
+
Badge,
|
|
209
|
+
{
|
|
210
|
+
variant: permission.type === "write" ? "warning" : "secondary",
|
|
211
|
+
children: permission.type === "write" ? "Write" : "Read"
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
] }),
|
|
215
|
+
permission.detail ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm", children: permission.detail }) : null
|
|
216
|
+
] }, permission.label)) })
|
|
217
|
+
] });
|
|
218
|
+
}
|
|
219
|
+
function ConnectedState({
|
|
220
|
+
name,
|
|
221
|
+
onDisconnect
|
|
222
|
+
}) {
|
|
223
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-5", children: [
|
|
224
|
+
/* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-sm", children: [
|
|
225
|
+
name,
|
|
226
|
+
" is connected to your workspace."
|
|
227
|
+
] }),
|
|
228
|
+
/* @__PURE__ */ jsx(
|
|
229
|
+
Button,
|
|
230
|
+
{
|
|
231
|
+
variant: "outline",
|
|
232
|
+
className: "w-fit",
|
|
233
|
+
onClick: onDisconnect,
|
|
234
|
+
"data-test": "activation-disconnect",
|
|
235
|
+
children: "Disconnect"
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
] });
|
|
239
|
+
}
|
|
240
|
+
export {
|
|
241
|
+
ActivationPage
|
|
242
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HarnessIntegration } from '../../registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* The landing page: every registered integration, with a link to each of its
|
|
4
|
+
* surfaces.
|
|
5
|
+
*
|
|
6
|
+
* A server component, and worth keeping one — it reads nothing but the array
|
|
7
|
+
* it is handed. The registry travels as a prop rather than through
|
|
8
|
+
* `useHarnessRegistry()` precisely so this page never needs to be a client
|
|
9
|
+
* module; the provider below it is for the routes that do.
|
|
10
|
+
*/
|
|
11
|
+
export declare function IndexPage({ integrations, }: {
|
|
12
|
+
integrations: HarnessIntegration[];
|
|
13
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { Icon } from "@ekanos/ui/icon";
|
|
4
|
+
import { HARNESS_SURFACES } from "../surfaces.js";
|
|
5
|
+
function IndexPage({
|
|
6
|
+
integrations
|
|
7
|
+
}) {
|
|
8
|
+
return /* @__PURE__ */ jsxs("main", { className: "mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-10", children: [
|
|
9
|
+
/* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-2", children: [
|
|
10
|
+
/* @__PURE__ */ jsx("h1", { className: "font-heading text-3xl tracking-tight", children: "Integration harness" }),
|
|
11
|
+
/* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "Every registered integration, rendered in real Fusion chrome from fixtures. No Supabase, no auth, no network." })
|
|
12
|
+
] }),
|
|
13
|
+
integrations.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "bg-card flex flex-col items-center gap-3 rounded-lg border p-6 text-center", children: [
|
|
14
|
+
/* @__PURE__ */ jsx(
|
|
15
|
+
Icon,
|
|
16
|
+
{
|
|
17
|
+
name: "fa-solid fa-box-open",
|
|
18
|
+
className: "text-muted-foreground h-6 w-6"
|
|
19
|
+
}
|
|
20
|
+
),
|
|
21
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm", children: "No integrations registered. Add one to your harness config at the project root." })
|
|
22
|
+
] }) : /* @__PURE__ */ jsx("ul", { className: "flex flex-col gap-5", "data-test": "integration-list", children: integrations.map((integration) => /* @__PURE__ */ jsxs(
|
|
23
|
+
"li",
|
|
24
|
+
{
|
|
25
|
+
className: "bg-card shadow-card flex flex-col gap-5 rounded-lg p-6",
|
|
26
|
+
children: [
|
|
27
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
|
|
28
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-3", children: [
|
|
29
|
+
/* @__PURE__ */ jsx("h2", { className: "font-heading text-xl tracking-tight", children: integration.name }),
|
|
30
|
+
/* @__PURE__ */ jsx("code", { className: "text-muted-foreground text-xs", children: integration.slug })
|
|
31
|
+
] }),
|
|
32
|
+
/* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: integration.description })
|
|
33
|
+
] }),
|
|
34
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: HARNESS_SURFACES.map((surface) => /* @__PURE__ */ jsx(
|
|
35
|
+
Link,
|
|
36
|
+
{
|
|
37
|
+
href: `/${integration.slug}/${surface.segment}`,
|
|
38
|
+
className: "bg-accent/50 hover:bg-accent rounded-md px-3 py-2 text-sm transition-colors",
|
|
39
|
+
children: surface.shortLabel
|
|
40
|
+
},
|
|
41
|
+
surface.segment
|
|
42
|
+
)) }),
|
|
43
|
+
/* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-xs", children: [
|
|
44
|
+
integration.widgets.length,
|
|
45
|
+
" widget",
|
|
46
|
+
integration.widgets.length === 1 ? "" : "s",
|
|
47
|
+
" \xB7",
|
|
48
|
+
" ",
|
|
49
|
+
integration.tile ? "tile" : "no tile",
|
|
50
|
+
" \xB7",
|
|
51
|
+
" ",
|
|
52
|
+
integration.activationForm ? "activation form" : "no activation form"
|
|
53
|
+
] })
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
integration.slug
|
|
57
|
+
)) })
|
|
58
|
+
] });
|
|
59
|
+
}
|
|
60
|
+
export {
|
|
61
|
+
IndexPage
|
|
62
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Client, not server: the registry holds React component references, which
|
|
4
|
+
* cannot cross the server/client boundary as props. Every surface below reads
|
|
5
|
+
* the injected registry out of context instead.
|
|
6
|
+
*/
|
|
7
|
+
export declare function IntegrationLayout({ children }: {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { notFound } from "next/navigation";
|
|
5
|
+
import { Alert, AlertDescription, AlertTitle } from "@ekanos/ui/alert";
|
|
6
|
+
import {
|
|
7
|
+
findEgressMismatch,
|
|
8
|
+
resolveEgress,
|
|
9
|
+
seedsForMode,
|
|
10
|
+
supportsLiveMode
|
|
11
|
+
} from "../../registry.js";
|
|
12
|
+
import { HarnessProviders } from "../components/harness-providers.js";
|
|
13
|
+
import { SurfaceNav } from "../components/surface-nav.js";
|
|
14
|
+
import { compileFixtures, needsSelfOrigin } from "../lib/http-fixtures.js";
|
|
15
|
+
import { useToolbar } from "../lib/toolbar-context.js";
|
|
16
|
+
import { useHarnessIntegration } from "../registry-context.js";
|
|
17
|
+
function IntegrationLayout({ children }) {
|
|
18
|
+
const { state } = useToolbar();
|
|
19
|
+
const integration = useHarnessIntegration();
|
|
20
|
+
const egressMismatch = useMemo(
|
|
21
|
+
() => integration ? findEgressMismatch(integration) : null,
|
|
22
|
+
[integration]
|
|
23
|
+
);
|
|
24
|
+
const mode = supportsLiveMode(integration) && !egressMismatch ? state.dataMode : "fixtures";
|
|
25
|
+
const seeds = useMemo(
|
|
26
|
+
() => integration ? seedsForMode(integration, state.variant, mode) : [],
|
|
27
|
+
[integration, state.variant, mode]
|
|
28
|
+
);
|
|
29
|
+
const egress = useMemo(
|
|
30
|
+
() => integration ? resolveEgress(integration) : [],
|
|
31
|
+
[integration]
|
|
32
|
+
);
|
|
33
|
+
const httpFixtures = useMemo(() => {
|
|
34
|
+
if (!integration) return [];
|
|
35
|
+
const declared = integration.fixtures?.[state.variant] ?? [];
|
|
36
|
+
const selfOrigin = globalThis.location?.origin;
|
|
37
|
+
if (selfOrigin !== void 0) {
|
|
38
|
+
return compileFixtures(declared, egress, selfOrigin);
|
|
39
|
+
}
|
|
40
|
+
return compileFixtures(
|
|
41
|
+
declared.filter((fixture) => !needsSelfOrigin(fixture)),
|
|
42
|
+
egress
|
|
43
|
+
);
|
|
44
|
+
}, [integration, state.variant, egress]);
|
|
45
|
+
if (!integration) notFound();
|
|
46
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
|
|
47
|
+
/* @__PURE__ */ jsx(SurfaceNav, { integration }),
|
|
48
|
+
egressMismatch ? /* @__PURE__ */ jsx(EgressMismatchNotice, { mismatch: egressMismatch }) : null,
|
|
49
|
+
/* @__PURE__ */ jsx(
|
|
50
|
+
HarnessProviders,
|
|
51
|
+
{
|
|
52
|
+
seeds,
|
|
53
|
+
mode,
|
|
54
|
+
...integration.live && !egressMismatch ? { live: integration.live } : {},
|
|
55
|
+
egress,
|
|
56
|
+
httpFixtures,
|
|
57
|
+
slug: integration.slug,
|
|
58
|
+
children
|
|
59
|
+
},
|
|
60
|
+
`${mode}-${state.variant}`
|
|
61
|
+
)
|
|
62
|
+
] });
|
|
63
|
+
}
|
|
64
|
+
function EgressMismatchNotice({ mismatch }) {
|
|
65
|
+
return /* @__PURE__ */ jsx("div", { className: "px-6 pt-6", "data-test": "egress-mismatch", children: /* @__PURE__ */ jsxs(Alert, { variant: "destructive", children: [
|
|
66
|
+
/* @__PURE__ */ jsx(AlertTitle, { children: "Live mode is disabled: `live.egress` declares origins your integration does not" }),
|
|
67
|
+
/* @__PURE__ */ jsxs(AlertDescription, { children: [
|
|
68
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
69
|
+
"These origins are in your harness entry's",
|
|
70
|
+
" ",
|
|
71
|
+
/* @__PURE__ */ jsx("code", { children: "live.egress" }),
|
|
72
|
+
" but not in",
|
|
73
|
+
" ",
|
|
74
|
+
/* @__PURE__ */ jsx("code", { children: "defineIntegration({ egress })" }),
|
|
75
|
+
":"
|
|
76
|
+
] }),
|
|
77
|
+
/* @__PURE__ */ jsx("ul", { className: "mt-2 flex flex-col gap-1", children: mismatch.undeclared.map((origin) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("code", { className: "text-sm", children: origin }) }, origin)) }),
|
|
78
|
+
/* @__PURE__ */ jsx("p", { className: "mt-3", children: "The definition is what a reviewer reads and what production enforces, so running live against a wider list would prove nothing about the integration we actually ship. Import the same constant into both \u2014 never retype it \u2014 and this goes away." })
|
|
79
|
+
] })
|
|
80
|
+
] }) });
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
IntegrationLayout
|
|
84
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import '@fortawesome/fontawesome-free/css/all.min.css';
|
|
2
|
+
import type { HarnessIntegration } from '../../registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* The `<html>`/`<body>` shell and every provider that has to sit above all
|
|
5
|
+
* routes.
|
|
6
|
+
*
|
|
7
|
+
* THE CLIENT BOUNDARY OF THE WHOLE HARNESS, and it has to be — the registry is
|
|
8
|
+
* the reason. A `HarnessIntegration` holds live React component references
|
|
9
|
+
* (`widgets[].component`, `tile`, `activationForm`) and, once a definition is
|
|
10
|
+
* linked, MCP tool `run` functions. Functions cannot be serialized across the
|
|
11
|
+
* server/client boundary, so a SERVER `RootLayout` handing this array to the
|
|
12
|
+
* client `HarnessRegistryProvider` fails the build outright:
|
|
13
|
+
*
|
|
14
|
+
* Functions cannot be passed directly to Client Components …
|
|
15
|
+
* {name: …, description: …, parameters: …, run: function run, …}
|
|
16
|
+
*
|
|
17
|
+
* The old app dodged this by having each client route import the registry
|
|
18
|
+
* module directly — the exact coupling this package exists to break. Injecting
|
|
19
|
+
* it instead means the boundary must sit ABOVE the injection point, which is
|
|
20
|
+
* here.
|
|
21
|
+
*
|
|
22
|
+
* Almost nothing is lost: every route, the toolbar and all four providers were
|
|
23
|
+
* already `'use client'`. `metadata` is unaffected — it is exported from the
|
|
24
|
+
* shell's own `app/layout.tsx`, which stays a server module, and `IndexPage`
|
|
25
|
+
* stays a server component because the shell's `app/page.tsx` renders it and
|
|
26
|
+
* passes the result down as `children`.
|
|
27
|
+
*
|
|
28
|
+
* `integrations` is the partner's registry, handed down from the shell's
|
|
29
|
+
* `app/layout.tsx`. Nothing in this package imports it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function RootLayout({ integrations, children, }: {
|
|
32
|
+
integrations: HarnessIntegration[];
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { IconStyleOverrideProvider } from "@ekanos/ui/icon";
|
|
4
|
+
import { cn } from "@ekanos/ui/utils";
|
|
5
|
+
import "@fortawesome/fontawesome-free/css/all.min.css";
|
|
6
|
+
import { ThemeProvider } from "next-themes";
|
|
7
|
+
import { DevToolbar } from "../components/dev-toolbar.js";
|
|
8
|
+
import { I18nProvider } from "../components/i18n-provider.js";
|
|
9
|
+
import { fontVariables } from "../lib/fonts.js";
|
|
10
|
+
import { ToolbarProvider } from "../lib/toolbar-context.js";
|
|
11
|
+
import { HarnessRegistryProvider } from "../registry-context.js";
|
|
12
|
+
function RootLayout({
|
|
13
|
+
integrations,
|
|
14
|
+
children
|
|
15
|
+
}) {
|
|
16
|
+
return /* @__PURE__ */ jsx("html", { lang: "en", className: fontVariables, suppressHydrationWarning: true, children: /* @__PURE__ */ jsx(
|
|
17
|
+
"body",
|
|
18
|
+
{
|
|
19
|
+
className: cn("bg-background text-foreground min-h-screen antialiased"),
|
|
20
|
+
children: /* @__PURE__ */ jsx(IconStyleOverrideProvider, { value: "solid", children: /* @__PURE__ */ jsx(HarnessRegistryProvider, { integrations, children: /* @__PURE__ */ jsx(I18nProvider, { children: /* @__PURE__ */ jsx(
|
|
21
|
+
ThemeProvider,
|
|
22
|
+
{
|
|
23
|
+
attribute: "class",
|
|
24
|
+
defaultTheme: "light",
|
|
25
|
+
enableSystem: false,
|
|
26
|
+
enableColorScheme: false,
|
|
27
|
+
disableTransitionOnChange: true,
|
|
28
|
+
children: /* @__PURE__ */ jsxs(ToolbarProvider, { children: [
|
|
29
|
+
/* @__PURE__ */ jsx(DevToolbar, {}),
|
|
30
|
+
children
|
|
31
|
+
] })
|
|
32
|
+
}
|
|
33
|
+
) }) }) })
|
|
34
|
+
}
|
|
35
|
+
) });
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
RootLayout
|
|
39
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { notFound } from "next/navigation";
|
|
4
|
+
import { findWidget } from "../../registry.js";
|
|
5
|
+
import { WidgetSurface } from "../components/widget-surface.js";
|
|
6
|
+
import { useHarnessIntegration, useHarnessWidgetId } from "../registry-context.js";
|
|
7
|
+
function SingleWidgetPage() {
|
|
8
|
+
const integration = useHarnessIntegration();
|
|
9
|
+
const widgetId = useHarnessWidgetId();
|
|
10
|
+
if (!integration) notFound();
|
|
11
|
+
const widget = findWidget(integration, widgetId);
|
|
12
|
+
if (!widget) notFound();
|
|
13
|
+
return /* @__PURE__ */ jsx(
|
|
14
|
+
"main",
|
|
15
|
+
{
|
|
16
|
+
className: "mx-auto w-full max-w-4xl px-6 py-8",
|
|
17
|
+
"data-test": "single-widget",
|
|
18
|
+
children: /* @__PURE__ */ jsx(
|
|
19
|
+
WidgetSurface,
|
|
20
|
+
{
|
|
21
|
+
widgets: [{ ...widget, width: "full" }],
|
|
22
|
+
productSlug: integration.slug
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
SingleWidgetPage
|
|
30
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function TilePage(): import("react").JSX.Element;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { notFound } from "next/navigation";
|
|
4
|
+
import { Alert, AlertDescription, AlertTitle } from "@ekanos/ui/alert";
|
|
5
|
+
import { HARNESS_ACCOUNT_ID } from "../../registry.js";
|
|
6
|
+
import { ViewportFrame } from "../components/viewport-frame.js";
|
|
7
|
+
import { useHarnessIntegration } from "../registry-context.js";
|
|
8
|
+
const NEIGHBOURS = [
|
|
9
|
+
{
|
|
10
|
+
name: "Google Workspace",
|
|
11
|
+
description: "Mail, calendar and drive on the dashboard, and available to the assistant.",
|
|
12
|
+
vendor: "Google"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "HubSpot",
|
|
16
|
+
description: "Deals, contacts and pipeline stages, kept in step with your CRM.",
|
|
17
|
+
vendor: "HubSpot"
|
|
18
|
+
}
|
|
19
|
+
];
|
|
20
|
+
function TilePage() {
|
|
21
|
+
const integration = useHarnessIntegration();
|
|
22
|
+
if (!integration) notFound();
|
|
23
|
+
const Tile = integration.tile;
|
|
24
|
+
return /* @__PURE__ */ jsxs(
|
|
25
|
+
"main",
|
|
26
|
+
{
|
|
27
|
+
className: "mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-8",
|
|
28
|
+
"data-test": "tile",
|
|
29
|
+
children: [
|
|
30
|
+
/* @__PURE__ */ jsxs(Alert, { children: [
|
|
31
|
+
/* @__PURE__ */ jsx(AlertTitle, { children: "The marketplace does not currently render this" }),
|
|
32
|
+
/* @__PURE__ */ jsx(AlertDescription, { children: /* @__PURE__ */ jsxs("p", { children: [
|
|
33
|
+
"22 of 23 shipped integrations return ",
|
|
34
|
+
/* @__PURE__ */ jsx("code", { children: "null" }),
|
|
35
|
+
" from",
|
|
36
|
+
" ",
|
|
37
|
+
/* @__PURE__ */ jsx("code", { children: "getMarketplaceTile()" }),
|
|
38
|
+
", and the live marketplace route renders a generic product card that ignores the tile entirely. Treat this surface as a preview of a component that is, today, effectively unused \u2014 worth keeping correct, not worth investing heavily in."
|
|
39
|
+
] }) })
|
|
40
|
+
] }),
|
|
41
|
+
Tile ? /* @__PURE__ */ jsx(ViewportFrame, { children: /* @__PURE__ */ jsxs("section", { className: "flex flex-col gap-4", children: [
|
|
42
|
+
/* @__PURE__ */ jsxs("header", { children: [
|
|
43
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-baseline gap-2.5", children: [
|
|
44
|
+
/* @__PURE__ */ jsx("h2", { className: "font-heading text-foreground text-xl font-semibold tracking-tight", children: "Available integrations" }),
|
|
45
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-sm tabular-nums", children: "3" })
|
|
46
|
+
] }),
|
|
47
|
+
/* @__PURE__ */ jsx("p", { className: "text-muted-foreground mt-1 max-w-prose text-sm", children: "Your tile in the catalogue grid, with two inert neighbours for scale. Column width, gap and heading come from the real marketplace." })
|
|
48
|
+
] }),
|
|
49
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[repeat(auto-fill,minmax(17rem,1fr))] gap-4", children: [
|
|
50
|
+
/* @__PURE__ */ jsx(
|
|
51
|
+
Tile,
|
|
52
|
+
{
|
|
53
|
+
accountId: HARNESS_ACCOUNT_ID,
|
|
54
|
+
productSlug: integration.slug,
|
|
55
|
+
href: `/${integration.slug}/activation`
|
|
56
|
+
}
|
|
57
|
+
),
|
|
58
|
+
NEIGHBOURS.map((neighbour) => /* @__PURE__ */ jsx(NeighbourCard, { ...neighbour }, neighbour.name))
|
|
59
|
+
] })
|
|
60
|
+
] }) }) : /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-sm", children: "This integration registers no marketplace tile." })
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
function NeighbourCard({
|
|
66
|
+
name,
|
|
67
|
+
description,
|
|
68
|
+
vendor
|
|
69
|
+
}) {
|
|
70
|
+
return /* @__PURE__ */ jsxs(
|
|
71
|
+
"div",
|
|
72
|
+
{
|
|
73
|
+
"aria-hidden": "true",
|
|
74
|
+
className: "bg-card text-card-foreground shadow-card flex h-full flex-col gap-0 rounded-xl border p-5 opacity-60",
|
|
75
|
+
children: [
|
|
76
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
|
|
77
|
+
/* @__PURE__ */ jsx("span", { className: "font-heading bg-muted text-muted-foreground flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-base font-semibold", children: name.charAt(0) }),
|
|
78
|
+
/* @__PURE__ */ jsx("span", { className: "font-heading truncate text-base font-semibold tracking-tight", children: name })
|
|
79
|
+
] }),
|
|
80
|
+
/* @__PURE__ */ jsx("p", { className: "text-muted-foreground mt-3 line-clamp-2 text-sm", children: description }),
|
|
81
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground mt-auto pt-4 text-sm", children: vendor })
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
export {
|
|
87
|
+
TilePage
|
|
88
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function TriggersPage(): import("react").JSX.Element;
|