+ ) : null}
+
+ >
+ )}
+
+ {/* Add Limit (header button) + the row context-menu dialogs (Edit Max
+ Value / Rename / Delete), opened via CustomEvents. */}
+
+
+
+
+
+ );
+}
diff --git a/cueweb/app/utils/action_utils.ts b/cueweb/app/utils/action_utils.ts
index 182b437d0..6cbcd6bd4 100644
--- a/cueweb/app/utils/action_utils.ts
+++ b/cueweb/app/utils/action_utils.ts
@@ -20,7 +20,7 @@ import * as React from "react";
import { Frame } from "../frames/frame-columns";
import { Layer } from "../layers/layer-columns";
import { accessActionApi, accessGetApi } from "./api_utils";
-import { getFrameLogDir, getJobForLayer, Host, JobComment, Show } from "./get_utils";
+import { getFrameLogDir, getJobForLayer, Host, JobComment, Limit, Show } from "./get_utils";
import { handleError, toastSuccess, toastWarning } from "./notify_utils";
/**************************************/
@@ -1159,3 +1159,60 @@ export function createSubscriptionGivenRow(row: Row) {
}),
);
}
+
+/**************************************/
+// Limit actions (CueCommander Limits window parity)
+/**************************************/
+
+// Limit mutations key on the limit name (the proto requests take name /
+// old_name, not an id or object). They call accessActionApi directly so the
+// calling dialog can show a single toast; errors are surfaced by accessActionApi.
+async function limitAction(endpoint: string, body: object): Promise {
+ const result = await accessActionApi(endpoint, [JSON.stringify(body)]);
+ return !!result?.success;
+}
+
+export async function createLimit(name: string, maxValue: number): Promise {
+ return limitAction("/api/limit/action/create", { name, max_value: maxValue });
+}
+
+export async function deleteLimit(name: string): Promise {
+ return limitAction("/api/limit/action/delete", { name });
+}
+
+export async function renameLimit(oldName: string, newName: string): Promise {
+ return limitAction("/api/limit/action/rename", { old_name: oldName, new_name: newName });
+}
+
+export async function setLimitMaxValue(name: string, maxValue: number): Promise {
+ return limitAction("/api/limit/action/setmaxvalue", { name, max_value: maxValue });
+}
+
+// Context-menu dispatchers: open the page-level dialogs via CustomEvent so the
+// free-function handlers stay free of component state (same pattern as hosts/shows).
+export function editLimitMaxValueGivenRow(row: Row) {
+ if (typeof window === "undefined") return;
+ window.dispatchEvent(
+ new CustomEvent("cueweb:open-limit-edit-max-value", {
+ detail: { limit: row.original as Limit },
+ }),
+ );
+}
+
+export function renameLimitGivenRow(row: Row) {
+ if (typeof window === "undefined") return;
+ window.dispatchEvent(
+ new CustomEvent("cueweb:open-limit-rename", {
+ detail: { limit: row.original as Limit },
+ }),
+ );
+}
+
+export function deleteLimitGivenRow(row: Row) {
+ if (typeof window === "undefined") return;
+ window.dispatchEvent(
+ new CustomEvent("cueweb:open-limit-delete", {
+ detail: { limit: row.original as Limit },
+ }),
+ );
+}
diff --git a/cueweb/app/utils/get_utils.ts b/cueweb/app/utils/get_utils.ts
index 12e5f92ff..6179ee391 100644
--- a/cueweb/app/utils/get_utils.ts
+++ b/cueweb/app/utils/get_utils.ts
@@ -173,6 +173,15 @@ export type Allocation = {
};
};
+// Limit shape - mirrors limit.Limit. maxValue / currentRunning arrive from the
+// gateway in camelCase.
+export type Limit = {
+ id: string;
+ name: string;
+ maxValue: number;
+ currentRunning: number;
+};
+
// Fetch a single frame based on the request body
export async function getFrame(body: string): Promise {
const ENDPOINT = "/api/frame/getframe";
@@ -343,6 +352,13 @@ export async function getAllocations(): Promise {
return Array.isArray(response) ? response : [];
}
+// Fetch every limit known to Cuebot (for the Limits page).
+export async function getLimits(): Promise {
+ const ENDPOINT = "/api/limit/getall";
+ const response = await accessGetApi(ENDPOINT, JSON.stringify({}));
+ return Array.isArray(response) ? response : [];
+}
+
// Fetch all comments for a given job
export async function getJobComments(job: Job): Promise {
const ENDPOINT = "/api/job/getcomments";
diff --git a/cueweb/components/ui/context_menus/action-context-menu.tsx b/cueweb/components/ui/context_menus/action-context-menu.tsx
index 748147cce..54b44dc39 100644
--- a/cueweb/components/ui/context_menus/action-context-menu.tsx
+++ b/cueweb/components/ui/context_menus/action-context-menu.tsx
@@ -26,6 +26,7 @@ import {
copyJobNameGivenRow,
copyLayerNameGivenRow,
createSubscriptionGivenRow,
+ deleteLimitGivenRow,
dependencyWizardGivenRow,
dropExternalDependsGivenRow,
dropInternalDependsGivenRow,
@@ -33,6 +34,7 @@ import {
eatJobsDeadFramesGivenRow,
eatLayerFramesGivenRow,
editHostTagsGivenRow,
+ editLimitMaxValueGivenRow,
emailArtistGivenRow,
killFrameGivenRow,
killJobGivenRow,
@@ -45,6 +47,7 @@ import {
retryFrameGivenRow,
retryJobsDeadFramesGivenRow,
retryLayerDeadFramesGivenRow,
+ renameLimitGivenRow,
retryLayerFramesGivenRow,
setCoresGivenRow,
setMaxRetriesGivenRow,
@@ -77,6 +80,7 @@ import {
TbLockOpen,
TbMessage,
TbPacman,
+ TbPencil,
TbPlayerPause,
TbPlayerPlay,
TbPlugConnectedX,
@@ -86,6 +90,7 @@ import {
TbReload,
TbSettings,
TbStar,
+ TbTrash,
TbTag,
} from "react-icons/tb";
import { BaseContextMenu } from "./base-context-menu";
@@ -659,6 +664,54 @@ export const ShowContextMenu: React.FC = ({
);
};
+interface LimitContextMenuProps {
+ contextMenuState: ContextMenuState;
+ contextMenuHandleClose: () => void;
+ contextMenuRef: React.RefObject;
+ contextMenuTargetAreaRef: React.RefObject;
+}
+
+// Context menu for the Limits table (CueGUI LimitsWidget parity): Edit Max
+// Value, Delete Limit, Rename.
+export const LimitContextMenu: React.FC = ({
+ contextMenuState,
+ contextMenuHandleClose,
+ contextMenuRef,
+ contextMenuTargetAreaRef,
+}) => {
+ const items: MenuItem[] = [
+ {
+ label: "Edit Max Value",
+ onClick: editLimitMaxValueGivenRow,
+ isActive: true,
+ component: ,
+ },
+ sep("group-limit-modify"),
+ {
+ label: "Delete Limit",
+ onClick: deleteLimitGivenRow,
+ isActive: true,
+ component: ,
+ },
+ {
+ label: "Rename",
+ onClick: renameLimitGivenRow,
+ isActive: true,
+ component: ,
+ },
+ ];
+
+ return (
+
+ );
+};
+
// Context menu for tables that contain Frames
export const FrameContextMenu: React.FC = ({
username,
diff --git a/cueweb/components/ui/limit-action-events.ts b/cueweb/components/ui/limit-action-events.ts
new file mode 100644
index 000000000..72dfc0a0f
--- /dev/null
+++ b/cueweb/components/ui/limit-action-events.ts
@@ -0,0 +1,33 @@
+/*
+ * 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 type { Limit } from "@/app/utils/get_utils";
+
+// Shared CustomEvent names + payload types for the Limits window dialogs,
+// kept in one module so the row context menu, the dialogs, and the page agree
+// on the contract (same pattern as host-/show-action-events).
+
+export const OPEN_LIMIT_EDIT_MAX_VALUE_EVENT = "cueweb:open-limit-edit-max-value";
+export const OPEN_LIMIT_RENAME_EVENT = "cueweb:open-limit-rename";
+export const OPEN_LIMIT_DELETE_EVENT = "cueweb:open-limit-delete";
+
+export type OpenLimitDetail = {
+ limit: Limit;
+};
+
+// Fired after a limit changes (created, renamed, deleted, max value set) so
+// the Limits page re-fetches.
+export const LIMITS_CHANGED_EVENT = "cueweb:limits-changed";
diff --git a/cueweb/components/ui/limit-add-dialog.tsx b/cueweb/components/ui/limit-add-dialog.tsx
new file mode 100644
index 000000000..31f18f4ae
--- /dev/null
+++ b/cueweb/components/ui/limit-add-dialog.tsx
@@ -0,0 +1,101 @@
+"use client";
+
+/*
+ * 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 * as React from "react";
+
+import { createLimit } from "@/app/utils/action_utils";
+import { toastSuccess, toastWarning } from "@/app/utils/notify_utils";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { LIMITS_CHANGED_EVENT } from "@/components/ui/limit-action-events";
+
+interface LimitAddDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+/**
+ * "Add Limit" dialog (CueGUI LimitsWidget parity). Opened from the Limits page
+ * header button. Asks for a name; the new limit is created with max value 0
+ * (matching CueGUI, which then lets you Edit Max Value). Fires
+ * `cueweb:limits-changed` on success.
+ */
+export function LimitAddDialog({ open, onOpenChange }: LimitAddDialogProps) {
+ const [name, setName] = React.useState("");
+ const [submitting, setSubmitting] = React.useState(false);
+
+ const handleOpenChange = (next: boolean) => {
+ if (!next) setName("");
+ onOpenChange(next);
+ };
+
+ async function handleCreate() {
+ const trimmed = name.trim();
+ if (trimmed.length === 0) {
+ toastWarning("Enter a name for the new limit.");
+ return;
+ }
+ setSubmitting(true);
+ try {
+ const ok = await createLimit(trimmed, 0);
+ if (ok) {
+ toastSuccess(`Created limit ${trimmed}`);
+ window.dispatchEvent(new CustomEvent(LIMITS_CHANGED_EVENT));
+ handleOpenChange(false);
+ }
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/cueweb/components/ui/limit-delete-dialog.tsx b/cueweb/components/ui/limit-delete-dialog.tsx
new file mode 100644
index 000000000..9c5b408b0
--- /dev/null
+++ b/cueweb/components/ui/limit-delete-dialog.tsx
@@ -0,0 +1,99 @@
+"use client";
+
+/*
+ * 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 * as React from "react";
+
+import type { Limit } from "@/app/utils/get_utils";
+import { deleteLimit } from "@/app/utils/action_utils";
+import { toastSuccess } from "@/app/utils/notify_utils";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import {
+ LIMITS_CHANGED_EVENT,
+ OPEN_LIMIT_DELETE_EVENT,
+ type OpenLimitDetail,
+} from "@/components/ui/limit-action-events";
+
+/**
+ * "Delete selected limits?" confirmation (CueGUI LimitsWidget parity). Mounted
+ * once on the Limits page and opened by a `cueweb:open-limit-delete` event from
+ * the row context menu. On confirm it calls Delete (by name) and fires
+ * `cueweb:limits-changed`.
+ */
+export function LimitDeleteDialog() {
+ const [open, setOpen] = React.useState(false);
+ const [limit, setLimit] = React.useState(null);
+ const [submitting, setSubmitting] = React.useState(false);
+
+ React.useEffect(() => {
+ function handler(e: Event) {
+ const detail = (e as CustomEvent).detail;
+ if (!detail?.limit) return;
+ setLimit(detail.limit);
+ setOpen(true);
+ }
+ window.addEventListener(OPEN_LIMIT_DELETE_EVENT, handler);
+ return () => window.removeEventListener(OPEN_LIMIT_DELETE_EVENT, handler);
+ }, []);
+
+ async function handleConfirm() {
+ if (!limit) return;
+ setSubmitting(true);
+ try {
+ const ok = await deleteLimit(limit.name);
+ if (ok) {
+ toastSuccess(`Deleted limit ${limit.name}`);
+ window.dispatchEvent(new CustomEvent(LIMITS_CHANGED_EVENT));
+ setOpen(false);
+ }
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/cueweb/components/ui/limit-edit-max-value-dialog.tsx b/cueweb/components/ui/limit-edit-max-value-dialog.tsx
new file mode 100644
index 000000000..a238768fa
--- /dev/null
+++ b/cueweb/components/ui/limit-edit-max-value-dialog.tsx
@@ -0,0 +1,124 @@
+"use client";
+
+/*
+ * 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 * as React from "react";
+
+import type { Limit } from "@/app/utils/get_utils";
+import { setLimitMaxValue } from "@/app/utils/action_utils";
+import { toastSuccess, toastWarning } from "@/app/utils/notify_utils";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import {
+ LIMITS_CHANGED_EVENT,
+ OPEN_LIMIT_EDIT_MAX_VALUE_EVENT,
+ type OpenLimitDetail,
+} from "@/components/ui/limit-action-events";
+
+/**
+ * "Edit Max Value" dialog (CueGUI LimitsWidget parity). Mounted once on the
+ * Limits page and opened by a `cueweb:open-limit-edit-max-value` event from the
+ * row context menu. Validates a non-negative integer before calling
+ * SetMaxValue, then fires `cueweb:limits-changed` so the table refreshes.
+ */
+export function LimitEditMaxValueDialog() {
+ const [open, setOpen] = React.useState(false);
+ const [limit, setLimit] = React.useState(null);
+ const [value, setValue] = React.useState("0");
+ const [submitting, setSubmitting] = React.useState(false);
+
+ React.useEffect(() => {
+ function handler(e: Event) {
+ const detail = (e as CustomEvent).detail;
+ if (!detail?.limit) return;
+ setLimit(detail.limit);
+ setValue(String(detail.limit.maxValue ?? 0));
+ setOpen(true);
+ }
+ window.addEventListener(OPEN_LIMIT_EDIT_MAX_VALUE_EVENT, handler);
+ return () => window.removeEventListener(OPEN_LIMIT_EDIT_MAX_VALUE_EVENT, handler);
+ }, []);
+
+ async function handleSave() {
+ if (!limit) return;
+ if (value.trim() === "") {
+ toastWarning("Max value must be a non-negative integer.");
+ return;
+ }
+ const n = Number(value);
+ if (!Number.isInteger(n) || n < 0) {
+ toastWarning("Max value must be a non-negative integer.");
+ return;
+ }
+ setSubmitting(true);
+ try {
+ const ok = await setLimitMaxValue(limit.name, n);
+ if (ok) {
+ toastSuccess(`Set max value ${n} on ${limit.name}`);
+ window.dispatchEvent(new CustomEvent(LIMITS_CHANGED_EVENT));
+ setOpen(false);
+ }
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/cueweb/components/ui/limit-rename-dialog.tsx b/cueweb/components/ui/limit-rename-dialog.tsx
new file mode 100644
index 000000000..a2c85a39a
--- /dev/null
+++ b/cueweb/components/ui/limit-rename-dialog.tsx
@@ -0,0 +1,111 @@
+"use client";
+
+/*
+ * 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 * as React from "react";
+
+import type { Limit } from "@/app/utils/get_utils";
+import { renameLimit } from "@/app/utils/action_utils";
+import { toastSuccess, toastWarning } from "@/app/utils/notify_utils";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import {
+ LIMITS_CHANGED_EVENT,
+ OPEN_LIMIT_RENAME_EVENT,
+ type OpenLimitDetail,
+} from "@/components/ui/limit-action-events";
+
+/**
+ * "Rename a Limit" dialog (CueGUI LimitsWidget parity). Mounted once on the
+ * Limits page and opened by a `cueweb:open-limit-rename` event from the row
+ * context menu. On OK it calls Rename (old_name -> new_name) and fires
+ * `cueweb:limits-changed`.
+ */
+export function LimitRenameDialog() {
+ const [open, setOpen] = React.useState(false);
+ const [limit, setLimit] = React.useState(null);
+ const [name, setName] = React.useState("");
+ const [submitting, setSubmitting] = React.useState(false);
+
+ React.useEffect(() => {
+ function handler(e: Event) {
+ const detail = (e as CustomEvent).detail;
+ if (!detail?.limit) return;
+ setLimit(detail.limit);
+ setName("");
+ setOpen(true);
+ }
+ window.addEventListener(OPEN_LIMIT_RENAME_EVENT, handler);
+ return () => window.removeEventListener(OPEN_LIMIT_RENAME_EVENT, handler);
+ }, []);
+
+ async function handleSave() {
+ if (!limit) return;
+ const trimmed = name.trim();
+ if (trimmed.length === 0) {
+ toastWarning("Please enter a new limit name.");
+ return;
+ }
+ setSubmitting(true);
+ try {
+ const ok = await renameLimit(limit.name, trimmed);
+ if (ok) {
+ toastSuccess(`Renamed ${limit.name} to ${trimmed}`);
+ window.dispatchEvent(new CustomEvent(LIMITS_CHANGED_EVENT));
+ setOpen(false);
+ }
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/cueweb/components/ui/simple-data-table.tsx b/cueweb/components/ui/simple-data-table.tsx
index fa00e176d..842aa2db5 100644
--- a/cueweb/components/ui/simple-data-table.tsx
+++ b/cueweb/components/ui/simple-data-table.tsx
@@ -29,7 +29,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { EmptyState } from "@/components/ui/empty-state";
-import { FrameContextMenu, HostContextMenu, LayerContextMenu, ShowContextMenu } from "@/components/ui/context_menus/action-context-menu";
+import { FrameContextMenu, HostContextMenu, LayerContextMenu, LimitContextMenu, ShowContextMenu } from "@/components/ui/context_menus/action-context-menu";
import { useContextMenu } from "@/components/ui/context_menus/useContextMenu";
import { Input } from "@/components/ui/input";
import { DataTablePagination } from "@/components/ui/pagination";
@@ -46,7 +46,7 @@ import {
useReactTable,
VisibilityState,
} from "@tanstack/react-table";
-import { ChevronDown, ChevronLeft, ChevronRight, Cpu, Film, Layers, PieChart, Search, Server, X } from "lucide-react";
+import { ChevronDown, ChevronLeft, ChevronRight, Cpu, Film, Gauge, Layers, PieChart, Search, Server, X } from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import * as React from "react";
import { Job } from "../../app/jobs/columns";
@@ -79,6 +79,9 @@ interface SimpleDataTableProps {
// Allocations variant (read-only, Allocations page): allocation-specific
// filter/empty copy and no row context menu.
isAllocationsTable?: boolean;
+ // Limits variant (Limits page): limit-specific filter/empty copy and the
+ // LimitContextMenu (Edit Max Value, Delete, Rename).
+ isLimitsTable?: boolean;
username: string;
// When set, column visibility for this table persists to localStorage
// under the given key. Use stable keys like "cueweb.layers.columnVisibility"
@@ -114,6 +117,7 @@ export function SimpleDataTable({
isProcsTable = false,
isShowsTable = false,
isAllocationsTable = false,
+ isLimitsTable = false,
username,
columnVisibilityStorageKey,
defaultColumnVisibility,
@@ -515,8 +519,8 @@ export function SimpleDataTable({
type="search"
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
- placeholder={isHostsTable ? "Filter hosts..." : isProcsTable ? "Filter procs..." : isShowsTable ? "Filter shows..." : isAllocationsTable ? "Filter allocations..." : (isFramesTable || isFramesLogTable) ? "Filter frames..." : "Filter layers..."}
- aria-label={isHostsTable ? "Filter hosts" : isProcsTable ? "Filter procs" : isShowsTable ? "Filter shows" : isAllocationsTable ? "Filter allocations" : (isFramesTable || isFramesLogTable) ? "Filter frames" : "Filter layers"}
+ placeholder={isHostsTable ? "Filter hosts..." : isProcsTable ? "Filter procs..." : isShowsTable ? "Filter shows..." : isAllocationsTable ? "Filter allocations..." : isLimitsTable ? "Filter limits..." : (isFramesTable || isFramesLogTable) ? "Filter frames..." : "Filter layers..."}
+ aria-label={isHostsTable ? "Filter hosts" : isProcsTable ? "Filter procs" : isShowsTable ? "Filter shows" : isAllocationsTable ? "Filter allocations" : isLimitsTable ? "Filter limits" : (isFramesTable || isFramesLogTable) ? "Filter frames" : "Filter layers"}
className="h-8 w-44 pl-7 pr-7 text-xs"
/>
{globalFilter ? (
@@ -629,6 +633,8 @@ export function SimpleDataTable({
) : isAllocationsTable ? (
+ ) : isLimitsTable ? (
+
) : (
)
@@ -642,6 +648,8 @@ export function SimpleDataTable({
? "No shows"
: isAllocationsTable
? "No allocations"
+ : isLimitsTable
+ ? "No limits"
: isFramesTable
? "Layer has no frames"
: isFramesLogTable
@@ -657,6 +665,8 @@ export function SimpleDataTable({
? "No active shows. Use Create Show to add one."
: isAllocationsTable
? "No allocations are configured in Cuebot."
+ : isLimitsTable
+ ? "No limits are configured. Use Add Limit to create one."
: isFramesTable
? "No frames matched the current filter. Clear the frame-state chips above to see every frame."
: isFramesLogTable
@@ -681,9 +691,10 @@ export function SimpleDataTable({
)}
{/* Row context menu. Hosts get Lock/Unlock/Reboot; shows get Show
- Properties / Create Subscription; frames/frame-logs get the frame
- menu; the read-only procs and allocations tables get none; everything
- else (layers) gets the layer menu. */}
+ Properties / Create Subscription; limits get Edit/Delete/Rename;
+ frames/frame-logs get the frame menu; the read-only procs and
+ allocations tables get none; everything else (layers) gets the layer
+ menu. */}
{(isProcsTable || isAllocationsTable) ? null : isHostsTable ? (
({
contextMenuRef={contextMenuRef}
contextMenuTargetAreaRef={contextMenuTargetAreaRef}
/>
+ ) : isLimitsTable ? (
+
) : isShowsTable ? (
?tab=overview")`.
- **`JobDetailsPage`** (`app/jobs/[job-name]/page.tsx`): Standalone tabbed job-details route reached via the **View Job Details** right-click entry (or the row's `⋮` Actions button). Resolves the job by name through `findJobByName(...)`, polls layers + frames every 5s with cancellation guards, and exposes five tabs - **Overview**, **Layers**, **Frames**, **Comments**, **Dependencies**. The active tab is mirrored to the URL as `?tab=` and read back through `useSearchParams()` + `router.replace(...)` so the page is bookmarkable and browser back/forward walks between tabs. `isTabKey(value)` rejects unknown query values so the URL can never select a missing tab. The Comments tab embeds a read-only preview of `getJobComments(...)` with a link out to the full `/jobs//comments` editor; Dependencies is currently a placeholder. The standard `Breadcrumbs` + `EmptyState` (`FileX` icon, "Job not found") wrappers cover loading and missing-job paths.
-- **`SimpleDataTable`** (`components/ui/simple-data-table.tsx`): Shared TanStack-table wrapper used by Layers, Frames, the Monitor Hosts table, the host detail page's procs table, the Shows table, and the Allocations table (plus the standalone log-viewer / per-job detail page). Owns the per-table substring filter (`globalFilter` + `getFilteredRowModel`), column-visibility persistence (`columnVisibilityStorageKey`), and column-order persistence (a parallel `cueweb.
.columnOrder` key derived from the visibility key). Renders the Columns dropdown that holds the `←` / `→` reorder buttons and the **Reset to Default** action. The mutually-exclusive `isFramesTable` / `isFramesLogTable` / `isHostsTable` / `isProcsTable` / `isShowsTable` / `isAllocationsTable` flags select per-table filter/empty-state copy and which row context menu renders (`isHostsTable` → `HostContextMenu`; `isShowsTable` → `ShowContextMenu`; frames → `FrameContextMenu`; `isProcsTable` / `isAllocationsTable` → none, read-only; otherwise `LayerContextMenu`).
+- **`SimpleDataTable`** (`components/ui/simple-data-table.tsx`): Shared TanStack-table wrapper used by Layers, Frames, the Monitor Hosts table, the host detail page's procs table, the Shows table, the Allocations table, and the Limits table (plus the standalone log-viewer / per-job detail page). Owns the per-table substring filter (`globalFilter` + `getFilteredRowModel`), column-visibility persistence (`columnVisibilityStorageKey`), and column-order persistence (a parallel `cueweb.
.columnOrder` key derived from the visibility key). Renders the Columns dropdown that holds the `←` / `→` reorder buttons and the **Reset to Default** action. The mutually-exclusive `isFramesTable` / `isFramesLogTable` / `isHostsTable` / `isProcsTable` / `isShowsTable` / `isAllocationsTable` / `isLimitsTable` flags select per-table filter/empty-state copy and which row context menu renders (`isHostsTable` → `HostContextMenu`; `isShowsTable` → `ShowContextMenu`; `isLimitsTable` → `LimitContextMenu`; frames → `FrameContextMenu`; `isProcsTable` / `isAllocationsTable` → none, read-only; otherwise `LayerContextMenu`).
- **`JobProgressBar` / `LayerProgressBar`** (`components/ui/{job,layer}-progress-bar.tsx`): Stacked progress bars with a hover tooltip showing per-state counts and percentages. Both delegate to the shared `` renderer in `components/ui/progressbar.tsx`. Segment colors and ordering come from `app/utils/{job,layer}_progress_utils.ts`.
- **`KeyboardShortcuts`** (`components/ui/shortcuts-overlay.tsx`): Global keyboard handler + cheat-sheet `Dialog` mounted once from `app/layout.tsx`. Exports `CUEWEB_REFRESH_NOW_EVENT`, `CUEWEB_FOCUS_SEARCH_EVENT`, and `CUEWEB_OPEN_SHORTCUTS_EVENT` so menu items / pages can subscribe without prop drilling. Fires a `toastSuccess(...)` on every triggered shortcut when `getShortcutNotificationsEnabled()` returns true (read imperatively so the latest pref applies on the next keypress).
- **`FrameViewer`**: Frame log viewer component
diff --git a/docs/_docs/other-guides/cueweb.md b/docs/_docs/other-guides/cueweb.md
index 6e9bd8b99..c3c793f93 100644
--- a/docs/_docs/other-guides/cueweb.md
+++ b/docs/_docs/other-guides/cueweb.md
@@ -173,6 +173,12 @@ CueWeb replicates the core functionality of [CueGUI](https://www.opencue.io/docs
- **Create Show** dialog: enter a unique alphanumeric name and optionally subscribe the new show to one or more allocations (checkbox + Size + Burst per allocation).
- **Show actions** via the row's right-click menu: **Show Properties** (a four-tab dialog - Settings with default max/min cores and comment email, Booking with enable booking / enable dispatch, read-only Statistics, and Raw Show Data) and **Create Subscription...** (subscribe a show to an allocation with Size and Burst).
+31. **Limits (CueCommander → Limits):**
+ - A limits table at `/limits`, the CueWeb equivalent of CueGUI's CueCommander Limits window. Reached from the CueCommander menu / sidebar entry.
+ - Columns: Limit Name, Max Value, Current Running. Auto-refreshes every 30 seconds, with a **Refresh** button for an immediate reload.
+ - **Add Limit** dialog creates a new limit (max value starts at 0).
+ - **Limit actions** via the row's right-click menu: **Edit Max Value** (validates a non-negative integer), **Rename**, and **Delete Limit** (with a confirmation).
+
## CueWeb's user interface
diff --git a/docs/_docs/reference/cueweb.md b/docs/_docs/reference/cueweb.md
index c1f2529c4..b9bfd30e9 100644
--- a/docs/_docs/reference/cueweb.md
+++ b/docs/_docs/reference/cueweb.md
@@ -418,6 +418,22 @@ Clicking a show name opens `/shows/[showName]` (`cueweb/app/shows/[showName]/pag
| **Reparent** | Dragging a group onto another calls `reparentGroups()` → `/api/group/action/reparentgroups` → `job.GroupInterface/ReparentGroups`; dragging a job onto a group calls `reparentJobs()` → `/api/group/action/reparentjobs` → `job.GroupInterface/ReparentJobs`. Drop targets are validated client-side (no self/descendant cycles, no same-parent no-ops), and reparents are serialized one at a time and rolled back on a failed RPC. |
| **Refresh** | The header **Refresh** button remounts the tree to reload groups and jobs. |
+### Limits
+
+A limits table at `/limits` (`cueweb/app/limits/page.tsx`), the CueWeb equivalent of CueGUI's CueCommander Limits window. Reached from **CueCommander → Limits** (header dropdown and sidebar).
+
+
+
+| Behavior | Description |
+|----------|-------------|
+| **Data source** | Loads via `getLimits()` (`app/utils/get_utils.ts`) → `/api/limit/getall` → `limit.LimitInterface/GetAll`. Unlike the host/show `GetAll` responses (which wrap a `*Seq`), `LimitGetAllResponse` nests a single level (`{ limits: [...] }`), so the route unwraps one level. Auto-refreshes every 30s and re-fetches on the `cueweb:limits-changed` event. |
+| **Columns** | Limit Name, Max Value, Current Running (`app/limits/limit-columns.tsx`). Numeric columns sort by their underlying value. |
+| **Table** | Rendered by the shared `SimpleDataTable` with the `isLimitsTable` flag - limit-specific filter/empty-state copy and the `LimitContextMenu`. Column show/hide persists to `localStorage["cueweb.limits.columnVisibility"]`. |
+| **Add Limit** | The header **Add Limit** button opens `limit-add-dialog.tsx`; on OK it calls `createLimit(name, 0)` → `/api/limit/action/create` → `limit.LimitInterface/Create`. |
+| **Row actions** | A right-click `LimitContextMenu` exposes **Edit Max Value**, **Delete Limit**, and **Rename**, opened via the `cueweb:open-limit-edit-max-value` / `cueweb:open-limit-delete` / `cueweb:open-limit-rename` events (`components/ui/limit-action-events.ts`). |
+
+The action helpers (`createLimit` / `deleteLimit` / `renameLimit` / `setLimitMaxValue` in `app/utils/action_utils.ts`) key on the limit **name** (the proto requests take `name` / `old_name`, not an id). `setLimitMaxValue` validates a non-negative integer before calling `SetMaxValue`; on success each dialog fires `cueweb:limits-changed`.
+
### Job-finished notifications
| Behavior | Description |
@@ -1038,6 +1054,8 @@ CueWeb communicates with these REST Gateway endpoints:
| `frame.FrameInterface/Kill` | Kill frame |
| `frame.FrameInterface/Eat` | Eat frame |
| `host.HostInterface/GetHosts` | List hosts for the Monitor Hosts page |
+| `limit.LimitInterface/GetAll` | List limits for the Limits page |
+| `limit.LimitInterface/Create` / `Delete` / `Rename` / `SetMaxValue` | Create / delete / rename a limit, set its max value |
| `facility.AllocationInterface/GetAll` | List allocations (Allocations page + subscription dropdowns) |
| `host.HostInterface/FindHost` | Resolve a single host by name for the host detail page |
| `host.HostInterface/GetProcs` | List the procs running on a host (detail page Procs tab) |
@@ -1069,6 +1087,11 @@ The browser does not call REST Gateway directly; it goes through Next.js API pro
| `POST /api/host/action/removetags` | `host.HostInterface/RemoveTags` (body `{ host, tags }`) |
| `POST /api/show/getactiveshows` | `show.ShowInterface/GetActiveShows` (unwraps `{shows:{shows:[...]}}` to a flat array) |
| `POST /api/allocation/getall` | `facility.AllocationInterface/GetAll` (unwraps `{allocations:{allocations:[...]}}` to a flat array) |
+| `POST /api/limit/getall` | `limit.LimitInterface/GetAll` (unwraps the single-level `{limits:[...]}` to a flat array) |
+| `POST /api/limit/action/create` | `limit.LimitInterface/Create` (body `{ name, max_value }`) |
+| `POST /api/limit/action/delete` | `limit.LimitInterface/Delete` (body `{ name }`) |
+| `POST /api/limit/action/rename` | `limit.LimitInterface/Rename` (body `{ old_name, new_name }`) |
+| `POST /api/limit/action/setmaxvalue` | `limit.LimitInterface/SetMaxValue` (body `{ name, max_value }`, non-negative) |
| `POST /api/show/action/enablebooking` | `show.ShowInterface/EnableBooking` (body `{ show, enabled }`) |
| `POST /api/show/action/enabledispatching` | `show.ShowInterface/EnableDispatching` (body `{ show, enabled }`) |
| `POST /api/show/action/setdefaultmaxcores` | `show.ShowInterface/SetDefaultMaxCores` (body `{ show, max_cores }`) |
@@ -1161,7 +1184,8 @@ Layout, left to right:
- **CueCommander** dropdown (mirrors the CueGUI Views/Plugins menu):
- Allocations (`/allocations`) - implemented; allocations table with
cores/hosts stats (see [Allocations](#allocations)).
- - Limits (`/limits`)
+ - Limits (`/limits`) - implemented; limits table with Add Limit and
+ Edit Max Value / Rename / Delete row actions (see [Limits](#limits)).
- Monitor Cue (`/monitor-cue`)
- Monitor Hosts (`/hosts`) - implemented; host registry with row actions
(lock/unlock, reboot, edit tags) and a per-host detail page (see
diff --git a/docs/_docs/user-guides/cueweb-user-guide.md b/docs/_docs/user-guides/cueweb-user-guide.md
index afdbb89ef..f3738a49a 100644
--- a/docs/_docs/user-guides/cueweb-user-guide.md
+++ b/docs/_docs/user-guides/cueweb-user-guide.md
@@ -1162,6 +1162,48 @@ Clicking a show name (or navigating to `/shows/`) opens the show's **group
---
+## Limits
+
+The **Limits** page (CueCommander → Limits in the sidebar or header) lists the limits configured in Cuebot. It is the CueWeb equivalent of CueGUI's CueCommander Limits window, with an **Add Limit** button and a per-row actions menu.
+
+Open it from the **CueCommander** menu (or the matching entry in the left sidebar).
+
+
+
+The page renders a sortable, filterable table with columns **Limit Name**, **Max Value**, and **Current Running**. Use **Refresh** to reload immediately; the table also auto-refreshes every 30 seconds.
+
+
+
+### Add a limit
+
+Click **Add Limit** and enter a name. The new limit is created with a max value of 0; use **Edit Max Value** afterward to set it.
+
+
+
+A toast confirms the limit was created.
+
+
+
+### Limit row actions
+
+Right-click a limit row to open its actions menu: **Edit Max Value**, **Delete Limit**, and **Rename**.
+
+
+
+**Edit Max Value** opens a dialog to set the limit's max value. The value must be a non-negative integer.
+
+
+
+**Rename** opens a dialog to give the limit a new name.
+
+
+
+**Delete Limit** asks you to confirm before removing the limit.
+
+
+
+---
+
## Keyboard Shortcuts
CueWeb registers a small set of global keyboard shortcuts. Single-letter keys are ignored while typing into a text field, and modifier-key combos (Ctrl / Cmd / Alt) are passed through to the browser, so they will not collide with native shortcuts such as Ctrl+R.
diff --git a/docs/assets/images/cueweb/cueweb_cuecommander_limits.png b/docs/assets/images/cueweb/cueweb_cuecommander_limits.png
new file mode 100644
index 000000000..028d96c6e
Binary files /dev/null and b/docs/assets/images/cueweb/cueweb_cuecommander_limits.png differ
diff --git a/docs/assets/images/cueweb/cueweb_cuecommander_limits_add_limit.png b/docs/assets/images/cueweb/cueweb_cuecommander_limits_add_limit.png
new file mode 100644
index 000000000..7af156a21
Binary files /dev/null and b/docs/assets/images/cueweb/cueweb_cuecommander_limits_add_limit.png differ
diff --git a/docs/assets/images/cueweb/cueweb_cuecommander_limits_add_limit_confirmation.png b/docs/assets/images/cueweb/cueweb_cuecommander_limits_add_limit_confirmation.png
new file mode 100644
index 000000000..95a749b90
Binary files /dev/null and b/docs/assets/images/cueweb/cueweb_cuecommander_limits_add_limit_confirmation.png differ
diff --git a/docs/assets/images/cueweb/cueweb_cuecommander_limits_add_limit_confirmation_dark.png b/docs/assets/images/cueweb/cueweb_cuecommander_limits_add_limit_confirmation_dark.png
new file mode 100644
index 000000000..41feb4d3b
Binary files /dev/null and b/docs/assets/images/cueweb/cueweb_cuecommander_limits_add_limit_confirmation_dark.png differ
diff --git a/docs/assets/images/cueweb/cueweb_cuecommander_limits_add_limit_dark.png b/docs/assets/images/cueweb/cueweb_cuecommander_limits_add_limit_dark.png
new file mode 100644
index 000000000..01f6c01ca
Binary files /dev/null and b/docs/assets/images/cueweb/cueweb_cuecommander_limits_add_limit_dark.png differ
diff --git a/docs/assets/images/cueweb/cueweb_cuecommander_limits_dark.png b/docs/assets/images/cueweb/cueweb_cuecommander_limits_dark.png
new file mode 100644
index 000000000..357f65f19
Binary files /dev/null and b/docs/assets/images/cueweb/cueweb_cuecommander_limits_dark.png differ
diff --git a/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu.png b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu.png
new file mode 100644
index 000000000..352a7a1b1
Binary files /dev/null and b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu.png differ
diff --git a/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_dark.png b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_dark.png
new file mode 100644
index 000000000..c02356dd2
Binary files /dev/null and b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_dark.png differ
diff --git a/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options.png b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options.png
new file mode 100644
index 000000000..c9d3b8004
Binary files /dev/null and b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options.png differ
diff --git a/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_dark.png b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_dark.png
new file mode 100644
index 000000000..bb500fa49
Binary files /dev/null and b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_dark.png differ
diff --git a/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_delete_selected_limit.png b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_delete_selected_limit.png
new file mode 100644
index 000000000..d077a5283
Binary files /dev/null and b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_delete_selected_limit.png differ
diff --git a/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_delete_selected_limit_dark.png b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_delete_selected_limit_dark.png
new file mode 100644
index 000000000..87468f840
Binary files /dev/null and b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_delete_selected_limit_dark.png differ
diff --git a/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_edit_max_value.png b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_edit_max_value.png
new file mode 100644
index 000000000..299ab91c1
Binary files /dev/null and b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_edit_max_value.png differ
diff --git a/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_edit_max_value_dark.png b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_edit_max_value_dark.png
new file mode 100644
index 000000000..150a6f49c
Binary files /dev/null and b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_edit_max_value_dark.png differ
diff --git a/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_rename_a_limit.png b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_rename_a_limit.png
new file mode 100644
index 000000000..4d95bd484
Binary files /dev/null and b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_rename_a_limit.png differ
diff --git a/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_rename_a_limit_dark.png b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_rename_a_limit_dark.png
new file mode 100644
index 000000000..2745a83cf
Binary files /dev/null and b/docs/assets/images/cueweb/cueweb_cuecommander_limits_menu_options_rename_a_limit_dark.png differ