Skip to content

Commit fc96971

Browse files
committed
Signals menu
1 parent e7f5ce6 commit fc96971

File tree

3 files changed

+338
-29
lines changed

3 files changed

+338
-29
lines changed

src/lib/config/navigation.ts

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,21 @@ function buildSystemItems(): NavigationItem[] {
123123
label: "Migrations",
124124
iconComponent: GitBranch,
125125
},
126+
{
127+
href: "/system/webui-props",
128+
label: "WebUI Props",
129+
iconComponent: Settings,
130+
},
131+
];
132+
133+
return items;
134+
}
135+
136+
export const systemItems = buildSystemItems();
137+
138+
// Signals navigation items
139+
function buildSignalsItems(): NavigationItem[] {
140+
const items: NavigationItem[] = [
126141
{
127142
href: "/system/signal-channels",
128143
label: "Signal Channels",
@@ -131,19 +146,25 @@ function buildSystemItems(): NavigationItem[] {
131146
{
132147
href: "/system/signal-channels-stats",
133148
label: "Signal Stats",
134-
iconComponent: Radio,
135-
},
136-
{
137-
href: "/system/webui-props",
138-
label: "WebUI Props",
139-
iconComponent: Settings,
149+
iconComponent: BarChart3,
140150
},
141151
];
142152

143153
return items;
144154
}
145155

146-
export const systemItems = buildSystemItems();
156+
export const signalsItems = buildSignalsItems();
157+
158+
export function getActiveSignalsMenuItem(pathname: string) {
159+
const found = signalsItems.find((item) => {
160+
if (item.external) {
161+
return false;
162+
}
163+
return pathname.startsWith(item.href);
164+
});
165+
166+
return found || signalsItems[0];
167+
}
147168

148169
export function getActiveSystemMenuItem(pathname: string) {
149170
const found = systemItems.find((item) => {
@@ -511,6 +532,7 @@ export function getActiveAbacMenuItem(pathname: string) {
511532
export const navSections: NavigationSection[] = [
512533
{ id: "my-account", label: "My Account", iconComponent: User, items: myAccountItems, basePaths: ["/user"] },
513534
{ id: "system", label: "System", iconComponent: Server, items: systemItems, basePaths: ["/system"] },
535+
{ id: "signals", label: "Signals", iconComponent: Radio, items: signalsItems, basePaths: ["/system/signal-channels", "/system/signal-channels-stats"] },
514536
{ id: "integration", label: "Integration", iconComponent: Plug, items: integrationItems, basePaths: ["/integration"] },
515537
{ id: "metrics", label: "Metrics", iconComponent: BarChart3, items: metricsItems, basePaths: ["/metrics", "/aggregate-metrics", "/connector-metrics", "/connector-traces", "/connector-counts"] },
516538
{ id: "abac", label: "ABAC", iconComponent: Lock, items: abacItems, basePaths: ["/abac"] },

src/routes/(protected)/system/signal-channels/+page.svelte

Lines changed: 271 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@
3636
let deleteError = $state<string | null>(null);
3737
let deleteSuccess = $state<string | null>(null);
3838
39+
// Publish state
40+
let showPublishForm = $state(false);
41+
let publishChannel = $state("task-requests");
42+
let publishPayload = $state('{"message": "Please report what time it is where you are"}');
43+
let publishMessageType = $state("");
44+
let isPublishing = $state(false);
45+
let publishError = $state<string | null>(null);
46+
let publishSuccess = $state<string | null>(null);
47+
3948
let filteredChannels = $derived.by(() => {
4049
if (!channels.length) return [];
4150
if (!searchQuery.trim()) return channels;
@@ -205,6 +214,51 @@
205214
}
206215
}
207216
217+
async function publishMessage() {
218+
if (!publishChannel.trim()) return;
219+
220+
let parsedPayload: any;
221+
try {
222+
parsedPayload = JSON.parse(publishPayload);
223+
} catch {
224+
publishError = "Invalid JSON payload";
225+
return;
226+
}
227+
228+
try {
229+
isPublishing = true;
230+
publishError = null;
231+
publishSuccess = null;
232+
233+
const body: any = { payload: parsedPayload };
234+
if (publishMessageType.trim()) {
235+
body.message_type = publishMessageType.trim();
236+
}
237+
238+
const response = await fetch(
239+
`/api/signal/channels/${encodeURIComponent(publishChannel.trim())}/messages`,
240+
{
241+
method: "POST",
242+
headers: { "Content-Type": "application/json" },
243+
body: JSON.stringify(body),
244+
},
245+
);
246+
247+
if (!response.ok) {
248+
const errorData = await response.json().catch(() => ({}));
249+
throw new Error(errorData.error || `Failed (${response.status})`);
250+
}
251+
252+
publishSuccess = `Message published to "${publishChannel.trim()}"`;
253+
await fetchChannels();
254+
} catch (err) {
255+
publishError =
256+
err instanceof Error ? err.message : "Failed to publish message";
257+
} finally {
258+
isPublishing = false;
259+
}
260+
}
261+
208262
onMount(() => {
209263
fetchChannels();
210264
});
@@ -225,29 +279,37 @@
225279
</div>
226280
</div>
227281
<div class="header-controls">
228-
<button
229-
class="refresh-button"
230-
onclick={fetchChannels}
231-
disabled={isLoading}
232-
aria-label="Refresh signal channels"
233-
>
234-
<svg
235-
class="refresh-icon"
236-
class:spinning={isLoading}
237-
xmlns="http://www.w3.org/2000/svg"
238-
viewBox="0 0 24 24"
239-
fill="none"
240-
stroke="currentColor"
241-
stroke-width="2"
242-
stroke-linecap="round"
243-
stroke-linejoin="round"
282+
<div class="header-buttons">
283+
<button
284+
class="publish-button"
285+
onclick={() => { showPublishForm = !showPublishForm; publishError = null; publishSuccess = null; }}
286+
>
287+
{showPublishForm ? "Cancel" : "Publish Message"}
288+
</button>
289+
<button
290+
class="refresh-button"
291+
onclick={fetchChannels}
292+
disabled={isLoading}
293+
aria-label="Refresh signal channels"
244294
>
245-
<path
246-
d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"
247-
/>
248-
</svg>
249-
{isLoading ? "Refreshing..." : "Refresh"}
250-
</button>
295+
<svg
296+
class="refresh-icon"
297+
class:spinning={isLoading}
298+
xmlns="http://www.w3.org/2000/svg"
299+
viewBox="0 0 24 24"
300+
fill="none"
301+
stroke="currentColor"
302+
stroke-width="2"
303+
stroke-linecap="round"
304+
stroke-linejoin="round"
305+
>
306+
<path
307+
d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"
308+
/>
309+
</svg>
310+
{isLoading ? "Refreshing..." : "Refresh"}
311+
</button>
312+
</div>
251313
{#if lastUpdated}
252314
<div class="last-updated">
253315
Last updated: <span class="timestamp">{lastUpdated}</span>
@@ -256,6 +318,57 @@
256318
</div>
257319
</div>
258320
</div>
321+
{#if showPublishForm}
322+
<div class="publish-form">
323+
<div class="publish-row">
324+
<div class="publish-field">
325+
<label class="publish-label" for="pub-channel">Channel</label>
326+
<input
327+
id="pub-channel"
328+
type="text"
329+
class="publish-input"
330+
bind:value={publishChannel}
331+
disabled={isPublishing}
332+
/>
333+
</div>
334+
<div class="publish-field">
335+
<label class="publish-label" for="pub-type">Type <span class="optional">(optional)</span></label>
336+
<input
337+
id="pub-type"
338+
type="text"
339+
class="publish-input"
340+
placeholder="e.g. task-request"
341+
bind:value={publishMessageType}
342+
disabled={isPublishing}
343+
/>
344+
</div>
345+
</div>
346+
<div class="publish-field">
347+
<label class="publish-label" for="pub-payload">Payload (JSON)</label>
348+
<textarea
349+
id="pub-payload"
350+
class="publish-textarea"
351+
bind:value={publishPayload}
352+
disabled={isPublishing}
353+
rows="4"
354+
></textarea>
355+
</div>
356+
{#if publishError}
357+
<div class="alert alert-error">{publishError}</div>
358+
{/if}
359+
{#if publishSuccess}
360+
<div class="alert alert-success">{publishSuccess}</div>
361+
{/if}
362+
<button
363+
class="btn-publish"
364+
onclick={publishMessage}
365+
disabled={isPublishing || !publishChannel.trim()}
366+
>
367+
{isPublishing ? "Publishing..." : "Publish"}
368+
</button>
369+
</div>
370+
{/if}
371+
259372
<div class="panel-content">
260373
<!-- Status Messages -->
261374
{#if deleteSuccess}
@@ -541,6 +654,142 @@
541654
color: var(--color-surface-300);
542655
}
543656
657+
.header-buttons {
658+
display: flex;
659+
gap: 0.5rem;
660+
}
661+
662+
.publish-button {
663+
display: flex;
664+
align-items: center;
665+
gap: 0.5rem;
666+
padding: 0.5rem 1rem;
667+
background: #51b265;
668+
color: white;
669+
border: none;
670+
border-radius: 0.375rem;
671+
font-size: 0.875rem;
672+
font-weight: 500;
673+
cursor: pointer;
674+
transition: background-color 0.2s;
675+
}
676+
677+
.publish-button:hover {
678+
background: #3d9e52;
679+
}
680+
681+
:global([data-mode="dark"]) .publish-button {
682+
background: #51b265;
683+
}
684+
685+
:global([data-mode="dark"]) .publish-button:hover {
686+
background: #3d9e52;
687+
}
688+
689+
.publish-form {
690+
padding: 1rem 1.5rem;
691+
background: #f9fafb;
692+
border-bottom: 1px solid #e5e7eb;
693+
display: flex;
694+
flex-direction: column;
695+
gap: 0.75rem;
696+
}
697+
698+
:global([data-mode="dark"]) .publish-form {
699+
background: rgb(var(--color-surface-900));
700+
border-bottom-color: rgb(var(--color-surface-700));
701+
}
702+
703+
.publish-row {
704+
display: flex;
705+
gap: 0.75rem;
706+
}
707+
708+
.publish-field {
709+
display: flex;
710+
flex-direction: column;
711+
gap: 0.25rem;
712+
flex: 1;
713+
}
714+
715+
.publish-label {
716+
font-size: 0.75rem;
717+
font-weight: 600;
718+
color: #374151;
719+
}
720+
721+
:global([data-mode="dark"]) .publish-label {
722+
color: var(--color-surface-300);
723+
}
724+
725+
.publish-label .optional {
726+
font-weight: 400;
727+
color: #9ca3af;
728+
}
729+
730+
.publish-input {
731+
padding: 0.5rem 0.75rem;
732+
border: 1px solid #d1d5db;
733+
border-radius: 0.375rem;
734+
font-size: 0.875rem;
735+
font-family: monospace;
736+
}
737+
738+
.publish-input:focus {
739+
outline: none;
740+
border-color: #3b82f6;
741+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
742+
}
743+
744+
:global([data-mode="dark"]) .publish-input {
745+
background: rgb(var(--color-surface-700));
746+
border-color: rgb(var(--color-surface-600));
747+
color: var(--color-surface-100);
748+
}
749+
750+
.publish-textarea {
751+
padding: 0.5rem 0.75rem;
752+
border: 1px solid #d1d5db;
753+
border-radius: 0.375rem;
754+
font-size: 0.8125rem;
755+
font-family: monospace;
756+
resize: vertical;
757+
}
758+
759+
.publish-textarea:focus {
760+
outline: none;
761+
border-color: #3b82f6;
762+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
763+
}
764+
765+
:global([data-mode="dark"]) .publish-textarea {
766+
background: rgb(var(--color-surface-700));
767+
border-color: rgb(var(--color-surface-600));
768+
color: var(--color-surface-100);
769+
}
770+
771+
.btn-publish {
772+
align-self: flex-start;
773+
padding: 0.5rem 1.5rem;
774+
background: #51b265;
775+
color: white;
776+
border: none;
777+
border-radius: 0.375rem;
778+
font-size: 0.875rem;
779+
font-weight: 600;
780+
cursor: pointer;
781+
transition: background-color 0.2s;
782+
}
783+
784+
.btn-publish:hover:not(:disabled) {
785+
background: #3d9e52;
786+
}
787+
788+
.btn-publish:disabled {
789+
opacity: 0.6;
790+
cursor: not-allowed;
791+
}
792+
544793
.panel-content {
545794
padding: 1.5rem;
546795
}

0 commit comments

Comments
 (0)