@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
package/dist/app.d.ts
ADDED
package/dist/app.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { RootLayout } from "./internal/routes/root-layout.js";
|
|
2
|
+
import { IndexPage } from "./internal/routes/index-page.js";
|
|
3
|
+
const harnessMetadata = {
|
|
4
|
+
title: "Fusion Integration Harness",
|
|
5
|
+
description: "Render integration widgets, marketplace tiles and activation forms in real Fusion chrome, from fixtures, with no Supabase, auth or network."
|
|
6
|
+
};
|
|
7
|
+
export {
|
|
8
|
+
IndexPage,
|
|
9
|
+
RootLayout,
|
|
10
|
+
harnessMetadata
|
|
11
|
+
};
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HarnessIntegration } from './registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* Identity function over the harness registry.
|
|
4
|
+
*
|
|
5
|
+
* It exists for the type inference, not for the runtime: wrapping the array in
|
|
6
|
+
* the partner's config module gets them completions and errors on every field
|
|
7
|
+
* of every entry — widget ids, fixture query keys, the `live` block — at the
|
|
8
|
+
* point they are typing them, rather than a wall of red on the array's closing
|
|
9
|
+
* bracket, which is what a bare `: HarnessIntegration[]` annotation produces.
|
|
10
|
+
*
|
|
11
|
+
* It also performs the few checks that TypeScript cannot: that slugs and widget
|
|
12
|
+
* ids are usable as URL segments, and that neither collides. Those are the
|
|
13
|
+
* mistakes that produce a blank page or a wrong page rather than an error, and
|
|
14
|
+
* this is the one place every registry entry passes through. Nothing else is
|
|
15
|
+
* validated or normalised — a partner can still fall back to a plain annotated
|
|
16
|
+
* array, and gets identical behaviour minus these checks.
|
|
17
|
+
*/
|
|
18
|
+
export declare function defineHarnessConfig(integrations: HarnessIntegration[]): HarnessIntegration[];
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const SAFE_SEGMENT = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
2
|
+
function assertSafeSegment(kind, value, where) {
|
|
3
|
+
if (SAFE_SEGMENT.test(value)) return;
|
|
4
|
+
throw new Error(
|
|
5
|
+
`defineHarnessConfig(): ${kind} ${JSON.stringify(value)} (${where}) is not usable as a URL segment. It must start with a letter or digit and contain only letters, digits, dot, underscore or hyphen \u2014 the harness routes every surface at /<slug>/\u2026, so a slash, space or "#" here silently produces a page that 404s.`
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
function defineHarnessConfig(integrations) {
|
|
9
|
+
const seenSlugs = /* @__PURE__ */ new Map();
|
|
10
|
+
integrations.forEach((integration, index) => {
|
|
11
|
+
assertSafeSegment("slug", integration.slug, `entry ${index}`);
|
|
12
|
+
const previous = seenSlugs.get(integration.slug);
|
|
13
|
+
if (previous !== void 0) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`defineHarnessConfig(): duplicate slug ${JSON.stringify(integration.slug)} at entries ${previous} and ${index}. Every surface is routed by slug, so the later entry would be permanently unreachable and the two would collide as React keys on the index page.`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
seenSlugs.set(integration.slug, index);
|
|
19
|
+
const seenWidgets = /* @__PURE__ */ new Set();
|
|
20
|
+
for (const widget of integration.widgets) {
|
|
21
|
+
assertSafeSegment("widget id", widget.id, `slug "${integration.slug}"`);
|
|
22
|
+
if (seenWidgets.has(widget.id)) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`defineHarnessConfig(): duplicate widget id ${JSON.stringify(widget.id)} in "${integration.slug}". Widget ids address the single-widget surface, so the second one could never be opened.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
seenWidgets.add(widget.id);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return integrations;
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
defineHarnessConfig
|
|
34
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stands in for the AI assistant dock.
|
|
3
|
+
*
|
|
4
|
+
* In the real app, clicking a widget's "Ask about this" footer sends the
|
|
5
|
+
* widget's snapshot to the assistant. There is no assistant here — so instead
|
|
6
|
+
* the harness shows you the exact payload that would have been sent. That makes
|
|
7
|
+
* this panel the fastest way to check the snapshot rules: is the
|
|
8
|
+
* data faithful to what's on screen, are the filters included, did it recompute.
|
|
9
|
+
*/
|
|
10
|
+
export declare function AskAssistantBridge(): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
4
|
+
import { useRegisterAskAssistant } from "@ekanos/sdk/hooks";
|
|
5
|
+
import { Button } from "@ekanos/ui/button";
|
|
6
|
+
import { Icon } from "@ekanos/ui/icon";
|
|
7
|
+
function AskAssistantBridge() {
|
|
8
|
+
const [asked, setAsked] = useState(null);
|
|
9
|
+
const handler = useCallback((ctx) => {
|
|
10
|
+
setAsked(ctx);
|
|
11
|
+
}, []);
|
|
12
|
+
useRegisterAskAssistant(handler);
|
|
13
|
+
if (!asked) return null;
|
|
14
|
+
return /* @__PURE__ */ jsxs("div", { className: "fixed right-6 bottom-24 z-50 flex max-h-[60vh] w-[min(28rem,calc(100vw-3rem))] flex-col overflow-hidden rounded-lg border", children: [
|
|
15
|
+
/* @__PURE__ */ jsxs("div", { className: "bg-card flex items-center gap-3 border-b px-5 py-3", children: [
|
|
16
|
+
/* @__PURE__ */ jsx(
|
|
17
|
+
Icon,
|
|
18
|
+
{
|
|
19
|
+
name: "fa-solid fa-wand-magic-sparkles",
|
|
20
|
+
className: "text-primary h-5 w-5"
|
|
21
|
+
}
|
|
22
|
+
),
|
|
23
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
24
|
+
/* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium", children: "Ask payload" }),
|
|
25
|
+
/* @__PURE__ */ jsxs("p", { className: "text-muted-foreground truncate text-xs", children: [
|
|
26
|
+
asked.title,
|
|
27
|
+
" \xB7 ",
|
|
28
|
+
asked.widgetId
|
|
29
|
+
] })
|
|
30
|
+
] }),
|
|
31
|
+
/* @__PURE__ */ jsx(
|
|
32
|
+
Button,
|
|
33
|
+
{
|
|
34
|
+
variant: "ghost",
|
|
35
|
+
size: "icon",
|
|
36
|
+
onClick: () => setAsked(null),
|
|
37
|
+
"aria-label": "Close ask payload",
|
|
38
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "fa-solid fa-xmark", className: "h-4 w-4" })
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
] }),
|
|
42
|
+
/* @__PURE__ */ jsxs("div", { className: "bg-card min-h-0 flex-1 overflow-auto p-5", children: [
|
|
43
|
+
/* @__PURE__ */ jsx("p", { className: "text-muted-foreground mb-3 text-sm", children: asked.suggestedPrompt }),
|
|
44
|
+
/* @__PURE__ */ jsx("pre", { className: "bg-muted overflow-x-auto rounded-md p-3 text-xs", children: JSON.stringify(asked.snapshot, null, 2) ?? "null" })
|
|
45
|
+
] })
|
|
46
|
+
] });
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
AskAssistantBridge
|
|
50
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { WidgetConfig } from '@ekanos/sdk';
|
|
2
|
+
import { type WidgetRenderState } from '@ekanos/sdk/components';
|
|
3
|
+
/**
|
|
4
|
+
* A copy of the host's workspace dashboard grid.
|
|
5
|
+
*
|
|
6
|
+
* WHAT CHANGED, and why it had to be a copy rather than an import:
|
|
7
|
+
*
|
|
8
|
+
* - REMOVED the three server-action imports (`pinWidget`, `unpinWidget`,
|
|
9
|
+
* `setWidgetCollapsed`). They are `'use server'` + `server-only` + Supabase,
|
|
10
|
+
* which is the entire reason the real file cannot be imported here.
|
|
11
|
+
*
|
|
12
|
+
* - SWAPPED the host's `DashboardWidgetProvider` for `HarnessWidgetProvider`,
|
|
13
|
+
* which fills the SAME published `WidgetContext` from local state. The host
|
|
14
|
+
* provider is host chrome and is not on the `@ekanos/sdk` surface, because
|
|
15
|
+
* what makes it host chrome is the widget-config persistence this app has
|
|
16
|
+
* no database for. See the long note in `harness-widget-provider.tsx`; the
|
|
17
|
+
* resolved render state is identical.
|
|
18
|
+
*
|
|
19
|
+
* - ADDED `stateOverride`, so the dev toolbar can drive the REAL provider
|
|
20
|
+
* state machine. This is not cosmetic: `WidgetConfig.widgetState` is typed
|
|
21
|
+
* `'active' | 'inactive' | 'disabled'` and cannot express `loading` or
|
|
22
|
+
* `error`, even though the provider accepts all five. Without
|
|
23
|
+
* this the toolbar could not reach two of the five states at all.
|
|
24
|
+
*
|
|
25
|
+
* - ADDED a `WidgetBoundary` around each widget so one partner's crash
|
|
26
|
+
* renders a readable card instead of blanking the page.
|
|
27
|
+
*
|
|
28
|
+
* Everything else — grid classes, row grouping, the props handed to the
|
|
29
|
+
* provider, the two props handed to the widget — is verbatim. If the real grid
|
|
30
|
+
* changes, this needs the same change.
|
|
31
|
+
*/
|
|
32
|
+
type DashboardGridLayout = 'masonry' | 'two-column';
|
|
33
|
+
interface DashboardGridProps {
|
|
34
|
+
widgets: WidgetConfig[];
|
|
35
|
+
accountId: string;
|
|
36
|
+
layout?: DashboardGridLayout;
|
|
37
|
+
/** Forced render state from the dev toolbar. */
|
|
38
|
+
stateOverride?: WidgetRenderState;
|
|
39
|
+
/** Forced collapsed state from the dev toolbar. */
|
|
40
|
+
collapsedOverride?: boolean;
|
|
41
|
+
/** Remount key — changing it resets crashed widget boundaries. */
|
|
42
|
+
resetKey?: string;
|
|
43
|
+
}
|
|
44
|
+
export declare function DashboardGrid({ widgets, accountId, layout, stateOverride, collapsedOverride, resetKey, }: DashboardGridProps): import("react").JSX.Element;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { HarnessWidgetProvider } from "./harness-widget-provider.js";
|
|
4
|
+
import { buildRowGroups, splitBalanced } from "./row-groups.js";
|
|
5
|
+
import { WidgetBoundary } from "./widget-boundary.js";
|
|
6
|
+
function DashboardGrid({
|
|
7
|
+
widgets,
|
|
8
|
+
accountId,
|
|
9
|
+
layout = "masonry",
|
|
10
|
+
stateOverride,
|
|
11
|
+
collapsedOverride,
|
|
12
|
+
resetKey
|
|
13
|
+
}) {
|
|
14
|
+
const shared = { accountId, stateOverride, collapsedOverride, resetKey };
|
|
15
|
+
if (layout === "two-column") {
|
|
16
|
+
return /* @__PURE__ */ jsx("div", { className: "@container/dashboard grid grid-cols-1 items-start gap-5 @4xl/dashboard:grid-cols-2", children: widgets.map((widget) => /* @__PURE__ */ jsx(WidgetRenderer, { widget, ...shared }, widget.id)) });
|
|
17
|
+
}
|
|
18
|
+
const rowGroups = buildRowGroups(widgets);
|
|
19
|
+
return /* @__PURE__ */ jsx("div", { className: "@container/dashboard space-y-5", children: rowGroups.map((group) => {
|
|
20
|
+
if (group.type === "full") {
|
|
21
|
+
return /* @__PURE__ */ jsx(
|
|
22
|
+
WidgetRenderer,
|
|
23
|
+
{
|
|
24
|
+
widget: group.widget,
|
|
25
|
+
...shared
|
|
26
|
+
},
|
|
27
|
+
group.widget.id
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const { left, right } = splitBalanced(group.widgets);
|
|
31
|
+
return /* @__PURE__ */ jsxs(
|
|
32
|
+
"div",
|
|
33
|
+
{
|
|
34
|
+
className: "grid grid-cols-1 gap-5 @4xl/dashboard:grid-cols-2 @4xl/dashboard:items-start",
|
|
35
|
+
children: [
|
|
36
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-5", children: left.map((widget) => /* @__PURE__ */ jsx(WidgetRenderer, { widget, ...shared }, widget.id)) }),
|
|
37
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-5", children: right.map((widget) => /* @__PURE__ */ jsx(WidgetRenderer, { widget, ...shared }, widget.id)) })
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
group.widgets[0].id
|
|
41
|
+
);
|
|
42
|
+
}) });
|
|
43
|
+
}
|
|
44
|
+
function WidgetRenderer({
|
|
45
|
+
widget,
|
|
46
|
+
accountId,
|
|
47
|
+
stateOverride,
|
|
48
|
+
collapsedOverride,
|
|
49
|
+
resetKey
|
|
50
|
+
}) {
|
|
51
|
+
const Component = widget.component;
|
|
52
|
+
const state = stateOverride ?? widget.widgetState;
|
|
53
|
+
return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
|
|
54
|
+
HarnessWidgetProvider,
|
|
55
|
+
{
|
|
56
|
+
widgetId: widget.id,
|
|
57
|
+
title: widget.name,
|
|
58
|
+
state,
|
|
59
|
+
isEnabled: state === "active",
|
|
60
|
+
isCollapsible: widget.isCollapsible ?? false,
|
|
61
|
+
isPinnable: widget.isPinnable ?? false,
|
|
62
|
+
defaultCollapsed: collapsedOverride ?? widget.collapsed ?? false,
|
|
63
|
+
defaultPinned: widget.isPinned ?? false,
|
|
64
|
+
maxContentHeight: getMaxContentHeight(widget),
|
|
65
|
+
aiFooterEnabled: widget.aiFooterEnabled,
|
|
66
|
+
health: widget.health,
|
|
67
|
+
children: /* @__PURE__ */ jsx(WidgetBoundary, { widgetId: widget.id, resetKey: `${state}-${resetKey}`, children: /* @__PURE__ */ jsx(
|
|
68
|
+
Component,
|
|
69
|
+
{
|
|
70
|
+
accountId,
|
|
71
|
+
productId: widget.integrationMetadata?.productId ?? "fusion"
|
|
72
|
+
}
|
|
73
|
+
) })
|
|
74
|
+
},
|
|
75
|
+
`${state}-${String(collapsedOverride)}-${resetKey ?? ""}`
|
|
76
|
+
) });
|
|
77
|
+
}
|
|
78
|
+
function getMaxContentHeight(widget) {
|
|
79
|
+
const maxHeight = widget.layouts?.lg?.maxHeight;
|
|
80
|
+
return maxHeight ? `${maxHeight}px` : void 0;
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
DashboardGrid
|
|
84
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The persistent dev toolbar. Every control here drives the real machinery
|
|
3
|
+
* rather than a harness-local imitation:
|
|
4
|
+
*
|
|
5
|
+
* - Render state feeds `DashboardWidgetProvider`'s `state` prop, so you are
|
|
6
|
+
* exercising the actual state machine, including the `isEnabled` and health
|
|
7
|
+
* downgrades it computes internally.
|
|
8
|
+
* - Fixture variant rebuilds the QueryClient with a different seed set.
|
|
9
|
+
* - Collapsed feeds `defaultCollapsed`.
|
|
10
|
+
* - Unhealthy feeds `health`, reproducing the one production condition that
|
|
11
|
+
* silently downgrades an active widget to `disabled`.
|
|
12
|
+
*
|
|
13
|
+
* All choices persist to localStorage, so a reload keeps your place.
|
|
14
|
+
*/
|
|
15
|
+
export declare function DevToolbar(): import("react").JSX.Element;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { usePathname } from "next/navigation";
|
|
4
|
+
import { Button } from "@ekanos/ui/button";
|
|
5
|
+
import { Icon } from "@ekanos/ui/icon";
|
|
6
|
+
import { Label } from "@ekanos/ui/label";
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue
|
|
13
|
+
} from "@ekanos/ui/select";
|
|
14
|
+
import { Switch } from "@ekanos/ui/switch";
|
|
15
|
+
import { cn } from "@ekanos/ui/utils";
|
|
16
|
+
import { useTheme } from "next-themes";
|
|
17
|
+
import {
|
|
18
|
+
DATA_MODES,
|
|
19
|
+
FIXTURE_VARIANTS,
|
|
20
|
+
findIntegration,
|
|
21
|
+
resolveEgress,
|
|
22
|
+
supportsLiveMode
|
|
23
|
+
} from "../../registry.js";
|
|
24
|
+
import {
|
|
25
|
+
RENDER_STATES,
|
|
26
|
+
VIEWPORTS,
|
|
27
|
+
useToolbar
|
|
28
|
+
} from "../lib/toolbar-context.js";
|
|
29
|
+
import { useHarnessRegistry } from "../registry-context.js";
|
|
30
|
+
function Field({
|
|
31
|
+
label,
|
|
32
|
+
children
|
|
33
|
+
}) {
|
|
34
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
|
|
35
|
+
/* @__PURE__ */ jsx(Label, { className: "text-muted-foreground text-xs", children: label }),
|
|
36
|
+
children
|
|
37
|
+
] });
|
|
38
|
+
}
|
|
39
|
+
const DATA_MODE_LABELS = {
|
|
40
|
+
fixtures: "Fixtures",
|
|
41
|
+
live: "Live API"
|
|
42
|
+
};
|
|
43
|
+
function useCurrentIntegration() {
|
|
44
|
+
const integrations = useHarnessRegistry();
|
|
45
|
+
const pathname = usePathname();
|
|
46
|
+
const slug = pathname.split("/").filter(Boolean)[0] ?? null;
|
|
47
|
+
return slug ? findIntegration(integrations, slug) : null;
|
|
48
|
+
}
|
|
49
|
+
function DevToolbar() {
|
|
50
|
+
const { state, setState, reset } = useToolbar();
|
|
51
|
+
const { resolvedTheme, setTheme } = useTheme();
|
|
52
|
+
const integration = useCurrentIntegration();
|
|
53
|
+
const liveAvailable = supportsLiveMode(integration);
|
|
54
|
+
const live = liveAvailable && state.dataMode === "live";
|
|
55
|
+
return /* @__PURE__ */ jsx("div", { className: "bg-card sticky top-0 z-40 border-b", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-end gap-5 px-6 py-3", children: [
|
|
56
|
+
/* @__PURE__ */ jsx(Field, { label: "Render state", children: /* @__PURE__ */ jsxs(
|
|
57
|
+
Select,
|
|
58
|
+
{
|
|
59
|
+
value: state.renderState,
|
|
60
|
+
onValueChange: (value) => setState({ renderState: value }),
|
|
61
|
+
children: [
|
|
62
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "w-36", "data-test": "toolbar-render-state", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
63
|
+
/* @__PURE__ */ jsx(SelectContent, { children: RENDER_STATES.map((value) => /* @__PURE__ */ jsx(SelectItem, { value, children: value }, value)) })
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
) }),
|
|
67
|
+
/* @__PURE__ */ jsx(Field, { label: "Data", children: /* @__PURE__ */ jsxs(
|
|
68
|
+
Select,
|
|
69
|
+
{
|
|
70
|
+
value: liveAvailable ? state.dataMode : "fixtures",
|
|
71
|
+
onValueChange: (value) => setState({ dataMode: value }),
|
|
72
|
+
disabled: !liveAvailable,
|
|
73
|
+
children: [
|
|
74
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "w-32", "data-test": "toolbar-data-mode", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
75
|
+
/* @__PURE__ */ jsx(SelectContent, { children: DATA_MODES.map((value) => /* @__PURE__ */ jsx(SelectItem, { value, children: DATA_MODE_LABELS[value] }, value)) })
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
) }),
|
|
79
|
+
/* @__PURE__ */ jsx(Field, { label: "Fixtures", children: /* @__PURE__ */ jsxs(
|
|
80
|
+
Select,
|
|
81
|
+
{
|
|
82
|
+
value: state.variant,
|
|
83
|
+
onValueChange: (value) => setState({ variant: value }),
|
|
84
|
+
disabled: live,
|
|
85
|
+
children: [
|
|
86
|
+
/* @__PURE__ */ jsx(SelectTrigger, { className: "w-32", "data-test": "toolbar-variant", children: /* @__PURE__ */ jsx(SelectValue, {}) }),
|
|
87
|
+
/* @__PURE__ */ jsx(SelectContent, { children: FIXTURE_VARIANTS.map((value) => /* @__PURE__ */ jsx(SelectItem, { value, children: value }, value)) })
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
) }),
|
|
91
|
+
/* @__PURE__ */ jsx(Field, { label: "Viewport", children: /* @__PURE__ */ jsx("div", { className: "flex gap-1", children: Object.keys(VIEWPORTS).map((key) => /* @__PURE__ */ jsx(
|
|
92
|
+
Button,
|
|
93
|
+
{
|
|
94
|
+
size: "sm",
|
|
95
|
+
variant: state.viewport === key ? "default" : "outline",
|
|
96
|
+
onClick: () => setState({ viewport: key }),
|
|
97
|
+
children: VIEWPORTS[key].label
|
|
98
|
+
},
|
|
99
|
+
key
|
|
100
|
+
)) }) }),
|
|
101
|
+
/* @__PURE__ */ jsx(Field, { label: "Collapsed", children: /* @__PURE__ */ jsx(
|
|
102
|
+
Switch,
|
|
103
|
+
{
|
|
104
|
+
checked: state.collapsed,
|
|
105
|
+
onCheckedChange: (checked) => setState({ collapsed: checked }),
|
|
106
|
+
"aria-label": "Toggle collapsed"
|
|
107
|
+
}
|
|
108
|
+
) }),
|
|
109
|
+
/* @__PURE__ */ jsx(Field, { label: "Unhealthy", children: /* @__PURE__ */ jsx(
|
|
110
|
+
Switch,
|
|
111
|
+
{
|
|
112
|
+
checked: state.unhealthy,
|
|
113
|
+
onCheckedChange: (checked) => setState({ unhealthy: checked }),
|
|
114
|
+
"aria-label": "Simulate unhealthy integration"
|
|
115
|
+
}
|
|
116
|
+
) }),
|
|
117
|
+
/* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-2", children: [
|
|
118
|
+
live ? /* @__PURE__ */ jsxs(
|
|
119
|
+
"span",
|
|
120
|
+
{
|
|
121
|
+
className: "text-primary flex items-center gap-2 text-sm font-medium",
|
|
122
|
+
"data-test": "toolbar-live-badge",
|
|
123
|
+
children: [
|
|
124
|
+
/* @__PURE__ */ jsx(Icon, { name: "fa-solid fa-tower-broadcast", className: "h-4 w-4" }),
|
|
125
|
+
"Reaching ",
|
|
126
|
+
resolveEgress(integration).join(", ")
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
) : null,
|
|
130
|
+
/* @__PURE__ */ jsxs(
|
|
131
|
+
Button,
|
|
132
|
+
{
|
|
133
|
+
size: "sm",
|
|
134
|
+
variant: "outline",
|
|
135
|
+
onClick: () => setTheme(resolvedTheme === "dark" ? "light" : "dark"),
|
|
136
|
+
"aria-label": "Toggle theme",
|
|
137
|
+
children: [
|
|
138
|
+
/* @__PURE__ */ jsxs("span", { className: cn("flex items-center dark:hidden"), children: [
|
|
139
|
+
/* @__PURE__ */ jsx(Icon, { name: "fa-solid fa-moon", className: "mr-2 h-4 w-4" }),
|
|
140
|
+
"Dark"
|
|
141
|
+
] }),
|
|
142
|
+
/* @__PURE__ */ jsxs("span", { className: cn("hidden items-center dark:flex"), children: [
|
|
143
|
+
/* @__PURE__ */ jsx(Icon, { name: "fa-solid fa-sun-bright", className: "mr-2 h-4 w-4" }),
|
|
144
|
+
"Light"
|
|
145
|
+
] })
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
),
|
|
149
|
+
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: reset, children: "Reset" })
|
|
150
|
+
] })
|
|
151
|
+
] }) });
|
|
152
|
+
}
|
|
153
|
+
export {
|
|
154
|
+
DevToolbar
|
|
155
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { DataMode, FixtureSeed, HarnessLiveMode } from '../../registry.js';
|
|
3
|
+
import type { CompiledFixture } from '../lib/http-fixtures.js';
|
|
4
|
+
export declare function HarnessProviders({ seeds, mode, live, egress, httpFixtures, slug, children, }: {
|
|
5
|
+
seeds: FixtureSeed[];
|
|
6
|
+
/** `fixtures` (default) or `live`. See `lib/harness-query-client.ts`. */
|
|
7
|
+
mode?: DataMode;
|
|
8
|
+
/** The integration's live-mode descriptor, when it declares one. */
|
|
9
|
+
live?: HarnessLiveMode;
|
|
10
|
+
/** Origins live mode may reach — resolved centrally, see `resolveEgress`. */
|
|
11
|
+
egress: readonly string[];
|
|
12
|
+
/** Compiled HTTP fixtures for the active variant. */
|
|
13
|
+
httpFixtures: readonly CompiledFixture[];
|
|
14
|
+
slug: string;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Suspense, useMemo } from "react";
|
|
4
|
+
import {
|
|
5
|
+
IntegrationActivationProvider
|
|
6
|
+
} from "@ekanos/sdk/hooks";
|
|
7
|
+
import { AskAssistantProvider } from "@ekanos/sdk/hooks";
|
|
8
|
+
import { WidgetEventProvider } from "@ekanos/sdk/hooks";
|
|
9
|
+
import { QueryClientProvider } from "@tanstack/react-query";
|
|
10
|
+
import { TeamAccountWorkspaceContextProvider } from "../../mocks/team-account-workspace.js";
|
|
11
|
+
import {
|
|
12
|
+
installHarnessFetch,
|
|
13
|
+
publishHarnessRouting
|
|
14
|
+
} from "../lib/harness-fetch-interceptor.js";
|
|
15
|
+
import {
|
|
16
|
+
createAllowlistedFetch,
|
|
17
|
+
createFixturesFetch,
|
|
18
|
+
logHarnessFetch
|
|
19
|
+
} from "../lib/harness-live-fetch.js";
|
|
20
|
+
import { createHarnessQueryClient } from "../lib/harness-query-client.js";
|
|
21
|
+
import { redactSensitive } from "../lib/redact.js";
|
|
22
|
+
import { AskAssistantBridge } from "./ask-assistant-bridge.js";
|
|
23
|
+
installHarnessFetch();
|
|
24
|
+
const fixtureActivationActions = {
|
|
25
|
+
activate: async (input) => {
|
|
26
|
+
console.info(
|
|
27
|
+
"[harness] activate (fixture, not persisted):",
|
|
28
|
+
redactSensitive(input)
|
|
29
|
+
);
|
|
30
|
+
return { success: true };
|
|
31
|
+
},
|
|
32
|
+
deactivate: async (input) => {
|
|
33
|
+
console.info(
|
|
34
|
+
"[harness] deactivate (fixture, not persisted):",
|
|
35
|
+
redactSensitive(input)
|
|
36
|
+
);
|
|
37
|
+
return { success: true };
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function HarnessProviders({
|
|
41
|
+
seeds,
|
|
42
|
+
mode = "fixtures",
|
|
43
|
+
live,
|
|
44
|
+
egress,
|
|
45
|
+
httpFixtures,
|
|
46
|
+
slug,
|
|
47
|
+
children
|
|
48
|
+
}) {
|
|
49
|
+
const queryClient = useMemo(
|
|
50
|
+
() => createHarnessQueryClient(seeds, mode),
|
|
51
|
+
[seeds, mode]
|
|
52
|
+
);
|
|
53
|
+
const harnessFetch = useMemo(() => {
|
|
54
|
+
if (mode === "live") {
|
|
55
|
+
return createAllowlistedFetch(egress, { onCall: logHarnessFetch(slug) });
|
|
56
|
+
}
|
|
57
|
+
return createFixturesFetch();
|
|
58
|
+
}, [mode, egress, slug]);
|
|
59
|
+
useMemo(() => {
|
|
60
|
+
installHarnessFetch();
|
|
61
|
+
publishHarnessRouting({
|
|
62
|
+
mode,
|
|
63
|
+
fixtures: httpFixtures,
|
|
64
|
+
egress: mode === "live" ? egress : [],
|
|
65
|
+
onCall: ({ url, method, outcome }) => {
|
|
66
|
+
if (outcome === "refused") {
|
|
67
|
+
console.error(`[harness:${slug}] refused ${method} ${url}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
console.info(`[harness:${slug}] ${outcome} ${method} ${url}`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}, [mode, httpFixtures, egress, slug]);
|
|
74
|
+
const FetchProvider = live?.FetchProvider;
|
|
75
|
+
const tree = /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children: /* @__PURE__ */ jsx(TeamAccountWorkspaceContextProvider, { children: /* @__PURE__ */ jsx(IntegrationActivationProvider, { actions: fixtureActivationActions, children: /* @__PURE__ */ jsx(WidgetEventProvider, { children: /* @__PURE__ */ jsxs(AskAssistantProvider, { children: [
|
|
76
|
+
/* @__PURE__ */ jsx(AskAssistantBridge, {}),
|
|
77
|
+
/* @__PURE__ */ jsx(Suspense, { fallback: null, children })
|
|
78
|
+
] }) }) }) }) });
|
|
79
|
+
return FetchProvider ? /* @__PURE__ */ jsx(FetchProvider, { fetch: harnessFetch, children: tree }) : tree;
|
|
80
|
+
}
|
|
81
|
+
export {
|
|
82
|
+
HarnessProviders
|
|
83
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { WidgetAskContext } from '@ekanos/sdk';
|
|
3
|
+
import { type WidgetHealth, type WidgetRenderState } from '@ekanos/sdk/components';
|
|
4
|
+
/**
|
|
5
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
* The harness's widget context provider.
|
|
7
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* WHY THIS EXISTS — the one symbol the harness could not import.
|
|
10
|
+
*
|
|
11
|
+
* `Widget.*` reads everything (render state, collapse/pin, title, ask config)
|
|
12
|
+
* from `WidgetContext`, and something has to PROVIDE it. In the real app that is
|
|
13
|
+
* `DashboardWidgetProvider`, which lives in the host's own package and is not
|
|
14
|
+
* part of the published `@ekanos/sdk` surface — on purpose. It is coupled to
|
|
15
|
+
* things that only exist inside Fusion: `'use server'` persistence callbacks
|
|
16
|
+
* for the widget-config hierarchy, and the Supabase rows behind them. The SDK
|
|
17
|
+
* inversion drew the line exactly there: the SDK exports the CONSUMER side
|
|
18
|
+
* (`WidgetContext` plus `WidgetContextValue` / `WidgetState` / `WidgetActions` /
|
|
19
|
+
* `WidgetMeta` / `WidgetRenderState` / `WidgetHealth`) so any shell can fill it,
|
|
20
|
+
* and keeps the persisting provider host-side.
|
|
21
|
+
*
|
|
22
|
+
* So this is not a copy of an unpublished component — it is the documented
|
|
23
|
+
* alternative. `dashboard-widget-provider.tsx` says so in its own doc comment:
|
|
24
|
+
*
|
|
25
|
+
* "For non-persistent contexts (e.g. admin previews), use a different
|
|
26
|
+
* provider that implements the same `WidgetContextValue` interface but
|
|
27
|
+
* stores state locally only."
|
|
28
|
+
*
|
|
29
|
+
* That is precisely the harness: no database, so nothing to persist. Collapse
|
|
30
|
+
* and pin toggle locally and reset on reload, which is correct harness
|
|
31
|
+
* behaviour and is what the host provider did here anyway once its optional
|
|
32
|
+
* callbacks were omitted.
|
|
33
|
+
*
|
|
34
|
+
* Kept deliberately faithful in one non-obvious respect: the `isEnabled` and
|
|
35
|
+
* `health` downgrade rules below are the host's, verbatim, because a widget's
|
|
36
|
+
* `disabled` state is reached through them and a harness that resolved state
|
|
37
|
+
* differently would show a state the product never shows. `pendingCollapse` /
|
|
38
|
+
* `pendingPin` are always false — with no persistence callback there is never
|
|
39
|
+
* anything in flight, exactly as in the real grid when it passes none.
|
|
40
|
+
*/
|
|
41
|
+
export interface HarnessWidgetProviderProps {
|
|
42
|
+
widgetId: string;
|
|
43
|
+
title?: string;
|
|
44
|
+
subtitle?: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
className?: string;
|
|
47
|
+
state: WidgetRenderState;
|
|
48
|
+
isEnabled?: boolean;
|
|
49
|
+
isCollapsible?: boolean;
|
|
50
|
+
isPinnable?: boolean;
|
|
51
|
+
defaultCollapsed?: boolean;
|
|
52
|
+
defaultPinned?: boolean;
|
|
53
|
+
maxContentHeight?: string;
|
|
54
|
+
askContext?: WidgetAskContext;
|
|
55
|
+
aiFooterEnabled?: boolean;
|
|
56
|
+
/** Integration health; `unhealthy` downgrades the widget to `disabled`. */
|
|
57
|
+
health?: WidgetHealth;
|
|
58
|
+
children: ReactNode;
|
|
59
|
+
}
|
|
60
|
+
export declare function HarnessWidgetProvider({ widgetId, title, subtitle, description, className, state: renderState, isEnabled, isCollapsible, isPinnable, defaultCollapsed, defaultPinned, maxContentHeight, askContext, aiFooterEnabled, health, children, }: HarnessWidgetProviderProps): import("react").JSX.Element;
|