okengine 0.14.1 → 0.15.2
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/package.json +3 -4
- package/site/content/docs/elements/ai.mdx +2 -1
- package/site/content/docs/elements/channel.mdx +1 -0
- package/site/content/docs/elements/clock.mdx +3 -0
- package/site/content/docs/elements/flow.mdx +3 -0
- package/site/content/docs/elements/gate.mdx +25 -22
- package/site/content/docs/elements/signal.mdx +4 -4
- package/site/content/docs/elements/store.mdx +86 -27
- package/site/content/docs/elements/vault.mdx +1 -0
- package/site/content/docs/get-started/basic-usage.mdx +22 -13
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/get-started/introduction.mdx +11 -3
- package/site/content/docs/get-started/meta.json +1 -1
- package/site/content/docs/get-started/project-structure.mdx +932 -0
- package/site/content/docs/reference/cli.md +13 -10
- package/site/content/docs/reference/client.mdx +25 -21
- package/site/content/docs/reference/security.md +1 -1
- package/src/auth/bindings.ts +14 -5
- package/src/auth/cross-plane.ts +10 -3
- package/src/cli/ai-setup/ai-setup.test.ts +62 -0
- package/src/cli/ai-setup/apply.ts +32 -1
- package/src/cli/attach-host-console.test.ts +3 -2
- package/src/cli/attach-host-console.ts +3 -0
- package/src/cli/build.test.ts +4 -2
- package/src/cli/db-seed.ts +48 -15
- package/src/cli/db.test.ts +104 -0
- package/src/cli/db.ts +122 -10
- package/src/cli/dev-controls.test.ts +20 -0
- package/src/cli/dev-controls.ts +13 -0
- package/src/cli/dev.test.ts +15 -4
- package/src/cli/dev.ts +102 -29
- package/src/cli/ensure-drizzle-config.ts +2 -1
- package/src/cli/tui/DevLive.tsx +12 -2
- package/src/cli/tui/keys.test.ts +2 -1
- package/src/cli/tui/keys.ts +3 -2
- package/src/compiler/aot.test.ts +1 -1
- package/src/compiler/extract.test.ts +409 -33
- package/src/compiler/extract.ts +318 -67
- package/src/compiler/flow-path.test.ts +96 -0
- package/src/compiler/flow-path.ts +173 -0
- package/src/compiler/generate-adopt.test.ts +112 -3
- package/src/compiler/generate-adopt.ts +280 -26
- package/src/compiler/schema-from-ast.ts +609 -0
- package/src/console/server/bind.ts +2 -2
- package/src/console/server/console.test.ts +3 -0
- package/src/console/server/flows.ts +120 -40
- package/src/console/server/invoke-user-flow.test.ts +3 -1
- package/src/console/server/invoke-user-flow.ts +17 -2
- package/src/console/server/iso-at.test.ts +9 -0
- package/src/console/server/iso-at.ts +11 -0
- package/src/console/server/security-headers.ts +6 -3
- package/src/console/server/spa-proxy.test.ts +1 -0
- package/src/console/server/sql-catalog.ts +147 -7
- package/src/console/server/sql-rls.test.ts +16 -0
- package/src/console/server/state.ts +8 -0
- package/src/console/server/store.test.ts +7 -4
- package/src/console/server/store.ts +142 -58
- package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-BanhLsEY.js → cache-glyph-F1FI122b.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
- package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
- package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
- package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
- package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
- package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
- package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
- package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
- package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
- package/src/console/ui-next/dist/assets/{observability-page-BEZDzyYh.js → observability-page-HK9-BO8z.js} +2 -2
- package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
- package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
- package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
- package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
- package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
- package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
- package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
- package/src/console/ui-next/dist/index.html +6 -6
- package/src/console/ui-next/src/client.ts +35 -7
- package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
- package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
- package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
- package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
- package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +19 -1
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
- package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
- package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
- package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
- package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
- package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
- package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
- package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
- package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
- package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
- package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
- package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
- package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
- package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
- package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
- package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
- package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
- package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
- package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
- package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
- package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
- package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
- package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
- package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
- package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
- package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
- package/src/docker/compose-health.test.ts +27 -0
- package/src/docker/compose-health.ts +19 -3
- package/src/docker/docker.test.ts +3 -0
- package/src/docker/recipes/pgdog.ts +2 -0
- package/src/drivers/clock-postgres.ts +13 -9
- package/src/drivers/instances-postgres.ts +4 -6
- package/src/drivers/journal-postgres.ts +13 -9
- package/src/drivers/pg-rls.test.ts +87 -0
- package/src/drivers/pg-rls.ts +233 -7
- package/src/drivers/pglite.ts +26 -3
- package/src/drivers/postgres.test.ts +23 -0
- package/src/drivers/postgres.ts +133 -9
- package/src/drivers/types.ts +10 -0
- package/src/elements/clock/declare.ts +26 -1
- package/src/elements/gate/boot.ts +5 -5
- package/src/elements/gate/declare.ts +36 -4
- package/src/elements/gate/flatten.ts +1 -1
- package/src/elements/gate/runtime.ts +1 -1
- package/src/elements/store/declare.ts +9 -6
- package/src/elements/store/emit-drizzle.ts +44 -8
- package/src/elements/store/resource.test.ts +1 -1
- package/src/elements/store/rls-identity.test.ts +95 -0
- package/src/elements/store/rls-identity.ts +116 -0
- package/src/elements/store/runtime.ts +4 -0
- package/src/elements/store/schema-decl.test.ts +37 -0
- package/src/elements/store/schema-decl.ts +161 -0
- package/src/elements/store/seed.ts +1 -1
- package/src/elements/store/sql-condition.test.ts +12 -0
- package/src/elements/store/sql-condition.ts +26 -2
- package/src/elements/store/sql-rls-isolation.test.ts +165 -0
- package/src/elements/store/sql-rls-stamp.test.ts +117 -0
- package/src/elements/store/sql-session.ts +81 -3
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/store.ts +14 -0
- package/src/http.ts +20 -3
- package/src/index.ts +10 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/adopt-routes.ts +30 -2
- package/src/kernel/app.ts +131 -14
- package/src/kernel/auto-cache.test.ts +8 -8
- package/src/kernel/auto-registry.test.ts +49 -9
- package/src/kernel/boot.test.ts +4 -4
- package/src/kernel/boot.ts +2 -2
- package/src/kernel/effects-stamping.test.ts +3 -3
- package/src/kernel/element-registries.ts +12 -5
- package/src/kernel/errors.ts +24 -0
- package/src/kernel/flow-units.ts +64 -0
- package/src/kernel/flow.ts +29 -17
- package/src/kernel/fx.ts +23 -0
- package/src/kernel/horizontal-child.ts +4 -4
- package/src/kernel/http-path-pending.ts +20 -0
- package/src/kernel/http-resource.ts +4 -0
- package/src/kernel/http-stream.test.ts +4 -4
- package/src/kernel/pipeline.test.ts +2 -2
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +2 -2
- package/src/kernel/resource-mount.test.ts +14 -0
- package/src/kernel/run-duration.test.ts +2 -2
- package/src/kernel/stamp-http.test.ts +79 -0
- package/src/kernel/stamp-http.ts +75 -0
- package/src/kernel/triggers.ts +52 -27
- package/src/manifest/types.ts +13 -0
- package/src/release/measure.ts +2 -2
- package/src/runtime/json-code-block.test.ts +2 -2
- package/src/runtime/serve.test.ts +4 -4
- package/src/term.test.ts +52 -4
- package/src/term.ts +125 -17
- package/src/test/reset-element-registries.ts +8 -3
- package/src/cli/ask-seed.test.ts +0 -96
- package/src/cli/ask-seed.ts +0 -82
- package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-DxDsOd4f.js +0 -1
- package/src/console/ui-next/dist/assets/http-method-CJCBYL2j.js +0 -1
- package/src/console/ui-next/dist/assets/index-Ce6WKWKM.js +0 -66
- package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
- package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
- package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
- package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
- package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
- package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
- package/src/console/ui-next/dist/assets/trace-detail-sheet-DFLFfUUX.js +0 -2
- package/src/console/ui-next/dist/assets/units-page-C0gW6Kdo.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-DuKzqwzW.js +0 -2
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
queryGateToolbarLabel,
|
|
22
22
|
queryGateUserChoices,
|
|
23
23
|
type QueryGateMode,
|
|
24
|
+
type RlsPickerValue,
|
|
24
25
|
} from "../lib/query-gate.ts";
|
|
25
26
|
import { mergeRlsGateCatalog, rlsGateCatalog } from "../lib/rls-gate-catalog.ts";
|
|
26
27
|
import {
|
|
@@ -35,8 +36,8 @@ import {
|
|
|
35
36
|
/** Props for {@link QueryGateMenu}. */
|
|
36
37
|
export interface QueryGateMenuProps {
|
|
37
38
|
readonly manifest: Manifest | null;
|
|
38
|
-
readonly
|
|
39
|
-
readonly onChange: (
|
|
39
|
+
readonly value: RlsPickerValue;
|
|
40
|
+
readonly onChange: (next: RlsPickerValue) => void;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
/**
|
|
@@ -44,7 +45,8 @@ export interface QueryGateMenuProps {
|
|
|
44
45
|
*
|
|
45
46
|
* @param props - Manifest + current pick
|
|
46
47
|
*/
|
|
47
|
-
export function QueryGateMenu({ manifest,
|
|
48
|
+
export function QueryGateMenu({ manifest, value, onChange }: QueryGateMenuProps): JSX.Element {
|
|
49
|
+
const { asGate, asUserId } = value;
|
|
48
50
|
const gatesQuery = useGates(true);
|
|
49
51
|
const identitiesQuery = useFlowsIdentities();
|
|
50
52
|
const catalog = useMemo(
|
|
@@ -61,33 +63,30 @@ export function QueryGateMenu({ manifest, asGate, onChange }: QueryGateMenuProps
|
|
|
61
63
|
"Intentionally unauthenticated.";
|
|
62
64
|
const mode = queryGateMode(asGate);
|
|
63
65
|
const active = asGate !== null;
|
|
64
|
-
const [
|
|
65
|
-
const [asTab, setAsTab] = useState<GateAsTab>("policy");
|
|
66
|
+
const [asTab, setAsTab] = useState<GateAsTab>(asUserId ? "user" : "policy");
|
|
66
67
|
const selectedPolicy = policies.find((policy) => policy.id === asGate) ?? null;
|
|
67
68
|
const selectedUser = users.find((user) => user.id === asUserId && user.gate === asGate) ?? null;
|
|
68
69
|
const asEnabled = policies.length > 0 || users.length > 0;
|
|
69
70
|
|
|
70
71
|
const selectMode = (next: QueryGateMode): void => {
|
|
71
72
|
if (next === "operator") {
|
|
72
|
-
|
|
73
|
-
onChange(null);
|
|
73
|
+
onChange({ asGate: null, asUserId: null });
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
76
|
if (next === "public") {
|
|
77
|
-
|
|
78
|
-
onChange("public");
|
|
77
|
+
onChange({ asGate: "public", asUserId: null });
|
|
79
78
|
return;
|
|
80
79
|
}
|
|
81
80
|
if (mode === "as") return;
|
|
82
81
|
const fallback = selectedPolicy?.id ?? policies[0]?.id ?? users[0]?.gate ?? null;
|
|
83
82
|
if (fallback === null) return;
|
|
84
83
|
if (users[0] && fallback === users[0].gate && policies.length === 0) {
|
|
85
|
-
setAsUserId(users[0].id);
|
|
86
84
|
setAsTab("user");
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
onChange({ asGate: users[0].gate, asUserId: users[0].id });
|
|
86
|
+
return;
|
|
89
87
|
}
|
|
90
|
-
|
|
88
|
+
setAsTab("policy");
|
|
89
|
+
onChange({ asGate: fallback, asUserId: null });
|
|
91
90
|
};
|
|
92
91
|
|
|
93
92
|
return (
|
|
@@ -111,7 +110,9 @@ export function QueryGateMenu({ manifest, asGate, onChange }: QueryGateMenuProps
|
|
|
111
110
|
data-icon="inline-start"
|
|
112
111
|
className="size-3.5"
|
|
113
112
|
/>
|
|
114
|
-
<span className="truncate">
|
|
113
|
+
<span className="truncate">
|
|
114
|
+
{queryGateToolbarLabel(asGate, asUserId, selectedUser?.label)}
|
|
115
|
+
</span>
|
|
115
116
|
</Button>
|
|
116
117
|
)}
|
|
117
118
|
/>
|
|
@@ -143,12 +144,12 @@ export function QueryGateMenu({ manifest, asGate, onChange }: QueryGateMenuProps
|
|
|
143
144
|
</GateModeStrip>
|
|
144
145
|
{mode === "operator" ? (
|
|
145
146
|
<GateModeDetail title="Operator" badge="Default">
|
|
146
|
-
Bypasses RLS. No <span className="font-mono">oke.gate</span>
|
|
147
|
+
Bypasses RLS. No <span className="font-mono">oke.gate()</span> stamp.
|
|
147
148
|
</GateModeDetail>
|
|
148
149
|
) : null}
|
|
149
150
|
{mode === "public" ? (
|
|
150
151
|
<GateModeDetail title="Public">
|
|
151
|
-
{publicDetail} Sets <span className="font-mono">oke.gate</span> to{" "}
|
|
152
|
+
{publicDetail} Sets <span className="font-mono">oke.gate()</span> to{" "}
|
|
152
153
|
<span className="font-mono">public</span>.
|
|
153
154
|
</GateModeDetail>
|
|
154
155
|
) : null}
|
|
@@ -161,14 +162,12 @@ export function QueryGateMenu({ manifest, asGate, onChange }: QueryGateMenuProps
|
|
|
161
162
|
asGate={asGate}
|
|
162
163
|
asUserId={selectedUser?.id ?? null}
|
|
163
164
|
onSelectPolicy={(policy) => {
|
|
164
|
-
setAsUserId(null);
|
|
165
165
|
setAsTab("policy");
|
|
166
|
-
onChange(policy.id);
|
|
166
|
+
onChange({ asGate: policy.id, asUserId: null });
|
|
167
167
|
}}
|
|
168
168
|
onSelectUser={(user) => {
|
|
169
|
-
setAsUserId(user.id);
|
|
170
169
|
setAsTab("user");
|
|
171
|
-
onChange(user.gate);
|
|
170
|
+
onChange({ asGate: user.gate, asUserId: user.id });
|
|
172
171
|
}}
|
|
173
172
|
/>
|
|
174
173
|
) : null}
|
|
@@ -263,7 +263,8 @@ export function GateAsPicker({
|
|
|
263
263
|
</>
|
|
264
264
|
) : (
|
|
265
265
|
<>
|
|
266
|
-
{selected.detail} Sets <span className="font-mono">oke.gate</span>
|
|
266
|
+
{selected.detail} Sets <span className="font-mono">oke.gate()</span> /{" "}
|
|
267
|
+
<span className="font-mono">oke.user()</span> for RLS.
|
|
267
268
|
</>
|
|
268
269
|
)}
|
|
269
270
|
</p>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Query console results — table / JSON / raw, copy, download, cell inspect.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { useMemo, useState, type JSX } from "react";
|
|
5
|
+
import { useEffect, useMemo, useRef, useState, type JSX } from "react";
|
|
6
6
|
import {
|
|
7
7
|
ArrowDown01Icon,
|
|
8
8
|
ArrowUp01Icon,
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
EmptyMedia,
|
|
31
31
|
EmptyTitle,
|
|
32
32
|
} from "@/components/ui/empty";
|
|
33
|
+
import { EASE_OUT } from "@/lib/ease.ts";
|
|
34
|
+
import { motion, useReducedMotion } from "@/lib/motion.ts";
|
|
33
35
|
import { cn } from "@/lib/utils.ts";
|
|
34
36
|
import { JsonValueSheet } from "../detail/json-value-sheet.tsx";
|
|
35
37
|
import { ToolbarTip } from "@/components/ui/toolbar-tip.tsx";
|
|
@@ -104,6 +106,14 @@ export function QueryResults({
|
|
|
104
106
|
);
|
|
105
107
|
}, [rows, columns]);
|
|
106
108
|
|
|
109
|
+
const reduce = useReducedMotion() ?? false;
|
|
110
|
+
const wasPending = useRef(pending);
|
|
111
|
+
const [appearId, setAppearId] = useState(0);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (wasPending.current && !pending) setAppearId((n) => n + 1);
|
|
114
|
+
wasPending.current = pending;
|
|
115
|
+
}, [pending]);
|
|
116
|
+
|
|
107
117
|
const copy = (kind: "json" | "csv"): void => {
|
|
108
118
|
const text = kind === "csv" ? csvText : jsonText;
|
|
109
119
|
if (!navigator.clipboard || text.length === 0) return;
|
|
@@ -210,93 +220,110 @@ export function QueryResults({
|
|
|
210
220
|
|
|
211
221
|
{collapsed ? null : (
|
|
212
222
|
<div className="min-h-0 flex-1 overflow-auto">
|
|
213
|
-
{
|
|
214
|
-
<p
|
|
215
|
-
className="px-3 py-4 font-mono text-xs leading-relaxed text-destructive"
|
|
216
|
-
role="alert"
|
|
217
|
-
>
|
|
218
|
-
{error}
|
|
219
|
-
</p>
|
|
220
|
-
) : pending ? (
|
|
223
|
+
{pending ? (
|
|
221
224
|
<p className="px-3 py-8 text-center text-sm text-muted-foreground">Running…</p>
|
|
222
|
-
) : rows === null && view !== "raw" ? (
|
|
223
|
-
<Empty className="h-full min-h-40 border-0">
|
|
224
|
-
<EmptyHeader>
|
|
225
|
-
<EmptyMedia variant="icon">
|
|
226
|
-
<HugeiconsIcon icon={PlayIcon} className="size-4" />
|
|
227
|
-
</EmptyMedia>
|
|
228
|
-
<EmptyTitle>No results yet</EmptyTitle>
|
|
229
|
-
<EmptyDescription>Run a query to see results here.</EmptyDescription>
|
|
230
|
-
</EmptyHeader>
|
|
231
|
-
</Empty>
|
|
232
|
-
) : view === "raw" ? (
|
|
233
|
-
executed ? (
|
|
234
|
-
<div className="p-3" data-slot="store-query-raw">
|
|
235
|
-
<QueryHighlightView
|
|
236
|
-
code={executed}
|
|
237
|
-
language={executedLanguage}
|
|
238
|
-
className="text-xs leading-5"
|
|
239
|
-
/>
|
|
240
|
-
</div>
|
|
241
|
-
) : (
|
|
242
|
-
<p className="px-3 py-8 text-center text-sm text-muted-foreground">
|
|
243
|
-
Nothing executed yet.
|
|
244
|
-
</p>
|
|
245
|
-
)
|
|
246
|
-
) : view === "json" ? (
|
|
247
|
-
<HighlightedJson
|
|
248
|
-
json={jsonText}
|
|
249
|
-
dataSlot="store-query-json"
|
|
250
|
-
className="min-h-full p-3"
|
|
251
|
-
/>
|
|
252
|
-
) : rows && rows.length === 0 ? (
|
|
253
|
-
<p className="px-3 py-8 text-center text-sm text-muted-foreground">
|
|
254
|
-
Query returned 0 rows.
|
|
255
|
-
</p>
|
|
256
225
|
) : (
|
|
257
|
-
<
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
226
|
+
<motion.div
|
|
227
|
+
key={appearId}
|
|
228
|
+
initial={
|
|
229
|
+
appearId === 0
|
|
230
|
+
? false
|
|
231
|
+
: reduce
|
|
232
|
+
? { opacity: 0 }
|
|
233
|
+
: { opacity: 0, transform: "translateY(4px)" }
|
|
234
|
+
}
|
|
235
|
+
animate={reduce ? { opacity: 1 } : { opacity: 1, transform: "translateY(0px)" }}
|
|
236
|
+
transition={{ duration: reduce ? 0.12 : 0.18, ease: EASE_OUT }}
|
|
237
|
+
>
|
|
238
|
+
{error ? (
|
|
239
|
+
<p
|
|
240
|
+
className="px-3 py-4 font-mono text-xs leading-relaxed text-destructive"
|
|
241
|
+
role="alert"
|
|
242
|
+
>
|
|
243
|
+
{error}
|
|
244
|
+
</p>
|
|
245
|
+
) : rows === null && view !== "raw" ? (
|
|
246
|
+
<Empty className="h-full min-h-40 border-0">
|
|
247
|
+
<EmptyHeader>
|
|
248
|
+
<EmptyMedia variant="icon">
|
|
249
|
+
<HugeiconsIcon icon={PlayIcon} className="size-4" />
|
|
250
|
+
</EmptyMedia>
|
|
251
|
+
<EmptyTitle>No results yet</EmptyTitle>
|
|
252
|
+
<EmptyDescription>Run a query to see results here.</EmptyDescription>
|
|
253
|
+
</EmptyHeader>
|
|
254
|
+
</Empty>
|
|
255
|
+
) : view === "raw" ? (
|
|
256
|
+
executed ? (
|
|
257
|
+
<div className="p-3" data-slot="store-query-raw">
|
|
258
|
+
<QueryHighlightView
|
|
259
|
+
code={executed}
|
|
260
|
+
language={executedLanguage}
|
|
261
|
+
className="text-xs leading-5"
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
264
|
+
) : (
|
|
265
|
+
<p className="px-3 py-8 text-center text-sm text-muted-foreground">
|
|
266
|
+
Nothing executed yet.
|
|
267
|
+
</p>
|
|
268
|
+
)
|
|
269
|
+
) : view === "json" ? (
|
|
270
|
+
<HighlightedJson
|
|
271
|
+
json={jsonText}
|
|
272
|
+
dataSlot="store-query-json"
|
|
273
|
+
className="min-h-full p-3"
|
|
274
|
+
/>
|
|
275
|
+
) : rows && rows.length === 0 ? (
|
|
276
|
+
<p className="px-3 py-8 text-center text-sm text-muted-foreground">
|
|
277
|
+
Query returned 0 rows.
|
|
278
|
+
</p>
|
|
279
|
+
) : (
|
|
280
|
+
<table className="w-full min-w-max border-collapse text-left text-[12px]">
|
|
281
|
+
<thead className="sticky top-0 z-10 bg-background">
|
|
282
|
+
<tr>
|
|
283
|
+
{columns.map((col) => (
|
|
284
|
+
<th
|
|
279
285
|
key={col}
|
|
280
|
-
|
|
286
|
+
scope="col"
|
|
287
|
+
className="border-b border-border/60 px-3 py-1.5 font-mono text-[10px] font-semibold tracking-[0.08em] text-muted-foreground uppercase"
|
|
281
288
|
>
|
|
282
|
-
{
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
289
|
+
{col}
|
|
290
|
+
</th>
|
|
291
|
+
))}
|
|
292
|
+
</tr>
|
|
293
|
+
</thead>
|
|
294
|
+
<tbody>
|
|
295
|
+
{(rows ?? []).map((row, i) => (
|
|
296
|
+
<tr key={i} className="border-b border-border/60 hover:bg-muted/50">
|
|
297
|
+
{columns.map((col) => {
|
|
298
|
+
const value = row[col];
|
|
299
|
+
const inspectable = asInspectableJson(value) !== null;
|
|
300
|
+
return (
|
|
301
|
+
<td
|
|
302
|
+
key={col}
|
|
303
|
+
className="max-w-xs truncate px-3 py-1.5 font-mono text-foreground/90"
|
|
287
304
|
>
|
|
288
|
-
{
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
305
|
+
{inspectable ? (
|
|
306
|
+
<button
|
|
307
|
+
type="button"
|
|
308
|
+
className="max-w-full truncate text-left underline decoration-border/70 underline-offset-2 hover:text-foreground"
|
|
309
|
+
onClick={() =>
|
|
310
|
+
setInspect({ rowId: String(i), column: col, value })
|
|
311
|
+
}
|
|
312
|
+
>
|
|
313
|
+
{formatGridCell(value)}
|
|
314
|
+
</button>
|
|
315
|
+
) : (
|
|
316
|
+
formatGridCell(value)
|
|
317
|
+
)}
|
|
318
|
+
</td>
|
|
319
|
+
);
|
|
320
|
+
})}
|
|
321
|
+
</tr>
|
|
322
|
+
))}
|
|
323
|
+
</tbody>
|
|
324
|
+
</table>
|
|
325
|
+
)}
|
|
326
|
+
</motion.div>
|
|
300
327
|
)}
|
|
301
328
|
</div>
|
|
302
329
|
)}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { Handle, Position, type Node, type NodeProps } from "@xyflow/react";
|
|
6
6
|
import type { JSX } from "react";
|
|
7
|
+
import { ShieldEnergyIcon, ShieldOff } from "@hugeicons/core-free-icons";
|
|
8
|
+
import { HugeiconsIcon } from "@hugeicons/react";
|
|
7
9
|
import { cn } from "@/lib/utils.ts";
|
|
8
10
|
import {
|
|
9
11
|
schemaRelationLabel,
|
|
@@ -47,6 +49,21 @@ export function SchemaTableNode({ data }: NodeProps<SchemaTableNode>): JSX.Eleme
|
|
|
47
49
|
>
|
|
48
50
|
<span className="size-2 shrink-0 rounded-full" style={{ background: hex }} aria-hidden />
|
|
49
51
|
<span className="min-w-0 truncate font-semibold text-foreground">{table.name}</span>
|
|
52
|
+
<span
|
|
53
|
+
data-slot="store-schema-rls"
|
|
54
|
+
data-rls={table.rls === true ? "on" : "off"}
|
|
55
|
+
title={table.rls === true ? "RLS enabled" : "RLS disabled"}
|
|
56
|
+
className={cn(
|
|
57
|
+
"shrink-0",
|
|
58
|
+
table.rls === true ? "text-emerald-500" : "text-muted-foreground/35",
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
<HugeiconsIcon
|
|
62
|
+
icon={table.rls === true ? ShieldEnergyIcon : ShieldOff}
|
|
63
|
+
className="size-3"
|
|
64
|
+
aria-hidden
|
|
65
|
+
/>
|
|
66
|
+
</span>
|
|
50
67
|
{showStore ? (
|
|
51
68
|
<span className="ml-auto shrink-0 font-mono text-[9px] tracking-[0.08em] text-muted-foreground uppercase">
|
|
52
69
|
{table.storeName}
|
|
@@ -53,8 +53,10 @@ import { cn } from "@/lib/utils";
|
|
|
53
53
|
import type { RunCache } from "@/features/flows/traces/cache-icon.ts";
|
|
54
54
|
import { CacheGlyph } from "@/features/flows/traces/cache-glyph.tsx";
|
|
55
55
|
import { formatDuration } from "@/features/flows/traces/format-duration.ts";
|
|
56
|
+
import { httpMethodHasBody } from "@/features/flows/traces/http-method.ts";
|
|
56
57
|
import { useStoreQuery } from "@/features/store/data/use-store-query.ts";
|
|
57
58
|
import { useClockRunNow, useFlowsIdentities, useFlowsInvoke } from "../data/use-flows-invoke.ts";
|
|
59
|
+
import { pickSeedFields, splitCallApiInput } from "../lib/contract-input.ts";
|
|
58
60
|
import {
|
|
59
61
|
fieldConstraintHint,
|
|
60
62
|
fieldsFromSchema,
|
|
@@ -313,33 +315,41 @@ function InvokeBodyPanel({
|
|
|
313
315
|
() => (kind === "http" && row.path ? pathParamNames(row.path) : []),
|
|
314
316
|
[kind, row.path],
|
|
315
317
|
);
|
|
318
|
+
const split = useMemo(
|
|
319
|
+
() =>
|
|
320
|
+
splitCallApiInput(fields, {
|
|
321
|
+
http: kind === "http",
|
|
322
|
+
method: row.method,
|
|
323
|
+
pathParams: params,
|
|
324
|
+
}),
|
|
325
|
+
[fields, kind, row.method, params],
|
|
326
|
+
);
|
|
327
|
+
const showBody = kind !== "http" || httpMethodHasBody(row.method);
|
|
316
328
|
|
|
317
329
|
const [invokeAs, setInvokeAs] = useState<CallInvokeAs>({ asGate: null, asUserId: null });
|
|
318
330
|
const [piiMasked, setPiiMasked] = useState(true);
|
|
319
331
|
const [pathValues, setPathValues] = useState<Record<string, string>>(() =>
|
|
320
332
|
seedPathValues(row.path, params),
|
|
321
333
|
);
|
|
322
|
-
const
|
|
334
|
+
const seedAll = useMemo(
|
|
323
335
|
() => (seedFromSchema(inSchema) as Record<string, unknown>) ?? {},
|
|
336
|
+
[inSchema],
|
|
324
337
|
);
|
|
338
|
+
const seedQuery = useMemo(() => pickSeedFields(seedAll, split.query), [seedAll, split.query]);
|
|
339
|
+
const seedBody = useMemo(() => pickSeedFields(seedAll, split.body), [seedAll, split.body]);
|
|
340
|
+
const [query, setQuery] = useState<Record<string, unknown>>(seedQuery);
|
|
341
|
+
const [body, setBody] = useState<Record<string, unknown>>(seedBody);
|
|
325
342
|
const [bodyView, setBodyView] = useState<BodyView>("fields");
|
|
326
|
-
const [rawText, setRawText] = useState(() =>
|
|
327
|
-
stringifyBody((seedFromSchema(inSchema) as Record<string, unknown>) ?? {}),
|
|
328
|
-
);
|
|
343
|
+
const [rawText, setRawText] = useState(() => stringifyBody(seedBody));
|
|
329
344
|
const [rawError, setRawError] = useState<string | null>(null);
|
|
330
345
|
const [localErrors, setLocalErrors] = useState<readonly { path: string; message: string }[]>([]);
|
|
331
346
|
const [startedAt, setStartedAt] = useState<number | null>(null);
|
|
332
347
|
const [elapsedMs, setElapsedMs] = useState<number | null>(null);
|
|
333
348
|
|
|
334
|
-
const seedBody = useMemo(
|
|
335
|
-
() => (seedFromSchema(inSchema) as Record<string, unknown>) ?? {},
|
|
336
|
-
[inSchema],
|
|
337
|
-
);
|
|
338
|
-
|
|
339
349
|
useEffect(() => {
|
|
340
|
-
|
|
341
|
-
setBody(
|
|
342
|
-
setRawText(stringifyBody(
|
|
350
|
+
setQuery(seedQuery);
|
|
351
|
+
setBody(seedBody);
|
|
352
|
+
setRawText(stringifyBody(seedBody));
|
|
343
353
|
setRawError(null);
|
|
344
354
|
setBodyView("fields");
|
|
345
355
|
setPathValues(seedPathValues(row.path, params));
|
|
@@ -388,8 +398,8 @@ function InvokeBodyPanel({
|
|
|
388
398
|
}
|
|
389
399
|
|
|
390
400
|
async function onCall(includePii = !piiMasked): Promise<void> {
|
|
391
|
-
let nextBody = body;
|
|
392
|
-
if (bodyView === "raw") {
|
|
401
|
+
let nextBody = showBody ? body : {};
|
|
402
|
+
if (showBody && bodyView === "raw") {
|
|
393
403
|
if (!commitRawText(rawText)) return;
|
|
394
404
|
try {
|
|
395
405
|
nextBody = JSON.parse(rawText) as Record<string, unknown>;
|
|
@@ -397,7 +407,8 @@ function InvokeBodyPanel({
|
|
|
397
407
|
return;
|
|
398
408
|
}
|
|
399
409
|
}
|
|
400
|
-
const
|
|
410
|
+
const invokeBody = { ...query, ...nextBody };
|
|
411
|
+
const local = validateContract(inSchema, { ...pathValues, ...invokeBody });
|
|
401
412
|
if (!local.ok) {
|
|
402
413
|
setLocalErrors(local.errors);
|
|
403
414
|
return;
|
|
@@ -408,7 +419,7 @@ function InvokeBodyPanel({
|
|
|
408
419
|
try {
|
|
409
420
|
await invoke.mutateAsync({
|
|
410
421
|
flowId: row.id,
|
|
411
|
-
body:
|
|
422
|
+
body: invokeBody,
|
|
412
423
|
...(invokeAs.asUserId ? { asUserId: invokeAs.asUserId } : {}),
|
|
413
424
|
...(invokeAs.asGate ? { asGate: invokeAs.asGate } : {}),
|
|
414
425
|
...(params.length > 0 ? { pathParams: pathValues } : {}),
|
|
@@ -420,6 +431,7 @@ function InvokeBodyPanel({
|
|
|
420
431
|
}
|
|
421
432
|
|
|
422
433
|
function onReset(): void {
|
|
434
|
+
setQuery(seedQuery);
|
|
423
435
|
setBody(seedBody);
|
|
424
436
|
setRawText(stringifyBody(seedBody));
|
|
425
437
|
setRawError(null);
|
|
@@ -471,8 +483,12 @@ function InvokeBodyPanel({
|
|
|
471
483
|
}
|
|
472
484
|
}}
|
|
473
485
|
/>
|
|
474
|
-
|
|
475
|
-
|
|
486
|
+
{showBody ? (
|
|
487
|
+
<>
|
|
488
|
+
<DockSep />
|
|
489
|
+
<BodyViewToggle view={bodyView} onChange={onBodyViewChange} />
|
|
490
|
+
</>
|
|
491
|
+
) : null}
|
|
476
492
|
<DockSep />
|
|
477
493
|
<Tooltip>
|
|
478
494
|
<TooltipTrigger
|
|
@@ -492,7 +508,7 @@ function InvokeBodyPanel({
|
|
|
492
508
|
)}
|
|
493
509
|
/>
|
|
494
510
|
<TooltipContent side="bottom" className="text-[11px]">
|
|
495
|
-
Reset body to the schema example
|
|
511
|
+
{showBody ? "Reset body to the schema example" : "Reset to schema examples"}
|
|
496
512
|
</TooltipContent>
|
|
497
513
|
</Tooltip>
|
|
498
514
|
<DockSep />
|
|
@@ -566,7 +582,7 @@ function InvokeBodyPanel({
|
|
|
566
582
|
) : null}
|
|
567
583
|
|
|
568
584
|
{params.length > 0 ? (
|
|
569
|
-
<section className="min-w-0" aria-label="Path params">
|
|
585
|
+
<section className="min-w-0" aria-label="Path params" data-slot="call-api-path">
|
|
570
586
|
<DockChapter title="Path params" hint={row.path ?? undefined} />
|
|
571
587
|
{params.map((name) => (
|
|
572
588
|
<SheetField key={name} label={name} className="[&>span]:px-3">
|
|
@@ -583,67 +599,89 @@ function InvokeBodyPanel({
|
|
|
583
599
|
</section>
|
|
584
600
|
) : null}
|
|
585
601
|
|
|
586
|
-
|
|
587
|
-
<
|
|
588
|
-
title="
|
|
589
|
-
hint={bodyView === "raw" ? "raw JSON" : "seeded from schema"}
|
|
590
|
-
/>
|
|
591
|
-
{bodyView === "raw" ? (
|
|
592
|
-
<div className="border-b border-border/50">
|
|
593
|
-
<textarea
|
|
594
|
-
data-slot="call-api-body-raw"
|
|
595
|
-
aria-label="Raw JSON body"
|
|
596
|
-
spellCheck={false}
|
|
597
|
-
value={rawText}
|
|
598
|
-
onChange={(e) => {
|
|
599
|
-
setRawText(e.target.value);
|
|
600
|
-
if (rawError) setRawError(null);
|
|
601
|
-
}}
|
|
602
|
-
className={cn(
|
|
603
|
-
"min-h-40 w-full resize-y rounded-none border-0 bg-transparent px-3 py-2 font-mono text-xs leading-relaxed text-foreground outline-none",
|
|
604
|
-
"placeholder:text-muted-foreground focus-visible:bg-muted/20",
|
|
605
|
-
rawError && "text-destructive",
|
|
606
|
-
)}
|
|
607
|
-
/>
|
|
608
|
-
{rawError ? (
|
|
609
|
-
<p
|
|
610
|
-
role="alert"
|
|
611
|
-
className="px-3 pb-2 text-xs text-destructive"
|
|
612
|
-
data-slot="call-api-raw-error"
|
|
613
|
-
>
|
|
614
|
-
{rawError}
|
|
615
|
-
</p>
|
|
616
|
-
) : null}
|
|
617
|
-
</div>
|
|
618
|
-
) : fields.length === 0 ? (
|
|
619
|
-
<p className="border-b border-border/50 px-3 py-2 text-xs text-muted-foreground">
|
|
620
|
-
No body fields (empty object).
|
|
621
|
-
</p>
|
|
622
|
-
) : (
|
|
602
|
+
{split.query.length > 0 ? (
|
|
603
|
+
<section className="min-w-0" aria-label="Query params" data-slot="call-api-query">
|
|
604
|
+
<DockChapter title="Query params" hint="seeded from schema" />
|
|
623
605
|
<SheetGrid>
|
|
624
|
-
{
|
|
606
|
+
{split.query.map((f) => (
|
|
625
607
|
<BodyField
|
|
626
608
|
key={f.path}
|
|
627
609
|
field={f}
|
|
628
|
-
|
|
629
|
-
|
|
610
|
+
idPrefix="query"
|
|
611
|
+
value={query[f.name]}
|
|
612
|
+
onChange={(v) => setQuery((prev) => ({ ...prev, [f.name]: v }))}
|
|
630
613
|
error={localErrors.find((e) => e.path === `/${f.name}` || e.path === f.path)}
|
|
631
614
|
wide={f.type === "object" || f.type === "array"}
|
|
632
615
|
manifest={manifest}
|
|
633
616
|
/>
|
|
634
617
|
))}
|
|
635
618
|
</SheetGrid>
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
619
|
+
</section>
|
|
620
|
+
) : null}
|
|
621
|
+
|
|
622
|
+
{showBody ? (
|
|
623
|
+
<section className="min-w-0" aria-label="Body" data-slot="call-api-body">
|
|
624
|
+
<DockChapter
|
|
625
|
+
title="Body"
|
|
626
|
+
hint={bodyView === "raw" ? "raw JSON" : "seeded from schema"}
|
|
627
|
+
/>
|
|
628
|
+
{bodyView === "raw" ? (
|
|
629
|
+
<div className="border-b border-border/50">
|
|
630
|
+
<textarea
|
|
631
|
+
data-slot="call-api-body-raw"
|
|
632
|
+
aria-label="Raw JSON body"
|
|
633
|
+
spellCheck={false}
|
|
634
|
+
value={rawText}
|
|
635
|
+
onChange={(e) => {
|
|
636
|
+
setRawText(e.target.value);
|
|
637
|
+
if (rawError) setRawError(null);
|
|
638
|
+
}}
|
|
639
|
+
className={cn(
|
|
640
|
+
"min-h-40 w-full resize-y rounded-none border-0 bg-transparent px-3 py-2 font-mono text-xs leading-relaxed text-foreground outline-none",
|
|
641
|
+
"placeholder:text-muted-foreground focus-visible:bg-muted/20",
|
|
642
|
+
rawError && "text-destructive",
|
|
643
|
+
)}
|
|
644
|
+
/>
|
|
645
|
+
{rawError ? (
|
|
646
|
+
<p
|
|
647
|
+
role="alert"
|
|
648
|
+
className="px-3 pb-2 text-xs text-destructive"
|
|
649
|
+
data-slot="call-api-raw-error"
|
|
650
|
+
>
|
|
651
|
+
{rawError}
|
|
652
|
+
</p>
|
|
653
|
+
) : null}
|
|
654
|
+
</div>
|
|
655
|
+
) : split.body.length === 0 ? (
|
|
656
|
+
<p className="border-b border-border/50 px-3 py-2 text-xs text-muted-foreground">
|
|
657
|
+
No body fields (empty object).
|
|
658
|
+
</p>
|
|
659
|
+
) : (
|
|
660
|
+
<SheetGrid>
|
|
661
|
+
{split.body.map((f) => (
|
|
662
|
+
<BodyField
|
|
663
|
+
key={f.path}
|
|
664
|
+
field={f}
|
|
665
|
+
value={body[f.name]}
|
|
666
|
+
onChange={(v) => setBody((prev) => ({ ...prev, [f.name]: v }))}
|
|
667
|
+
error={localErrors.find((e) => e.path === `/${f.name}` || e.path === f.path)}
|
|
668
|
+
wide={f.type === "object" || f.type === "array"}
|
|
669
|
+
manifest={manifest}
|
|
670
|
+
/>
|
|
671
|
+
))}
|
|
672
|
+
</SheetGrid>
|
|
673
|
+
)}
|
|
674
|
+
</section>
|
|
675
|
+
) : null}
|
|
676
|
+
{localErrors.length > 0 ? (
|
|
677
|
+
<p
|
|
678
|
+
role="alert"
|
|
679
|
+
className="px-3 py-2 text-xs text-destructive"
|
|
680
|
+
data-slot="call-api-validation"
|
|
681
|
+
>
|
|
682
|
+
Fix contract errors before sending.
|
|
683
|
+
</p>
|
|
684
|
+
) : null}
|
|
647
685
|
</div>
|
|
648
686
|
|
|
649
687
|
<ResponseBlock
|
|
@@ -1057,6 +1095,7 @@ function BodyField({
|
|
|
1057
1095
|
error,
|
|
1058
1096
|
wide = false,
|
|
1059
1097
|
manifest,
|
|
1098
|
+
idPrefix = "body",
|
|
1060
1099
|
}: {
|
|
1061
1100
|
readonly field: FormField;
|
|
1062
1101
|
readonly value: unknown;
|
|
@@ -1064,8 +1103,9 @@ function BodyField({
|
|
|
1064
1103
|
readonly error?: { path: string; message: string };
|
|
1065
1104
|
readonly wide?: boolean;
|
|
1066
1105
|
readonly manifest: Manifest | null;
|
|
1106
|
+
readonly idPrefix?: string;
|
|
1067
1107
|
}): JSX.Element {
|
|
1068
|
-
const id =
|
|
1108
|
+
const id = `${idPrefix}-${field.name}`;
|
|
1069
1109
|
const fk = useFkFieldOptions(field, manifest);
|
|
1070
1110
|
const hint = [
|
|
1071
1111
|
!field.required ? "optional" : null,
|