Skip to content

Commit 648e4e2

Browse files
[cueweb/docs] Monitor Hosts: Full CueCommander parity (#2421)
## Related Issues Main issue: - #2016 Issues related to this PR: - #2422 ## Summarize your change. [cueweb] Monitor Hosts: Full CueCommander parity Extend /hosts to match CueGUI's Monitor Hosts window: - [x] Full column set with Swap/Physical/GPU/Temp red/green bars, Load %, GPU + Temp Free columns, comment icon, and row coloring by hardware/ lock state (new SimpleDataTable getRowClassName hook). - [x] Filter bar: name/regex + Filter Allocation/HardwareState/LockState/OS multi-selects, Auto-refresh, Refresh, Clear. - [x] Context menu: Comments, View Procs, Lock/Unlock, Edit Tags, Rename Tag, Change Allocation, Reboot, Reboot when idle, Delete Host, Set/Clear Repair State (Take Ownership shown disabled). - [x] New dialogs: host Comments (view/add), Rename Tag, Change Allocation, Delete confirm. - [x] Bottom Proc monitor panel (View Procs) with View Job / Unbook / Kill / Unbook and Kill. - [x] Routes: host renametag/setallocation/delete/sethardwarestate/addcomment, proc kill/unbookone, proc getprocs. Extended Host/Proc types + actions. [cueweb] Monitor Hosts: click row to load procs; document full parity Code: - [x] hosts page: a left-click on a host row now loads that host's procs into the bottom proc panel (dispatches VIEW_HOST_PROCS_EVENT), in addition to selecting it in the Attributes panel, same result as the menu's "View Procs". Docs (Monitor Hosts full CueCommander parity) across all guides + README: - [x] user-guide: rewrote the Monitor Hosts section for full parity, complete column set with Swap/Physical/GPU/Temp bars and comment icon, row coloring by hardware/lock state, filter bar (Allocation/HardwareState/LockState/OS), the full context menu, host comments with predefined-comment macros, and the proc monitor panel; added a "Take ownership" subsection. - [x] reference: expanded the Monitor Hosts behavior table (columns, row coloring, filter bar, full menu, comment macros, gating, proc panel). - [x] developer-guide: extended the host file tree and menu-gating notes (new routes/dialogs/proc panel, Take Ownership, comment macros). - [x] other-guides: rewrote the Monitor Hosts feature entry for full parity. - [x] concepts: enriched the Host/Proc interface rows. - [x] getting-started: a "Monitor Hosts (host & proc management)" deploy note. - [x] quick-starts / tutorials: "Manage render hosts" walkthroughs. - [x] README: a dedicated Monitor Hosts feature bullet. Take Ownership: corrected the docs, it is enabled only for an NIMBY_LOCKED host (canTakeOwnership parity) and confirms before claiming the host for the signed-in user (OwnerInterface/TakeOwnership), not a disabled placeholder. Adds the Monitor Hosts screenshots (filters, lock/unlock, reboot, edit/rename tag, change allocation, repair state, delete, comments + macros, take ownership). ## LLM usage disclosure Parts of this solution's implementation were developed with assistance from Claude Opus.
1 parent 6a17c7f commit 648e4e2

158 files changed

Lines changed: 12307 additions & 329 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cueweb/.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
NEXT_PUBLIC_OPENCUE_ENDPOINT=http://your-rest-gateway-url.com
22

3+
# Optional allow-list for the frame preview route (/api/frame/preview), as a
4+
# colon-separated list of absolute path prefixes. When set, the server only
5+
# serves preview images located under one of these roots. When unset, preview
6+
# reads are not restricted to a root (set this to harden the deployment).
7+
# CUEWEB_PREVIEW_ROOTS=/mnt/render:/shows
8+
39
# Optional: Loki log backend. When set, the frame log viewer queries this Loki
410
# server (by frame id) instead of reading the on-disk .rqlog file. Leave unset
511
# to use the default file-based log viewer.

cueweb/Dockerfile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,29 @@ ENV NEXT_PUBLIC_OPENCUE_ENDPOINT=${NEXT_PUBLIC_OPENCUE_ENDPOINT}
108108
ARG NEXT_PUBLIC_LOG_EDITOR_URL=
109109
ENV NEXT_PUBLIC_LOG_EDITOR_URL=${NEXT_PUBLIC_LOG_EDITOR_URL}
110110

111+
# CueProgBar launcher (Monitor Jobs "Show Progress Bar"). CueProgBar is a
112+
# desktop tool (python -m cuegui.cueguiplugin.cueprogbar <job>); a browser
113+
# can only reach it via a registered URL scheme. {job} is replaced with the
114+
# job name. Leave empty to fall back to the in-table Progress column.
115+
ARG NEXT_PUBLIC_CUEPROGBAR_URL=
116+
ENV NEXT_PUBLIC_CUEPROGBAR_URL=${NEXT_PUBLIC_CUEPROGBAR_URL}
117+
118+
# The CueProgBar command shown in the "Show Progress Bar" dialog. {job} is
119+
# replaced with the job name. Sites override this with their own launcher,
120+
# e.g. "spawn launch cueprogbar {job}".
121+
ARG NEXT_PUBLIC_CUEPROGBAR_COMMAND=python -m cuegui.cueguiplugin.cueprogbar {job}
122+
ENV NEXT_PUBLIC_CUEPROGBAR_COMMAND=${NEXT_PUBLIC_CUEPROGBAR_COMMAND}
123+
124+
# "Preview All" (Frames menu) opens rendered output in an external image
125+
# viewer. PREVIEW_COMMAND is the command shown/copied; PREVIEW_URL is an
126+
# optional registered URL scheme the Launch button hands off to a local
127+
# handler. Placeholders {paths} (output paths), {job}, {layer}, {frame} are
128+
# substituted. Override per site, e.g. a viewer scheme "openrv://{paths}".
129+
ARG NEXT_PUBLIC_PREVIEW_COMMAND=rv {paths}
130+
ENV NEXT_PUBLIC_PREVIEW_COMMAND=${NEXT_PUBLIC_PREVIEW_COMMAND}
131+
ARG NEXT_PUBLIC_PREVIEW_URL=
132+
ENV NEXT_PUBLIC_PREVIEW_URL=${NEXT_PUBLIC_PREVIEW_URL}
133+
111134
# Authentication providers - use ARG to allow override at build time
112135
# Set to empty string to disable authentication (sandbox mode)
113136
# Set to comma-separated list for production (e.g., "google,okta,github,ldap")

cueweb/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ CueWeb replicates the core functionality of CueGUI (Cuetopia and Cuecommander) i
107107
- Frame navigation with hyperlinks to logs and data pages.
108108
- Stacked job progress bar with a hover tooltip showing per-state frame counts and percentages (succeeded / running / waiting / depend / dead). The Layers table reuses the same `<ProgressBar/>` renderer with `getLayerProgressSegments` so per-layer progress matches the per-job style.
109109
- Frame state filter chips above the frames table (`WAITING`, `RUNNING`, `SUCCEEDED`, `DEAD`, `EATEN`, `DEPEND`) with per-state counts, OR-combined selection, and selection mirrored to the `frameStates` URL query parameter for bookmarkable/shareable filtered views.
110+
- Visual **frame range selector** above the frames table (CueGUI `FrameRangeSelection.py` parity): a horizontal strip with one state-colored cell per frame in ascending frame order. Click-drag selects a contiguous range, shift-click extends the selection from the anchor, and the selected subset feeds straight into the same **Retry** / **Eat** / **Kill** frame actions as the right-click menu (with a confirm step; Kill is destructive). The strip reflects the active frame-state filter and survives the 5s auto-refresh.
110111
- CueGUI-parity right-click menus on every row, following the CueGUI Monitor Jobs and Monitor Job Details menu structure. Menus scroll instead of overflowing on small viewports; items not yet implemented surface a friendly placeholder toast.
111112
- Mobile-friendly equivalent of right-click: every Jobs / Layers / Frames row has a small `` button as its leftmost cell. Tapping it opens the same context menu the desktop right-click opens, so touch users get the full action set without a right-click event.
112113
- Wired copy actions: **Copy Job Name** (Job menu); **Copy Layer Name** (Layer menu); **Copy Frame Name** + **Copy Log Path** (Frame menu). Each pushes the value to the clipboard with a confirmation toast. Works whether CueWeb is served from `localhost` or from a LAN IP over plain HTTP.
@@ -411,6 +412,7 @@ The current CueWeb system offers a robust set of features designed to enhance us
411412
- **Auto-reloading:** Real-time updates for tables.
412413
- **Job-finished notifications (two channels):** A per-row **Notify bell** subscribes the browser - a background poller fires a toast (and an optional desktop popup when notification permission is granted) when the job reaches `FINISHED`; subscriptions persist in `localStorage`, sync across tabs, and the notify decision is serialized cross-tab via the Web Locks API so only one tab toasts when several poll the same job. The right-click **Subscribe to Job** menu entry opens a CueGUI-parity dialog that registers an *email* subscriber on Cuebot via the `AddSubscriber` RPC, so Cuebot mails the saved address when the job finishes. The two channels are independent.
413414
- **Job dependencies (CueGUI parity):** the job right-click menu groups four entries together. **View Dependencies...** opens a themed dialog mirroring CueGUI's `DependDialog` - a Type / Target / Active / OnJob / OnLayer / OnFrame table backed by the `GetDepends` RPC, with a **Refresh** button. **Dependency Wizard...** is a multi-step state machine covering every CueGUI `depend.DependType` (Job-On-Job / Layer / Frame, Frame By Frame for all layers / Hard Depend, Layer-On-Job / Layer / Frame, Frame By Frame, Frame-On-Job / Layer / Frame, Layer on Simulation Frame); every picker (source layers / frames, target jobs / layers / frames) is multi-select and Done fires the full source x target cross-product in one bulk batch. The Hard Depend variant pairs source/target layers by `layer.type` and fans out one `CreateFrameByFrameDependency` per matched pair across every picked target job. **Drop External Dependencies** and **Drop Internal Dependencies** call the `DropDepends` RPC with `target = EXTERNAL` / `INTERNAL` respectively; on success they dispatch `cueweb:refresh-now` and `cueweb:depends-changed` so the Jobs table re-polls and the Group-By Dependent tree cache rebuilds immediately.
415+
- **Monitor Hosts (CueCommander parity):** a host registry at `/hosts`, the CueWeb equivalent of CueGUI's Monitor Hosts window, with the full column set - Name, a Comments icon, Load %, Swap / Physical / GPU Memory / Temp red-green usage bars, Total/Idle Memory, Temp Free (%), Cores / Idle Cores, GPUs / Idle GPUs, GPU Mem / Idle, Ping, Boot Time, Hardware, Locked, ThreadMode, OS, Tags - and rows tinted by condition (`hostRowClassName`: red for non-`UP`, amber for `REBOOT_WHEN_IDLE`, yellow for `UP` + `LOCKED`). A filter bar adds a name/regex box plus Allocation / HardwareState / LockState / OS multi-selects (mirrored to the URL) and Auto-refresh / Refresh / Clear. The row right-click menu mirrors CueGUI: **Comments…** (with reusable predefined-comment macros, stored in `localStorage["cueweb-comment-macros"]`), **View Procs**, **Lock / Unlock / Take Ownership** (Take Ownership enabled only for a `NIMBY_LOCKED` host - confirms, then claims it for the signed-in user via `OwnerInterface/TakeOwnership`), **Edit Tags… / Rename Tag… / Change Allocation…**, **Reboot / Reboot when idle / Delete Host**, and **Set / Clear Repair State**, each routed through `HostInterface` (`Lock`, `Unlock`, `Reboot`, `RebootWhenIdle`, `AddTags`/`RemoveTags`, `RenameTag`, `SetAllocation`, `SetHardwareState`, `AddComment`, `Delete`). A bottom **proc monitor panel** lists procs for selected hosts (`ProcInterface/GetProcs`) - populated by a left-click on a host row, the menu's **View Procs**, or typing host names - with per-proc View Job / Unbook / Kill / Unbook and Kill.
414416
- **Redirect (CueCommander parity):** an admin tool at `/redirect` that moves cores to a job that needs them by reassigning busy procs to a target job (killing the frames on those procs). Job + resource filters (Show, Include Groups, Require Services, Exclude Regex, Allocations, Min/Max Cores, Min Memory, Result Limit, Proc Hour Cutoff) drive a `ProcInterface/GetProcs` search; typing a target job auto-detects its Show and core/memory needs; the redirect (`HostInterface/RedirectToJob`) refuses an invalid/maxed target and warns before a paused-target or cross-show redirect.
415417
- **Stuck Frames (CueCommander -> Stuck Frame):** a stuck-frame finder at `/stuck-frames`, the CueWeb equivalent of CueGUI's CueCommander Stuck Frame window. Scans every running frame across active jobs and flags the ones whose log has gone silent relative to runtime, grouped under their job, with columns Name / Frame / Host / LLU / Runtime / % Stuck / Average / Last Line and **Auto-refresh** / **Refresh** / **Clear** controls. Detection thresholds run client-side and persist per browser (% of Run Since LLU, Min LLU, % Avg Completion, Total Runtime, Exclude Keywords), with a **+** button to add per-service filter rows so long-running services (e.g. Arnold) can use looser limits than quick ones. Frame right-click actions: Tail / View / View Last Log, Retry / Eat / Kill, Log Stuck Frame (and Log and Retry / Eat / Kill), Frame Not Stuck, Add Job to Excludes / Exclude and Remove Job, **Core Up** (raise the layer's minimum cores), and View Host; job-header actions add View Comments, Job Not Stuck, and Core Up across the job's stuck layers.
416418
- **Logs:** View current and previous logs via dropdown.
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
/*
6+
* Copyright Contributors to the OpenCue Project
7+
*
8+
* Licensed under the Apache License, Version 2.0 (the "License");
9+
* you may not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing, software
15+
* distributed under the License is distributed on an "AS IS" BASIS,
16+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
* See the License for the specific language governing permissions and
18+
* limitations under the License.
19+
*/
20+
21+
import "@testing-library/jest-dom";
22+
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
23+
import type { Frame } from "@/app/frames/frame-columns";
24+
import { eatFrames, killFrames, retryFrames } from "@/app/utils/action_utils";
25+
import { FrameRangeSelector } from "@/components/ui/frame-range-selector";
26+
27+
// Mock the action layer so the test asserts the selected subset that gets
28+
// handed off, not the network behavior (which has its own coverage).
29+
jest.mock("@/app/utils/action_utils", () => ({
30+
retryFrames: jest.fn(),
31+
eatFrames: jest.fn(),
32+
killFrames: jest.fn(),
33+
}));
34+
35+
// The safety flag hook reads localStorage + window events; stub it "enabled".
36+
jest.mock("@/app/utils/use_disable_job_interaction", () => ({
37+
useDisableJobInteraction: () => ({ disabled: false, setDisabled: jest.fn(), toggle: jest.fn() }),
38+
}));
39+
40+
function makeFrame(number: number, state = "DEAD"): Frame {
41+
return {
42+
id: `frame-${number}`,
43+
name: `${number}-layer`,
44+
layerName: "layer",
45+
number,
46+
state,
47+
retryCount: 0,
48+
exitStatus: 0,
49+
dispatchOrder: number,
50+
startTime: 0,
51+
stopTime: 0,
52+
maxRss: "0",
53+
usedMemory: "0",
54+
reservedMemory: "0",
55+
reservedGpuMemory: "0",
56+
lastResource: "/",
57+
checkpointState: "",
58+
checkpointCount: 0,
59+
totalCoreTime: 0,
60+
lluTime: 0,
61+
totalGpuTime: 0,
62+
maxGpuMemory: "0",
63+
usedGpuMemory: "0",
64+
frameStateDisplayOverride: "",
65+
};
66+
}
67+
68+
const FRAMES = [1, 2, 3, 4, 5].map((n) => makeFrame(n));
69+
70+
function cell(number: number): HTMLElement {
71+
const el = document.querySelector(`[data-frame-number="${number}"]`);
72+
if (!el) throw new Error(`cell #${number} not found`);
73+
return el as HTMLElement;
74+
}
75+
76+
beforeEach(() => {
77+
jest.clearAllMocks();
78+
});
79+
80+
describe("FrameRangeSelector", () => {
81+
it("drag selects a contiguous range and feeds it into Retry", async () => {
82+
render(<FrameRangeSelector frames={FRAMES} username="tester" />);
83+
84+
// Drag from frame #2 to frame #4 -> selects {2,3,4}.
85+
fireEvent.mouseDown(cell(2));
86+
fireEvent.mouseEnter(cell(3));
87+
fireEvent.mouseEnter(cell(4));
88+
fireEvent.mouseUp(window);
89+
90+
expect(screen.getByText(/Selected 3 frames \(#2#4\)/)).toBeInTheDocument();
91+
92+
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
93+
94+
// Confirm in the dialog (there are two "Retry" buttons now; pick the
95+
// one inside the dialog).
96+
const dialog = await screen.findByRole("dialog");
97+
fireEvent.click(within(dialog).getByRole("button", { name: "Retry" }));
98+
99+
await waitFor(() => expect(retryFrames).toHaveBeenCalledTimes(1));
100+
const handed = (retryFrames as jest.Mock).mock.calls[0][0] as Frame[];
101+
expect(handed.map((f) => f.number).sort((a, b) => a - b)).toEqual([2, 3, 4]);
102+
expect(eatFrames).not.toHaveBeenCalled();
103+
expect(killFrames).not.toHaveBeenCalled();
104+
});
105+
106+
it("shift-click extends the selection from the anchor", async () => {
107+
render(<FrameRangeSelector frames={FRAMES} username="tester" />);
108+
109+
// Anchor at #2 (single click), then shift-click #5 -> {2,3,4,5}.
110+
fireEvent.mouseDown(cell(2));
111+
fireEvent.mouseUp(window);
112+
fireEvent.mouseDown(cell(5), { shiftKey: true });
113+
114+
expect(screen.getByText(/Selected 4 frames \(#2#5\)/)).toBeInTheDocument();
115+
});
116+
117+
it("routes Kill through a destructive confirm with the selected subset", async () => {
118+
render(<FrameRangeSelector frames={FRAMES} username="tester" />);
119+
120+
fireEvent.mouseDown(cell(1));
121+
fireEvent.mouseEnter(cell(2));
122+
fireEvent.mouseUp(window);
123+
124+
fireEvent.click(screen.getByRole("button", { name: "Kill" }));
125+
const dialog = await screen.findByRole("dialog");
126+
fireEvent.click(within(dialog).getByRole("button", { name: "Kill" }));
127+
128+
await waitFor(() => expect(killFrames).toHaveBeenCalledTimes(1));
129+
const [handed, username, reason] = (killFrames as jest.Mock).mock.calls[0];
130+
expect((handed as Frame[]).map((f) => f.number)).toEqual([1, 2]);
131+
expect(username).toBe("tester");
132+
expect(reason).toMatch(/frame range selector/i);
133+
});
134+
135+
it("Clear removes the current selection", () => {
136+
render(<FrameRangeSelector frames={FRAMES} username="tester" />);
137+
138+
fireEvent.mouseDown(cell(1));
139+
fireEvent.mouseEnter(cell(3));
140+
fireEvent.mouseUp(window);
141+
expect(screen.getByText(/Selected 3 frames/)).toBeInTheDocument();
142+
143+
fireEvent.click(screen.getByRole("button", { name: "Clear" }));
144+
expect(screen.getByText(/Drag to select a range of 5 frames/)).toBeInTheDocument();
145+
});
146+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright Contributors to the OpenCue Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { handleRoute } from '@/app/utils/gateway_server';
18+
import { NextRequest, NextResponse } from "next/server";
19+
20+
// List the known department names (for the Group dialog's Department dropdown).
21+
// Request: {} (empty). RPC: /department.DepartmentInterface/GetDepartmentNames.
22+
export async function POST(request: NextRequest) {
23+
const endpoint = "/department.DepartmentInterface/GetDepartmentNames";
24+
const method = request.method;
25+
if (method !== 'POST') {
26+
return NextResponse.json({ error: 'Invalid method. Only POST is allowed.' }, { status: 405 });
27+
}
28+
29+
const response = await handleRoute(method, endpoint, JSON.stringify({}));
30+
const responseData = await response.json();
31+
32+
if (!response.ok) {
33+
return NextResponse.json({ error: responseData?.error ?? "Failed to fetch department names" }, { status: response.status });
34+
}
35+
return NextResponse.json({ data: responseData.data?.names ?? [] }, { status: response.status });
36+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright Contributors to the OpenCue Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { handleRoute } from '@/app/utils/gateway_server';
18+
import { NextRequest, NextResponse } from "next/server";
19+
20+
// List a department's tasks. Request: { department }.
21+
// RPC: /department.DepartmentInterface/GetTasks (nested under tasks.tasks).
22+
export async function POST(request: NextRequest) {
23+
const endpoint = "/department.DepartmentInterface/GetTasks";
24+
const method = request.method;
25+
if (method !== 'POST') {
26+
return NextResponse.json({ error: 'Invalid method. Only POST is allowed.' }, { status: 405 });
27+
}
28+
29+
let jsonBody: any;
30+
try {
31+
jsonBody = await request.json();
32+
} catch {
33+
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 });
34+
}
35+
const body = JSON.stringify(jsonBody);
36+
if (!jsonBody || typeof jsonBody !== 'object' || !jsonBody.department?.id) {
37+
return NextResponse.json({ error: 'Invalid request body: department is required' }, { status: 400 });
38+
}
39+
40+
const response = await handleRoute(method, endpoint, body);
41+
const responseData = await response.json();
42+
if (!response.ok) return NextResponse.json({ error: responseData.error }, { status: response.status });
43+
return NextResponse.json({ data: responseData.data?.tasks?.tasks ?? [] }, { status: response.status });
44+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright Contributors to the OpenCue Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { handleRoute } from '@/app/utils/gateway_server';
18+
import { NextRequest, NextResponse } from "next/server";
19+
20+
// List a filter's actions. Request: { filter }.
21+
// RPC: /filter.FilterInterface/GetActions (nested under actions.actions).
22+
export async function POST(request: NextRequest) {
23+
const endpoint = "/filter.FilterInterface/GetActions";
24+
const method = request.method;
25+
if (method !== 'POST') {
26+
return NextResponse.json({ error: 'Invalid method. Only POST is allowed.' }, { status: 405 });
27+
}
28+
29+
let jsonBody: any;
30+
try {
31+
jsonBody = await request.json();
32+
} catch {
33+
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 });
34+
}
35+
const body = JSON.stringify(jsonBody);
36+
if (!jsonBody || typeof jsonBody !== 'object' || !jsonBody.filter?.id) {
37+
return NextResponse.json({ error: 'Invalid request body: filter is required' }, { status: 400 });
38+
}
39+
40+
const response = await handleRoute(method, endpoint, body);
41+
const responseData = await response.json();
42+
if (!response.ok) return NextResponse.json({ error: responseData.error }, { status: response.status });
43+
return NextResponse.json({ data: responseData.data?.actions?.actions ?? [] }, { status: response.status });
44+
}

0 commit comments

Comments
 (0)