Files
windmill/frontend/src/lib/components/sidebar/operatorRoutes.ts
T
GuilhemandClaude Opus 5 385086ffc2 feat: show the workspace an operator is in, and let them switch (#11059)
* feat: show the workspace an operator is in, and let them switch

Operators get a single burger button instead of the developer sidebar, so
nothing on screen named the workspace they were working in.

The button now carries the workspace colour as a disc behind the hamburger,
with the workspace name beside it — permanently on home, and on hover
elsewhere. Workspaces without a colour fall back to a neutral disc so the
button always reads as a control against the page behind it.

A "Switch workspace" submenu lists the operator's workspaces, reusing the
developer sidebar's picker: that list moved out of WorkspaceMenu into
WorkspacePickerBody so both render the same rows. "All workspaces" moved from
the operator menu into the bottom of that picker, outside its scroll area so it
stays reachable.

An operator switching workspaces lands on home, since their page access is
granted per workspace and the page they are on may not be theirs to open in the
one they switch into. Developers keep the existing stay-on-the-page behaviour,
as do pickers embedded in a page that drives its own navigation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmXFP3eaPPdZuVwVpLuYRX

* fix: decide the operator landing from positive role signals only

`operator_settings` is NULL both for a non-operator and for an operator whose
workspace never had settings written, so a `false` from `isOperatorInWorkspace`
could not stand as proof of a developer. Lead with `userStore.operator`, an
explicit flag for the workspace being left, and keep the target check only as a
confirmation. Also states the picker's expansion-seeding invariant as what the
host menu actually guarantees: a close and re-open inside its 100ms outro
resumes the same instance rather than a fresh one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmXFP3eaPPdZuVwVpLuYRX

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 16:37:28 +02:00

69 lines
2.9 KiB
TypeScript

import { base } from '$lib/base'
import type { UserWorkspace } from '$lib/stores'
/**
* The pages an operator can reach, each paired with the `operator_settings` key
* that admits them — the operator menu lists only the entries the active
* workspace permits.
*/
export type OperatorMenuLink = {
label: string
/** `operator_settings` key gating the page. */
id: string
href: string
}
export type OperatorTriggerLink = OperatorMenuLink & { kind: string }
/** Home needs no setting — it is the one page every operator always has. */
export const OPERATOR_MAIN_LINKS: OperatorMenuLink[] = [
{ label: 'Home', id: 'home', href: `${base}/` },
{ label: 'Runs', id: 'runs', href: `${base}/runs` },
{ label: 'Schedules', id: 'schedules', href: `${base}/schedules` }
]
export const OPERATOR_SECONDARY_LINKS: OperatorMenuLink[] = [
{ label: 'Resources', id: 'resources', href: `${base}/resources` },
{ label: 'Variables', id: 'variables', href: `${base}/variables` },
{ label: 'Assets', id: 'assets', href: `${base}/assets` },
{ label: 'Groups', id: 'groups', href: `${base}/groups` },
{ label: 'Folders', id: 'folders', href: `${base}/folders` },
{ label: 'Workers', id: 'workers', href: `${base}/workers` },
{ label: 'Audit logs', id: 'audit_logs', href: `${base}/audit_logs` }
]
export const OPERATOR_TRIGGER_LINKS: OperatorTriggerLink[] = [
{ label: 'Custom HTTP routes', id: 'triggers', href: `${base}/routes`, kind: 'http' },
{ label: 'Websocket triggers', id: 'triggers', href: `${base}/websocket_triggers`, kind: 'ws' },
{
label: 'Postgres triggers',
id: 'triggers',
href: `${base}/postgres_triggers`,
kind: 'postgres'
},
{ label: 'Kafka triggers', id: 'triggers', href: `${base}/kafka_triggers`, kind: 'kafka' },
{ label: 'NATS triggers', id: 'triggers', href: `${base}/nats_triggers`, kind: 'nats' },
{ label: 'SQS triggers', id: 'triggers', href: `${base}/sqs_triggers`, kind: 'sqs' },
{ label: 'GCP Pub/Sub triggers', id: 'triggers', href: `${base}/gcp_triggers`, kind: 'gcp' },
{
label: 'Azure Event Grid triggers',
id: 'triggers',
href: `${base}/azure_triggers`,
kind: 'azure'
},
{ label: 'MQTT triggers', id: 'triggers', href: `${base}/mqtt_triggers`, kind: 'mqtt' },
{ label: 'AMQP triggers', id: 'triggers', href: `${base}/amqp_triggers`, kind: 'amqp' },
{ label: 'Email triggers', id: 'triggers', href: `${base}/email_triggers`, kind: 'email' }
]
/**
* Whether the user is an operator in `workspace`. The server nulls
* `operator_settings` for a non-operator, so a non-null value narrows an operator
* correctly; the converse does not hold — the column is itself nullable, so a
* genuine operator whose workspace never had settings written also reads as NULL.
* Only rely on `true`, never on `false` meaning "not an operator".
*/
export function isOperatorInWorkspace(workspace: UserWorkspace | undefined): boolean {
return workspace?.operator_settings != null
}