station-kit 1.0.8 → 1.0.9
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/.next/standalone/package.json +3 -1
- package/.next/standalone/packages/station-kit/.next/BUILD_ID +1 -1
- package/.next/standalone/packages/station-kit/.next/app-build-manifest.json +76 -17
- package/.next/standalone/packages/station-kit/.next/app-path-routes-manifest.json +11 -4
- package/.next/standalone/packages/station-kit/.next/build-manifest.json +3 -3
- package/.next/standalone/packages/station-kit/.next/prerender-manifest.json +105 -9
- package/.next/standalone/packages/station-kit/.next/routes-manifest.json +49 -0
- package/.next/standalone/packages/station-kit/.next/server/app/_not-found/page.js +2 -2
- package/.next/standalone/packages/station-kit/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/_not-found.rsc +7 -7
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/[id]/page.js +2 -2
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/v/[n]/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/v/[n]/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/dyn/[name]/v/[n]/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new.html +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new.meta +7 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/new.rsc +25 -0
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/page.js +2 -2
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/broadcasts.rsc +8 -8
- package/.next/standalone/packages/station-kit/.next/server/app/index.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/index.rsc +8 -8
- package/.next/standalone/packages/station-kit/.next/server/app/page.js +2 -2
- package/.next/standalone/packages/station-kit/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/playground/expression/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/playground/expression/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/playground/expression/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/playground/expression.html +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/playground/expression.meta +7 -0
- package/.next/standalone/packages/station-kit/.next/server/app/playground/expression.rsc +25 -0
- package/.next/standalone/packages/station-kit/.next/server/app/runs/[id]/page.js +2 -2
- package/.next/standalone/packages/station-kit/.next/server/app/runs/[id]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/[id]/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/[id]/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/[id]/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/new/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/new/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/new/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/new.html +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/new.meta +7 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/new.rsc +25 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/page.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/page.js.nft.json +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules/page_client-reference-manifest.js +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules.html +1 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules.meta +7 -0
- package/.next/standalone/packages/station-kit/.next/server/app/schedules.rsc +25 -0
- package/.next/standalone/packages/station-kit/.next/server/app/settings/page.js +2 -2
- package/.next/standalone/packages/station-kit/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/settings.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/settings.rsc +8 -8
- package/.next/standalone/packages/station-kit/.next/server/app/signals/[name]/page.js +2 -2
- package/.next/standalone/packages/station-kit/.next/server/app/signals/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/signals/page.js +2 -2
- package/.next/standalone/packages/station-kit/.next/server/app/signals/page_client-reference-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/signals.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/app/signals.rsc +8 -8
- package/.next/standalone/packages/station-kit/.next/server/app-paths-manifest.json +11 -4
- package/.next/standalone/packages/station-kit/.next/server/chunks/102.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/chunks/535.js +2 -0
- package/.next/standalone/packages/station-kit/.next/server/chunks/606.js +14 -14
- package/.next/standalone/packages/station-kit/.next/server/chunks/783.js +3 -3
- package/.next/standalone/packages/station-kit/.next/server/middleware-build-manifest.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/pages/404.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/pages/500.html +1 -1
- package/.next/standalone/packages/station-kit/.next/server/pages/_app.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/pages/_document.js +1 -1
- package/.next/standalone/packages/station-kit/.next/server/pages/_error.js +9 -9
- package/.next/standalone/packages/station-kit/.next/server/pages-manifest.json +1 -1
- package/.next/standalone/packages/station-kit/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/145-9e370afd2e5aba39.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/285-ff198f0a909c4fdd.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/561-33d912169940283e.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/935-dff12960528de017.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/_not-found/{page-ce21b4ba9038a5a7.js → page-67ef312aee40cfeb.js} +1 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/[id]/page-fe2f5467a0c68fef.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/dyn/[name]/page-0d2505242014f51e.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/dyn/[name]/v/[n]/page-5eac0507f49a00ec.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/new/page-3d02707043d24dc7.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/page-dee500ccc01f0821.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/layout-e14e14f3e5b0b8a9.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/page-aac41ef7a470daab.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/playground/expression/page-dc9d91f3f50f4716.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/runs/[id]/page-9e4c4f751a4bea72.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/schedules/[id]/page-435f67be180b8e4f.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/schedules/new/page-f697c289c813496a.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/schedules/page-738d98dc0b63166e.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/settings/page-fc5654b31f57ac21.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/[name]/page-4b1c09a539a1ebcd.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/page-d2f2403dfede87cc.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/pages/_app-a3774a320f58a018.js +1 -0
- package/.next/standalone/packages/station-kit/.next/static/demLiQWDy62JuUkBw-ILG/_buildManifest.js +1 -0
- package/.next/standalone/packages/station-kit/package.json +5 -1
- package/dist/config/schema.d.ts +13 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +1 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/server/auth/keys.d.ts +56 -8
- package/dist/server/auth/keys.d.ts.map +1 -1
- package/dist/server/auth/keys.js +155 -53
- package/dist/server/auth/keys.js.map +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +53 -6
- package/dist/server/index.js.map +1 -1
- package/dist/server/middleware/auth.js +1 -1
- package/dist/server/middleware/auth.js.map +1 -1
- package/dist/server/routes/v1/definitions.d.ts +21 -0
- package/dist/server/routes/v1/definitions.d.ts.map +1 -0
- package/dist/server/routes/v1/definitions.js +139 -0
- package/dist/server/routes/v1/definitions.js.map +1 -0
- package/dist/server/routes/v1/expressions.d.ts +3 -0
- package/dist/server/routes/v1/expressions.d.ts.map +1 -0
- package/dist/server/routes/v1/expressions.js +56 -0
- package/dist/server/routes/v1/expressions.js.map +1 -0
- package/dist/server/routes/v1/keys.js +3 -3
- package/dist/server/routes/v1/keys.js.map +1 -1
- package/dist/server/routes/v1/schedules.d.ts +10 -0
- package/dist/server/routes/v1/schedules.d.ts.map +1 -0
- package/dist/server/routes/v1/schedules.js +169 -0
- package/dist/server/routes/v1/schedules.js.map +1 -0
- package/dist/server/routes/v1/trigger.d.ts.map +1 -1
- package/dist/server/routes/v1/trigger.js +21 -0
- package/dist/server/routes/v1/trigger.js.map +1 -1
- package/package.json +11 -7
- package/src/app/broadcasts/components/broadcast-builder.tsx +535 -0
- package/src/app/broadcasts/components/dag-editor.tsx +510 -0
- package/src/app/broadcasts/dyn/[name]/dynamic-detail.tsx +243 -0
- package/src/app/broadcasts/dyn/[name]/page.tsx +10 -0
- package/src/app/broadcasts/dyn/[name]/v/[n]/page.tsx +10 -0
- package/src/app/broadcasts/dyn/[name]/v/[n]/version-view.tsx +285 -0
- package/src/app/broadcasts/new/page.tsx +102 -0
- package/src/app/broadcasts/page.tsx +176 -91
- package/src/app/components/api-panel.tsx +151 -0
- package/src/app/components/shell.tsx +23 -0
- package/src/app/hooks/use-api.ts +117 -0
- package/src/app/playground/expression/page.tsx +245 -0
- package/src/app/schedules/[id]/page.tsx +10 -0
- package/src/app/schedules/[id]/schedule-editor.tsx +195 -0
- package/src/app/schedules/components/schedule-form.tsx +140 -0
- package/src/app/schedules/new/page.tsx +166 -0
- package/src/app/schedules/page.tsx +126 -0
- package/src/config/schema.ts +14 -0
- package/src/server/auth/keys.ts +191 -56
- package/src/server/index.ts +72 -8
- package/src/server/middleware/auth.ts +1 -1
- package/src/server/routes/v1/definitions.ts +164 -0
- package/src/server/routes/v1/expressions.ts +76 -0
- package/src/server/routes/v1/keys.ts +3 -3
- package/src/server/routes/v1/schedules.ts +176 -0
- package/src/server/routes/v1/trigger.ts +27 -0
- package/.next/standalone/packages/station-kit/.next/static/chunks/580-f007f4d4c050db4e.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/[id]/page-a0a20cccda13a0e9.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/broadcasts/page-937eb876f9087bc9.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/layout-68cd71116ba65cd8.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/page-70b0c0958c03459a.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/runs/[id]/page-01f8040619fe56c5.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/settings/page-beac11049f90da31.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/[name]/page-931e6a38a4a53d25.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/app/signals/page-6a123a355d93fec5.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/chunks/pages/_app-0a7b2e66ecbe3f0a.js +0 -1
- package/.next/standalone/packages/station-kit/.next/static/xYd6dn0Ox68DaamIrH_pB/_buildManifest.js +0 -1
- /package/.next/standalone/packages/station-kit/.next/static/{xYd6dn0Ox68DaamIrH_pB → demLiQWDy62JuUkBw-ILG}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useApi } from "../../hooks/use-api";
|
|
5
|
+
import { useBreadcrumb } from "../../hooks/use-breadcrumb";
|
|
6
|
+
import { ApiPanel } from "../../components/api-panel";
|
|
7
|
+
|
|
8
|
+
const STARTER_SOURCE = `input.amount > 100 && input.user.tier == "premium"`;
|
|
9
|
+
const STARTER_CONTEXT = JSON.stringify(
|
|
10
|
+
{
|
|
11
|
+
input: {
|
|
12
|
+
amount: 250,
|
|
13
|
+
user: { tier: "premium" },
|
|
14
|
+
},
|
|
15
|
+
upstream: {},
|
|
16
|
+
},
|
|
17
|
+
null,
|
|
18
|
+
2,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export default function ExpressionPlaygroundPage() {
|
|
22
|
+
const api = useApi();
|
|
23
|
+
const [source, setSource] = useState(STARTER_SOURCE);
|
|
24
|
+
const [contextJson, setContextJson] = useState(STARTER_CONTEXT);
|
|
25
|
+
const [astJson, setAstJson] = useState<string>("");
|
|
26
|
+
const [parseError, setParseError] = useState<string | null>(null);
|
|
27
|
+
const [evalResult, setEvalResult] = useState<unknown>(undefined);
|
|
28
|
+
const [evalError, setEvalError] = useState<string | null>(null);
|
|
29
|
+
const [parseRunning, setParseRunning] = useState(false);
|
|
30
|
+
const [evalRunning, setEvalRunning] = useState(false);
|
|
31
|
+
|
|
32
|
+
useBreadcrumb(
|
|
33
|
+
[
|
|
34
|
+
{ label: "Tools" },
|
|
35
|
+
{ label: "Expression playground" },
|
|
36
|
+
],
|
|
37
|
+
"playground",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
async function handleParse() {
|
|
41
|
+
setParseError(null);
|
|
42
|
+
setParseRunning(true);
|
|
43
|
+
try {
|
|
44
|
+
const res = await api.parseExpression(source);
|
|
45
|
+
setAstJson(JSON.stringify(res.data.node, null, 2));
|
|
46
|
+
} catch (err) {
|
|
47
|
+
setParseError(err instanceof Error ? err.message : String(err));
|
|
48
|
+
setAstJson("");
|
|
49
|
+
} finally {
|
|
50
|
+
setParseRunning(false);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function handleEvaluate() {
|
|
55
|
+
setEvalError(null);
|
|
56
|
+
setEvalResult(undefined);
|
|
57
|
+
setEvalRunning(true);
|
|
58
|
+
try {
|
|
59
|
+
// First parse if AST is empty
|
|
60
|
+
let node: unknown;
|
|
61
|
+
if (astJson.trim()) {
|
|
62
|
+
node = JSON.parse(astJson);
|
|
63
|
+
} else {
|
|
64
|
+
const parsed = await api.parseExpression(source);
|
|
65
|
+
node = parsed.data.node;
|
|
66
|
+
setAstJson(JSON.stringify(node, null, 2));
|
|
67
|
+
}
|
|
68
|
+
const ctx = JSON.parse(contextJson);
|
|
69
|
+
const res = await api.evaluateExpression(node, ctx);
|
|
70
|
+
setEvalResult(res.data.value);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
setEvalError(err instanceof Error ? err.message : String(err));
|
|
73
|
+
} finally {
|
|
74
|
+
setEvalRunning(false);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div>
|
|
80
|
+
<h1 className="page-title">Expression playground</h1>
|
|
81
|
+
<p style={{ color: "var(--muted)", marginBottom: "1.5rem", fontSize: "0.875rem" }}>
|
|
82
|
+
Pure, deterministic expressions used in dynamic broadcast `input` mappings and `when` guards.
|
|
83
|
+
References resolve against `input.*` (broadcast trigger input) and `upstream.nodeName.*` (upstream outputs).
|
|
84
|
+
</p>
|
|
85
|
+
|
|
86
|
+
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}>
|
|
87
|
+
<section>
|
|
88
|
+
<Label>Expression source</Label>
|
|
89
|
+
<textarea
|
|
90
|
+
className="mono"
|
|
91
|
+
value={source}
|
|
92
|
+
onChange={(e) => setSource(e.target.value)}
|
|
93
|
+
spellCheck={false}
|
|
94
|
+
rows={4}
|
|
95
|
+
style={editorStyle}
|
|
96
|
+
/>
|
|
97
|
+
<div style={{ marginTop: "0.5rem", display: "flex", gap: "0.5rem" }}>
|
|
98
|
+
<button className="btn" onClick={handleParse} disabled={parseRunning}>
|
|
99
|
+
{parseRunning ? "Parsing..." : "Parse → AST"}
|
|
100
|
+
</button>
|
|
101
|
+
<button className="btn btn--primary" onClick={handleEvaluate} disabled={evalRunning}>
|
|
102
|
+
{evalRunning ? "Evaluating..." : "Evaluate"}
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
{parseError && (
|
|
106
|
+
<div style={errorStyle}>
|
|
107
|
+
{parseError}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</section>
|
|
111
|
+
|
|
112
|
+
<section>
|
|
113
|
+
<Label>Context (input + upstream)</Label>
|
|
114
|
+
<textarea
|
|
115
|
+
className="mono"
|
|
116
|
+
value={contextJson}
|
|
117
|
+
onChange={(e) => setContextJson(e.target.value)}
|
|
118
|
+
spellCheck={false}
|
|
119
|
+
rows={10}
|
|
120
|
+
style={{ ...editorStyle, minHeight: "180px" }}
|
|
121
|
+
/>
|
|
122
|
+
</section>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<section style={{ marginTop: "1.5rem" }}>
|
|
126
|
+
<Label>Parsed AST</Label>
|
|
127
|
+
<textarea
|
|
128
|
+
className="mono"
|
|
129
|
+
value={astJson}
|
|
130
|
+
onChange={(e) => setAstJson(e.target.value)}
|
|
131
|
+
spellCheck={false}
|
|
132
|
+
rows={10}
|
|
133
|
+
placeholder="// Click 'Parse' or paste an ExprNode JSON manually."
|
|
134
|
+
style={{ ...editorStyle, minHeight: "180px" }}
|
|
135
|
+
/>
|
|
136
|
+
</section>
|
|
137
|
+
|
|
138
|
+
<section style={{ marginTop: "1.5rem" }}>
|
|
139
|
+
<Label>Result</Label>
|
|
140
|
+
{evalError ? (
|
|
141
|
+
<div style={errorStyle}>{evalError}</div>
|
|
142
|
+
) : evalResult === undefined ? (
|
|
143
|
+
<div style={{ ...resultStyle, color: "var(--muted)" }}>—</div>
|
|
144
|
+
) : (
|
|
145
|
+
<pre className="mono" style={resultStyle}>{JSON.stringify(evalResult, null, 2)}</pre>
|
|
146
|
+
)}
|
|
147
|
+
</section>
|
|
148
|
+
|
|
149
|
+
<section style={{ marginTop: "1.5rem" }}>
|
|
150
|
+
<Label>Reference</Label>
|
|
151
|
+
<div style={{ ...resultStyle, color: "var(--muted)" }}>
|
|
152
|
+
<p style={{ margin: "0 0 0.5rem 0", fontSize: "0.8125rem", lineHeight: 1.6 }}>
|
|
153
|
+
<strong>Operators:</strong> <span className="mono">==</span>, <span className="mono">!=</span>, <span className="mono"><</span>, <span className="mono">></span>, <span className="mono"><=</span>, <span className="mono">>=</span>, <span className="mono">&&</span>, <span className="mono">||</span>, <span className="mono">!</span>, <span className="mono">+</span>, <span className="mono">-</span>, <span className="mono">*</span>, <span className="mono">/</span>
|
|
154
|
+
</p>
|
|
155
|
+
<p style={{ margin: "0 0 0.5rem 0", fontSize: "0.8125rem", lineHeight: 1.6 }}>
|
|
156
|
+
<strong>Refs:</strong> <span className="mono">input.x</span>, <span className="mono">upstream.nodeName.x</span>, <span className="mono">nodeName.x</span> (shorthand)
|
|
157
|
+
</p>
|
|
158
|
+
<p style={{ margin: "0 0 0.5rem 0", fontSize: "0.8125rem", lineHeight: 1.6 }}>
|
|
159
|
+
<strong>Literals:</strong> numbers, <span className="mono">"strings"</span>, <span className="mono">true</span> / <span className="mono">false</span> / <span className="mono">null</span>
|
|
160
|
+
</p>
|
|
161
|
+
<p style={{ margin: "0.75rem 0 0 0", fontSize: "0.8125rem", lineHeight: 1.6, paddingTop: "0.5rem", borderTop: "1px dashed var(--border)" }}>
|
|
162
|
+
<strong>Escape hatch:</strong> the language is intentionally minimal — no loops, no user-defined functions, no I/O. If you can't express something here, write a code-defined signal that does the logic in TypeScript and reference it from the broadcast graph. The signal is the unit of arbitrary code; the expression layer is for connecting them.
|
|
163
|
+
</p>
|
|
164
|
+
</div>
|
|
165
|
+
</section>
|
|
166
|
+
|
|
167
|
+
<ApiPanel
|
|
168
|
+
title="Use these endpoints from your code"
|
|
169
|
+
snippets={[
|
|
170
|
+
{
|
|
171
|
+
label: "Parse string → AST",
|
|
172
|
+
method: "POST",
|
|
173
|
+
path: "/api/v1/expressions/parse",
|
|
174
|
+
body: { source: "input.amount > 100" },
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
label: "Evaluate AST against context",
|
|
178
|
+
method: "POST",
|
|
179
|
+
path: "/api/v1/expressions/evaluate",
|
|
180
|
+
body: { node: { kind: "ref", path: ["input", "x"] }, context: { input: { x: 42 } } },
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
label: "Validate AST against schemas",
|
|
184
|
+
method: "POST",
|
|
185
|
+
path: "/api/v1/expressions/validate",
|
|
186
|
+
body: {
|
|
187
|
+
node: { kind: "ref", path: ["input", "x"] },
|
|
188
|
+
schemaContext: {
|
|
189
|
+
inputSchema: { type: "object", properties: { x: { type: "number" } } },
|
|
190
|
+
upstreamSchemas: {},
|
|
191
|
+
expectedSchema: { type: "number" },
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
]}
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function Label({ children }: { children: React.ReactNode }) {
|
|
202
|
+
return (
|
|
203
|
+
<label className="mono" style={{
|
|
204
|
+
display: "block",
|
|
205
|
+
fontSize: "0.75rem",
|
|
206
|
+
color: "var(--muted)",
|
|
207
|
+
textTransform: "uppercase",
|
|
208
|
+
letterSpacing: "0.05em",
|
|
209
|
+
marginBottom: "0.25rem",
|
|
210
|
+
}}>{children}</label>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const editorStyle: React.CSSProperties = {
|
|
215
|
+
width: "100%",
|
|
216
|
+
padding: "0.625rem 0.75rem",
|
|
217
|
+
border: "1px solid var(--border)",
|
|
218
|
+
borderRadius: "4px",
|
|
219
|
+
background: "var(--surface)",
|
|
220
|
+
color: "var(--text)",
|
|
221
|
+
fontSize: "0.8125rem",
|
|
222
|
+
lineHeight: 1.5,
|
|
223
|
+
fontFamily: "var(--mono-font, monospace)",
|
|
224
|
+
resize: "vertical",
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const errorStyle: React.CSSProperties = {
|
|
228
|
+
marginTop: "0.75rem",
|
|
229
|
+
padding: "0.625rem 0.75rem",
|
|
230
|
+
background: "var(--error-bg, #fee)",
|
|
231
|
+
color: "var(--error, #b00)",
|
|
232
|
+
border: "1px solid var(--error, #b00)",
|
|
233
|
+
borderRadius: "4px",
|
|
234
|
+
fontSize: "0.8125rem",
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const resultStyle: React.CSSProperties = {
|
|
238
|
+
padding: "0.625rem 0.75rem",
|
|
239
|
+
background: "var(--surface)",
|
|
240
|
+
border: "1px solid var(--border)",
|
|
241
|
+
borderRadius: "4px",
|
|
242
|
+
fontSize: "0.8125rem",
|
|
243
|
+
margin: 0,
|
|
244
|
+
whiteSpace: "pre-wrap",
|
|
245
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { useApi, type Schedule } from "../../hooks/use-api";
|
|
6
|
+
import { useBreadcrumb } from "../../hooks/use-breadcrumb";
|
|
7
|
+
import { ScheduleForm, type ScheduleFormValue } from "../components/schedule-form";
|
|
8
|
+
import { ApiPanel } from "../../components/api-panel";
|
|
9
|
+
|
|
10
|
+
export function ScheduleEditor({ id }: { id: string }) {
|
|
11
|
+
const api = useApi();
|
|
12
|
+
const router = useRouter();
|
|
13
|
+
const [schedule, setSchedule] = useState<Schedule | null>(null);
|
|
14
|
+
const [value, setValue] = useState<ScheduleFormValue | null>(null);
|
|
15
|
+
const [previewFires, setPreviewFires] = useState<string[]>([]);
|
|
16
|
+
const [loading, setLoading] = useState(true);
|
|
17
|
+
const [busy, setBusy] = useState(false);
|
|
18
|
+
const [error, setError] = useState<string | null>(null);
|
|
19
|
+
const [savedAt, setSavedAt] = useState<number | null>(null);
|
|
20
|
+
|
|
21
|
+
useBreadcrumb(
|
|
22
|
+
[
|
|
23
|
+
{ label: "Schedules", href: "/schedules" },
|
|
24
|
+
{ label: schedule?.target ?? id },
|
|
25
|
+
],
|
|
26
|
+
"schedules",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
api.getSchedule(id)
|
|
31
|
+
.then((res) => {
|
|
32
|
+
setSchedule(res.data);
|
|
33
|
+
setValue({
|
|
34
|
+
kind: res.data.kind,
|
|
35
|
+
target: res.data.target,
|
|
36
|
+
interval: res.data.interval,
|
|
37
|
+
input: res.data.input !== undefined ? JSON.stringify(res.data.input, null, 2) : "{}",
|
|
38
|
+
enabled: res.data.enabled,
|
|
39
|
+
});
|
|
40
|
+
})
|
|
41
|
+
.catch((err) => setError(err instanceof Error ? err.message : String(err)))
|
|
42
|
+
.finally(() => setLoading(false));
|
|
43
|
+
|
|
44
|
+
api.previewSchedule(id, 5).then((res) => setPreviewFires(res.data.fires)).catch(() => {});
|
|
45
|
+
}, [id]);
|
|
46
|
+
|
|
47
|
+
async function handleSave() {
|
|
48
|
+
if (!value) return;
|
|
49
|
+
setError(null);
|
|
50
|
+
let inputParsed: unknown = undefined;
|
|
51
|
+
if (value.input.trim()) {
|
|
52
|
+
try {
|
|
53
|
+
inputParsed = JSON.parse(value.input);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
setError(`Input JSON parse error: ${err instanceof Error ? err.message : String(err)}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
setBusy(true);
|
|
60
|
+
try {
|
|
61
|
+
const res = await api.updateSchedule(id, {
|
|
62
|
+
interval: value.interval,
|
|
63
|
+
input: inputParsed,
|
|
64
|
+
enabled: value.enabled,
|
|
65
|
+
});
|
|
66
|
+
setSchedule(res.data);
|
|
67
|
+
setSavedAt(Date.now());
|
|
68
|
+
const preview = await api.previewSchedule(id, 5);
|
|
69
|
+
setPreviewFires(preview.data.fires);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
72
|
+
} finally {
|
|
73
|
+
setBusy(false);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function handleDelete() {
|
|
78
|
+
if (!confirm("Delete this schedule?")) return;
|
|
79
|
+
try {
|
|
80
|
+
await api.deleteSchedule(id);
|
|
81
|
+
router.push("/schedules");
|
|
82
|
+
} catch (err) {
|
|
83
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (loading) {
|
|
88
|
+
return (
|
|
89
|
+
<div>
|
|
90
|
+
<h1 className="page-title">Schedule</h1>
|
|
91
|
+
<div className="loading-bar"><div className="loading-bar-fill" /></div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!schedule || !value) {
|
|
97
|
+
return (
|
|
98
|
+
<div>
|
|
99
|
+
<h1 className="page-title">Schedule</h1>
|
|
100
|
+
<div className="empty-state">
|
|
101
|
+
<p className="empty-state-text">{error ?? "Not found."}</p>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div>
|
|
109
|
+
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "1rem" }}>
|
|
110
|
+
<h1 className="page-title" style={{ margin: 0 }}>
|
|
111
|
+
<span className="mono">{schedule.target}</span>
|
|
112
|
+
</h1>
|
|
113
|
+
<button className="btn btn--danger" onClick={handleDelete}>Delete</button>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div style={{ display: "grid", gridTemplateColumns: "minmax(0, 2fr) minmax(0, 1fr)", gap: "2rem" }}>
|
|
117
|
+
<div>
|
|
118
|
+
<ScheduleForm value={value} onChange={setValue} locked />
|
|
119
|
+
|
|
120
|
+
{error && (
|
|
121
|
+
<div style={{
|
|
122
|
+
marginTop: "1rem",
|
|
123
|
+
padding: "0.625rem 0.75rem",
|
|
124
|
+
background: "var(--error-bg, #fee)",
|
|
125
|
+
color: "var(--error, #b00)",
|
|
126
|
+
border: "1px solid var(--error, #b00)",
|
|
127
|
+
borderRadius: "4px",
|
|
128
|
+
fontSize: "0.8125rem",
|
|
129
|
+
maxWidth: "640px",
|
|
130
|
+
}}>{error}</div>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
<div style={{ marginTop: "1.5rem", display: "flex", gap: "0.5rem", alignItems: "center" }}>
|
|
134
|
+
<button className="btn btn--primary" onClick={handleSave} disabled={busy}>
|
|
135
|
+
{busy ? "Saving..." : "Save"}
|
|
136
|
+
</button>
|
|
137
|
+
{savedAt && Date.now() - savedAt < 3000 && (
|
|
138
|
+
<span className="mono" style={{ fontSize: "0.75rem", color: "var(--success, #060)" }}>Saved</span>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<aside>
|
|
144
|
+
<h3 className="mono" style={{ fontSize: "0.75rem", color: "var(--muted)", textTransform: "uppercase", letterSpacing: "0.05em", marginBottom: "0.5rem" }}>
|
|
145
|
+
Next fire times
|
|
146
|
+
</h3>
|
|
147
|
+
<ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
|
|
148
|
+
{previewFires.map((iso, i) => (
|
|
149
|
+
<li key={i} className="mono" style={{
|
|
150
|
+
fontSize: "0.8125rem",
|
|
151
|
+
padding: "0.375rem 0",
|
|
152
|
+
borderBottom: "1px dashed var(--border)",
|
|
153
|
+
color: i === 0 ? "var(--text)" : "var(--muted)",
|
|
154
|
+
}}>
|
|
155
|
+
{new Date(iso).toLocaleString()}
|
|
156
|
+
</li>
|
|
157
|
+
))}
|
|
158
|
+
</ul>
|
|
159
|
+
|
|
160
|
+
<h3 className="mono" style={{ fontSize: "0.75rem", color: "var(--muted)", textTransform: "uppercase", letterSpacing: "0.05em", marginTop: "1.5rem", marginBottom: "0.5rem" }}>
|
|
161
|
+
Last run
|
|
162
|
+
</h3>
|
|
163
|
+
<div className="mono" style={{ fontSize: "0.8125rem", color: "var(--muted)" }}>
|
|
164
|
+
{schedule.lastRunAt ? (
|
|
165
|
+
<>
|
|
166
|
+
<div>{new Date(schedule.lastRunAt).toLocaleString()}</div>
|
|
167
|
+
{schedule.lastRunStatus && <div>status: {schedule.lastRunStatus}</div>}
|
|
168
|
+
{schedule.lastRunId && <div>id: {schedule.lastRunId}</div>}
|
|
169
|
+
</>
|
|
170
|
+
) : "Never run."}
|
|
171
|
+
</div>
|
|
172
|
+
</aside>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<ApiPanel
|
|
176
|
+
title="Manage this schedule"
|
|
177
|
+
snippets={[
|
|
178
|
+
{ label: "Get", method: "GET", path: `/api/v1/schedules/${id}` },
|
|
179
|
+
{
|
|
180
|
+
label: "Update",
|
|
181
|
+
method: "PATCH",
|
|
182
|
+
path: `/api/v1/schedules/${id}`,
|
|
183
|
+
body: { interval: value.interval, enabled: value.enabled, input: tryParse(value.input) },
|
|
184
|
+
},
|
|
185
|
+
{ label: "Preview next 5 fires", method: "POST", path: `/api/v1/schedules/${id}/preview`, body: { count: 5 } },
|
|
186
|
+
{ label: "Delete", method: "DELETE", path: `/api/v1/schedules/${id}` },
|
|
187
|
+
]}
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function tryParse(s: string): unknown {
|
|
194
|
+
try { return JSON.parse(s); } catch { return s; }
|
|
195
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useApi, type ScheduleKind } from "../../hooks/use-api";
|
|
5
|
+
|
|
6
|
+
export interface ScheduleFormValue {
|
|
7
|
+
kind: ScheduleKind;
|
|
8
|
+
target: string;
|
|
9
|
+
interval: string;
|
|
10
|
+
input: string;
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ScheduleFormProps {
|
|
15
|
+
value: ScheduleFormValue;
|
|
16
|
+
onChange: (next: ScheduleFormValue) => void;
|
|
17
|
+
/** When true, kind/target are locked (edit mode). */
|
|
18
|
+
locked?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ScheduleForm({ value, onChange, locked }: ScheduleFormProps) {
|
|
22
|
+
const api = useApi();
|
|
23
|
+
const [signalNames, setSignalNames] = useState<string[]>([]);
|
|
24
|
+
const [staticBroadcastNames, setStaticBroadcastNames] = useState<string[]>([]);
|
|
25
|
+
const [dynamicBroadcastNames, setDynamicBroadcastNames] = useState<string[]>([]);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
Promise.all([
|
|
29
|
+
api.getSignals().catch(() => ({ data: [] })),
|
|
30
|
+
api.getBroadcasts().catch(() => ({ data: [] })),
|
|
31
|
+
api.getBroadcastDefinitions().catch(() => ({ data: [] })),
|
|
32
|
+
]).then(([sigs, stat, dyn]) => {
|
|
33
|
+
setSignalNames(sigs.data.map((s) => s.name));
|
|
34
|
+
setStaticBroadcastNames(stat.data.map((b) => b.name));
|
|
35
|
+
setDynamicBroadcastNames(dyn.data.map((d) => d.name));
|
|
36
|
+
});
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
const targetOptions =
|
|
40
|
+
value.kind === "signal"
|
|
41
|
+
? signalNames
|
|
42
|
+
: value.kind === "broadcast-static"
|
|
43
|
+
? staticBroadcastNames
|
|
44
|
+
: dynamicBroadcastNames;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div style={{ display: "grid", gap: "1rem", maxWidth: "640px" }}>
|
|
48
|
+
<Field label="Kind">
|
|
49
|
+
<select
|
|
50
|
+
value={value.kind}
|
|
51
|
+
disabled={locked}
|
|
52
|
+
onChange={(e) => onChange({ ...value, kind: e.target.value as ScheduleKind, target: "" })}
|
|
53
|
+
className="mono"
|
|
54
|
+
style={selectStyle}
|
|
55
|
+
>
|
|
56
|
+
<option value="signal">signal</option>
|
|
57
|
+
<option value="broadcast-static">broadcast-static</option>
|
|
58
|
+
<option value="broadcast-dynamic">broadcast-dynamic</option>
|
|
59
|
+
</select>
|
|
60
|
+
</Field>
|
|
61
|
+
|
|
62
|
+
<Field label="Target" hint={`${targetOptions.length} available`}>
|
|
63
|
+
{locked ? (
|
|
64
|
+
<input className="mono" value={value.target} readOnly style={inputStyle} />
|
|
65
|
+
) : (
|
|
66
|
+
<input
|
|
67
|
+
className="mono"
|
|
68
|
+
value={value.target}
|
|
69
|
+
list={`schedule-targets-${value.kind}`}
|
|
70
|
+
onChange={(e) => onChange({ ...value, target: e.target.value })}
|
|
71
|
+
placeholder="signal or broadcast name"
|
|
72
|
+
style={inputStyle}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
<datalist id={`schedule-targets-${value.kind}`}>
|
|
76
|
+
{targetOptions.map((name) => (
|
|
77
|
+
<option key={name} value={name} />
|
|
78
|
+
))}
|
|
79
|
+
</datalist>
|
|
80
|
+
</Field>
|
|
81
|
+
|
|
82
|
+
<Field label="Interval" hint='e.g. "30s", "5m", "1h", "1d"'>
|
|
83
|
+
<input
|
|
84
|
+
className="mono"
|
|
85
|
+
value={value.interval}
|
|
86
|
+
onChange={(e) => onChange({ ...value, interval: e.target.value })}
|
|
87
|
+
placeholder="5m"
|
|
88
|
+
style={inputStyle}
|
|
89
|
+
/>
|
|
90
|
+
</Field>
|
|
91
|
+
|
|
92
|
+
<Field label="Input (JSON)">
|
|
93
|
+
<textarea
|
|
94
|
+
className="mono"
|
|
95
|
+
value={value.input}
|
|
96
|
+
onChange={(e) => onChange({ ...value, input: e.target.value })}
|
|
97
|
+
rows={6}
|
|
98
|
+
spellCheck={false}
|
|
99
|
+
style={{ ...inputStyle, fontFamily: "var(--mono-font, monospace)", fontSize: "0.8125rem" }}
|
|
100
|
+
/>
|
|
101
|
+
</Field>
|
|
102
|
+
|
|
103
|
+
<label style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
|
104
|
+
<input
|
|
105
|
+
type="checkbox"
|
|
106
|
+
checked={value.enabled}
|
|
107
|
+
onChange={(e) => onChange({ ...value, enabled: e.target.checked })}
|
|
108
|
+
/>
|
|
109
|
+
<span className="mono" style={{ fontSize: "0.875rem" }}>Enabled</span>
|
|
110
|
+
</label>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) {
|
|
116
|
+
return (
|
|
117
|
+
<div>
|
|
118
|
+
<div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", marginBottom: "0.25rem" }}>
|
|
119
|
+
<label className="mono" style={{ fontSize: "0.75rem", color: "var(--muted)" }}>{label}</label>
|
|
120
|
+
{hint && <span className="mono" style={{ fontSize: "0.6875rem", color: "var(--muted)" }}>{hint}</span>}
|
|
121
|
+
</div>
|
|
122
|
+
{children}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const inputStyle: React.CSSProperties = {
|
|
128
|
+
width: "100%",
|
|
129
|
+
padding: "0.5rem 0.625rem",
|
|
130
|
+
border: "1px solid var(--border)",
|
|
131
|
+
borderRadius: "4px",
|
|
132
|
+
background: "var(--surface)",
|
|
133
|
+
color: "var(--text)",
|
|
134
|
+
fontSize: "0.875rem",
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const selectStyle: React.CSSProperties = {
|
|
138
|
+
...inputStyle,
|
|
139
|
+
appearance: "auto",
|
|
140
|
+
};
|