Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3dfd04b
[cueweb] Add Services (Facility Service Defaults) page (CueCommander …
ramonfigueiredo Jun 12, 2026
311cfb1
[cueweb] Add Stuck Frames page (CueCommander parity)
ramonfigueiredo Jun 12, 2026
e5502de
[cueweb] Stuck Frames: full StuckFramePlugin parity (CueCommander)
ramonfigueiredo Jun 12, 2026
791ddde
[cueweb] Monitor Hosts: Full CueCommander parity
ramonfigueiredo Jun 12, 2026
6015a08
[cueweb] Add Monitor Cue page (CueCommander parity)
ramonfigueiredo Jun 12, 2026
912cf57
[cueweb] Job/Layer/Frame context-menu parity + frame log viewer enhan…
ramonfigueiredo Jun 13, 2026
fc32257
Merge branch 'cueweb/job-layer-frame-actions-and-log-viewer' into cue…
ramonfigueiredo Jun 13, 2026
42c8846
[cueweb] Monitor Cue: CueGUI parity for menu, columns, booking bar, s…
ramonfigueiredo Jun 13, 2026
72bee10
Merge branch 'master' into cueweb-monitor-hosts-full
ramonfigueiredo Jun 13, 2026
3cedab5
Merge branch 'master' into cueweb/job-layer-frame-actions-and-log-viewer
ramonfigueiredo Jun 13, 2026
46edae4
Merge branch 'master' into cueweb-monitor-cue
ramonfigueiredo Jun 13, 2026
15ce44e
[cueweb] Address Monitor Hosts review: route cleanup, races, a11y, fi…
ramonfigueiredo Jun 13, 2026
53a59a2
[cueweb] Monitor Hosts: ownership, comments parity, tag refinements, …
ramonfigueiredo Jun 13, 2026
4ac4247
Merge branch 'master' into cueweb-facility-service-defaults
ramonfigueiredo Jun 13, 2026
a15f4bd
Merge branch 'master' into cueweb-stuck-frames
ramonfigueiredo Jun 13, 2026
8245516
[cueweb] Services: surface load/save/delete failures instead of maski…
ramonfigueiredo Jun 15, 2026
e57ab7f
[cueweb] Stuck Frames: bound per-job fan-out, paginate frames, surfac…
ramonfigueiredo Jun 15, 2026
5870b61
[cueweb] Stuck Frames: address review feedback
ramonfigueiredo Jun 15, 2026
74f55e2
[cueweb] Monitor Hosts: gate comment save on action success
ramonfigueiredo Jun 15, 2026
f7de7fb
[cueweb] Stuck Frames: address review feedback
ramonfigueiredo Jun 15, 2026
8260178
[cueweb] Monitor Cue: address review feedback
ramonfigueiredo Jun 15, 2026
dd59f8a
[cueweb] frame/preview: canonicalize with realpath before the root check
ramonfigueiredo Jun 15, 2026
0fc75ae
[cueweb] Stuck Frames: address review feedback
ramonfigueiredo Jun 15, 2026
2cf2f6f
[cueweb] Layer/frame actions & log viewer: address review feedback
ramonfigueiredo Jun 15, 2026
7d4a516
Merge branch 'cueweb/job-layer-frame-actions-and-log-viewer' of https…
ramonfigueiredo Jun 15, 2026
9d72247
Merge branch 'cueweb-monitor-cue' into cueweb/job-layer-frame-actions…
ramonfigueiredo Jun 15, 2026
e13790e
[cueweb] Stuck Frames: address review feedback
ramonfigueiredo Jun 16, 2026
956df07
[cueweb/docs] Document Facility Service Defaults (Services) page
ramonfigueiredo Jun 16, 2026
11d20e0
[cueweb] Monitor Cue: Add show right-click menu (Properties, Create G…
ramonfigueiredo Jun 17, 2026
8457de4
Merge branch 'cueweb-facility-service-defaults' into cueweb-monitor-cue
ramonfigueiredo Jun 17, 2026
cf7b48c
[cueweb] Monitor Cue: group folder tree + Service Properties (show ov…
ramonfigueiredo Jun 17, 2026
69f2737
[cueweb] Monitor Cue: harden API routes and dialogs per review feedback
ramonfigueiredo Jun 17, 2026
6c98a2e
[cueweb] Frame log viewer: version metadata, download, and error jumper
ramonfigueiredo Jun 18, 2026
9dfcd27
[cueweb] Attributes panel: frame selection + copy key/value buttons
ramonfigueiredo Jun 18, 2026
567e905
Merge branch 'cueweb-monitor-hosts-full' into cueweb/job-layer-frame-…
ramonfigueiredo Jun 18, 2026
0e211f2
Merge branch 'cueweb-monitor-cue' into cueweb/job-layer-frame-actions…
ramonfigueiredo Jun 18, 2026
bc964e0
Merge branch 'cueweb-stuck-frames' into cueweb/job-layer-frame-action…
ramonfigueiredo Jun 18, 2026
bae451b
[cueweb] Wire Attributes panel to all entity views (CueGUI parity)
ramonfigueiredo Jun 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions cueweb/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
NEXT_PUBLIC_OPENCUE_ENDPOINT=http://your-rest-gateway-url.com

# Optional allow-list for the frame preview route (/api/frame/preview), as a
# colon-separated list of absolute path prefixes. When set, the server only
# serves preview images located under one of these roots. When unset, preview
# reads are not restricted to a root (set this to harden the deployment).
# CUEWEB_PREVIEW_ROOTS=/mnt/render:/shows

# Optional allow-list for the Stuck Frames "Last Line" reader
# (/api/stuck-frames/lastline), as a colon-separated list of absolute path
# prefixes. When set, only .rqlog files under one of these roots are read.
# When unset, reads are not restricted to a root (job log paths are
# site-specific). Set this to harden the deployment.
# CUEWEB_LOG_ROOTS=/mnt/logs:/shows

# Sentry values
SENTRY_ENVIRONMENT='development'
SENTRY_DSN = sentrydsn
Expand Down
23 changes: 23 additions & 0 deletions cueweb/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,29 @@ ENV NEXT_PUBLIC_OPENCUE_ENDPOINT=${NEXT_PUBLIC_OPENCUE_ENDPOINT}
ARG NEXT_PUBLIC_LOG_EDITOR_URL=
ENV NEXT_PUBLIC_LOG_EDITOR_URL=${NEXT_PUBLIC_LOG_EDITOR_URL}

# CueProgBar launcher (Monitor Jobs "Show Progress Bar"). CueProgBar is a
# desktop tool (python -m cuegui.cueguiplugin.cueprogbar <job>); a browser
# can only reach it via a registered URL scheme. {job} is replaced with the
# job name. Leave empty to fall back to the in-table Progress column.
ARG NEXT_PUBLIC_CUEPROGBAR_URL=
ENV NEXT_PUBLIC_CUEPROGBAR_URL=${NEXT_PUBLIC_CUEPROGBAR_URL}

# The CueProgBar command shown in the "Show Progress Bar" dialog. {job} is
# replaced with the job name. Sites override this with their own launcher,
# e.g. "spawn launch cueprogbar {job}".
ARG NEXT_PUBLIC_CUEPROGBAR_COMMAND=python -m cuegui.cueguiplugin.cueprogbar {job}
ENV NEXT_PUBLIC_CUEPROGBAR_COMMAND=${NEXT_PUBLIC_CUEPROGBAR_COMMAND}

# "Preview All" (Frames menu) opens rendered output in an external image
# viewer. PREVIEW_COMMAND is the command shown/copied; PREVIEW_URL is an
# optional registered URL scheme the Launch button hands off to a local
# handler. Placeholders {paths} (output paths), {job}, {layer}, {frame} are
# substituted. Override per site, e.g. a viewer scheme "openrv://{paths}".
ARG NEXT_PUBLIC_PREVIEW_COMMAND=rv {paths}
ENV NEXT_PUBLIC_PREVIEW_COMMAND=${NEXT_PUBLIC_PREVIEW_COMMAND}
ARG NEXT_PUBLIC_PREVIEW_URL=
ENV NEXT_PUBLIC_PREVIEW_URL=${NEXT_PUBLIC_PREVIEW_URL}

# Authentication providers - use ARG to allow override at build time
# Set to empty string to disable authentication (sandbox mode)
# Set to comma-separated list for production (e.g., "google,okta,github,ldap")
Expand Down
1 change: 1 addition & 0 deletions cueweb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ CueWeb replicates the core functionality of CueGUI (Cuetopia and Cuecommander) i
- Frame navigation with hyperlinks to logs and data pages.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
Expand Down
146 changes: 146 additions & 0 deletions cueweb/app/__tests__/components/frame-range-selector.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* @jest-environment jsdom
*/

/*
* Copyright Contributors to the OpenCue Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import "@testing-library/jest-dom";
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import type { Frame } from "@/app/frames/frame-columns";
import { eatFrames, killFrames, retryFrames } from "@/app/utils/action_utils";
import { FrameRangeSelector } from "@/components/ui/frame-range-selector";

// Mock the action layer so the test asserts the selected subset that gets
// handed off, not the network behavior (which has its own coverage).
jest.mock("@/app/utils/action_utils", () => ({
retryFrames: jest.fn(),
eatFrames: jest.fn(),
killFrames: jest.fn(),
}));

// The safety flag hook reads localStorage + window events; stub it "enabled".
jest.mock("@/app/utils/use_disable_job_interaction", () => ({
useDisableJobInteraction: () => ({ disabled: false, setDisabled: jest.fn(), toggle: jest.fn() }),
}));

function makeFrame(number: number, state = "DEAD"): Frame {
return {
id: `frame-${number}`,
name: `${number}-layer`,
layerName: "layer",
number,
state,
retryCount: 0,
exitStatus: 0,
dispatchOrder: number,
startTime: 0,
stopTime: 0,
maxRss: "0",
usedMemory: "0",
reservedMemory: "0",
reservedGpuMemory: "0",
lastResource: "/",
checkpointState: "",
checkpointCount: 0,
totalCoreTime: 0,
lluTime: 0,
totalGpuTime: 0,
maxGpuMemory: "0",
usedGpuMemory: "0",
frameStateDisplayOverride: "",
};
}

const FRAMES = [1, 2, 3, 4, 5].map((n) => makeFrame(n));

function cell(number: number): HTMLElement {
const el = document.querySelector(`[data-frame-number="${number}"]`);
if (!el) throw new Error(`cell #${number} not found`);
return el as HTMLElement;
}

beforeEach(() => {
jest.clearAllMocks();
});

describe("FrameRangeSelector", () => {
it("drag selects a contiguous range and feeds it into Retry", async () => {
render(<FrameRangeSelector frames={FRAMES} username="tester" />);

// Drag from frame #2 to frame #4 -> selects {2,3,4}.
fireEvent.mouseDown(cell(2));
fireEvent.mouseEnter(cell(3));
fireEvent.mouseEnter(cell(4));
fireEvent.mouseUp(window);

expect(screen.getByText(/Selected 3 frames \(#2–#4\)/)).toBeInTheDocument();

fireEvent.click(screen.getByRole("button", { name: "Retry" }));

// Confirm in the dialog (there are two "Retry" buttons now; pick the
// one inside the dialog).
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByRole("button", { name: "Retry" }));

await waitFor(() => expect(retryFrames).toHaveBeenCalledTimes(1));
const handed = (retryFrames as jest.Mock).mock.calls[0][0] as Frame[];
expect(handed.map((f) => f.number).sort((a, b) => a - b)).toEqual([2, 3, 4]);
expect(eatFrames).not.toHaveBeenCalled();
expect(killFrames).not.toHaveBeenCalled();
});

it("shift-click extends the selection from the anchor", async () => {
render(<FrameRangeSelector frames={FRAMES} username="tester" />);

// Anchor at #2 (single click), then shift-click #5 -> {2,3,4,5}.
fireEvent.mouseDown(cell(2));
fireEvent.mouseUp(window);
fireEvent.mouseDown(cell(5), { shiftKey: true });

expect(screen.getByText(/Selected 4 frames \(#2–#5\)/)).toBeInTheDocument();
});

it("routes Kill through a destructive confirm with the selected subset", async () => {
render(<FrameRangeSelector frames={FRAMES} username="tester" />);

fireEvent.mouseDown(cell(1));
fireEvent.mouseEnter(cell(2));
fireEvent.mouseUp(window);

fireEvent.click(screen.getByRole("button", { name: "Kill" }));
const dialog = await screen.findByRole("dialog");
fireEvent.click(within(dialog).getByRole("button", { name: "Kill" }));

await waitFor(() => expect(killFrames).toHaveBeenCalledTimes(1));
const [handed, username, reason] = (killFrames as jest.Mock).mock.calls[0];
expect((handed as Frame[]).map((f) => f.number)).toEqual([1, 2]);
expect(username).toBe("tester");
expect(reason).toMatch(/frame range selector/i);
});

it("Clear removes the current selection", () => {
render(<FrameRangeSelector frames={FRAMES} username="tester" />);

fireEvent.mouseDown(cell(1));
fireEvent.mouseEnter(cell(3));
fireEvent.mouseUp(window);
expect(screen.getByText(/Selected 3 frames/)).toBeInTheDocument();

fireEvent.click(screen.getByRole("button", { name: "Clear" }));
expect(screen.getByText(/Drag to select a range of 5 frames/)).toBeInTheDocument();
});
});
36 changes: 36 additions & 0 deletions cueweb/app/api/department/getdepartmentnames/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright Contributors to the OpenCue Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { handleRoute } from '@/app/utils/api_utils';
import { NextRequest, NextResponse } from "next/server";

// List the known department names (for the Group dialog's Department dropdown).
// Request: {} (empty). RPC: /department.DepartmentInterface/GetDepartmentNames.
export async function POST(request: NextRequest) {
const endpoint = "/department.DepartmentInterface/GetDepartmentNames";
const method = request.method;
if (method !== 'POST') {
return NextResponse.json({ error: 'Invalid method. Only POST is allowed.' }, { status: 405 });
}

const response = await handleRoute(method, endpoint, JSON.stringify({}));
const responseData = await response.json();

if (!response.ok) {
return NextResponse.json({ error: responseData?.error ?? "Failed to fetch department names" }, { status: response.status });
}
return NextResponse.json({ data: responseData.data?.names ?? [] }, { status: response.status });
}
44 changes: 44 additions & 0 deletions cueweb/app/api/department/gettasks/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright Contributors to the OpenCue Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { handleRoute } from '@/app/utils/api_utils';
import { NextRequest, NextResponse } from "next/server";

// List a department's tasks. Request: { department }.
// RPC: /department.DepartmentInterface/GetTasks (nested under tasks.tasks).
export async function POST(request: NextRequest) {
const endpoint = "/department.DepartmentInterface/GetTasks";
const method = request.method;
if (method !== 'POST') {
return NextResponse.json({ error: 'Invalid method. Only POST is allowed.' }, { status: 405 });
}

let jsonBody: any;
try {
jsonBody = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 });
}
const body = JSON.stringify(jsonBody);
if (!jsonBody || typeof jsonBody !== 'object' || !jsonBody.department?.id) {
return NextResponse.json({ error: 'Invalid request body: department is required' }, { status: 400 });
}

const response = await handleRoute(method, endpoint, body);
const responseData = await response.json();
if (!response.ok) return NextResponse.json({ error: responseData.error }, { status: response.status });
return NextResponse.json({ data: responseData.data?.tasks?.tasks ?? [] }, { status: response.status });
}
44 changes: 44 additions & 0 deletions cueweb/app/api/filter/getactions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright Contributors to the OpenCue Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { handleRoute } from '@/app/utils/api_utils';
import { NextRequest, NextResponse } from "next/server";

// List a filter's actions. Request: { filter }.
// RPC: /filter.FilterInterface/GetActions (nested under actions.actions).
export async function POST(request: NextRequest) {
const endpoint = "/filter.FilterInterface/GetActions";
const method = request.method;
if (method !== 'POST') {
return NextResponse.json({ error: 'Invalid method. Only POST is allowed.' }, { status: 405 });
}

let jsonBody: any;
try {
jsonBody = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 });
}
const body = JSON.stringify(jsonBody);
if (!jsonBody || typeof jsonBody !== 'object' || !jsonBody.filter?.id) {
return NextResponse.json({ error: 'Invalid request body: filter is required' }, { status: 400 });
}

const response = await handleRoute(method, endpoint, body);
const responseData = await response.json();
if (!response.ok) return NextResponse.json({ error: responseData.error }, { status: response.status });
return NextResponse.json({ data: responseData.data?.actions?.actions ?? [] }, { status: response.status });
}
44 changes: 44 additions & 0 deletions cueweb/app/api/filter/getmatchers/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright Contributors to the OpenCue Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { handleRoute } from '@/app/utils/api_utils';
import { NextRequest, NextResponse } from "next/server";

// List a filter's matchers. Request: { filter }.
// RPC: /filter.FilterInterface/GetMatchers (nested under matchers.matchers).
export async function POST(request: NextRequest) {
const endpoint = "/filter.FilterInterface/GetMatchers";
const method = request.method;
if (method !== 'POST') {
return NextResponse.json({ error: 'Invalid method. Only POST is allowed.' }, { status: 405 });
}

let jsonBody: any;
try {
jsonBody = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 });
}
const body = JSON.stringify(jsonBody);
if (!jsonBody || typeof jsonBody !== 'object' || !jsonBody.filter?.id) {
return NextResponse.json({ error: 'Invalid request body: filter is required' }, { status: 400 });
}

const response = await handleRoute(method, endpoint, body);
const responseData = await response.json();
if (!response.ok) return NextResponse.json({ error: responseData.error }, { status: response.status });
return NextResponse.json({ data: responseData.data?.matchers?.matchers ?? [] }, { status: response.status });
}
Loading
Loading