From e0a33dc3d7e87f3277664c6d01c00acd032804b4 Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Mon, 21 Sep 2026 11:09:56 +0200 Subject: [PATCH] wip: one sidebar for operators and developers, with a detach mode Co-Authored-By: Claude Opus 5 (1M context) --- .../lib/components/copilot/AskAiButton.svelte | 3 +- .../copilot/chat/AiChatLayout.svelte | 8 +- .../components/copilot/chat/global/core.ts | 2 +- .../components/copilot/chat/global/gate.ts | 5 +- .../lib/components/flows/FlowEditor.svelte | 4 +- .../src/lib/components/home/HomeAIChat.svelte | 14 +- .../raw_apps/RawAppTemplatePicker.svelte | 2 +- .../sessions/OpenInSessionButton.svelte | 10 +- .../components/sessions/SessionWrapper.svelte | 2 +- .../sessions/SessionsBetaBanner.svelte | 22 +- .../components/sidebar/OperatorMenu.svelte | 337 -------- .../components/sidebar/SettingsMenu.svelte | 17 +- .../components/sidebar/SidebarContent.svelte | 47 +- .../sidebar/SwitchWorkspaceSubmenu.svelte | 68 -- .../components/sidebar/WorkspaceMenu.svelte | 1 + .../sidebar/WorkspacePickerBody.svelte | 5 +- .../components/sidebar/navDetached.svelte.ts | 20 + .../lib/components/sidebar/operatorRoutes.ts | 65 +- .../components/tutorials/OperatorTour.svelte | 20 +- .../lib/components/tutorials/operatorTour.ts | 2 - .../src/routes/(root)/(logged)/+layout.svelte | 728 +++++++++++------- .../src/routes/(root)/(logged)/+page.svelte | 4 +- .../apps_raw/edit/[...path]/+page.svelte | 2 +- .../(root)/(logged)/sessions/+page.svelte | 25 +- 24 files changed, 571 insertions(+), 842 deletions(-) delete mode 100644 frontend/src/lib/components/sidebar/OperatorMenu.svelte delete mode 100644 frontend/src/lib/components/sidebar/SwitchWorkspaceSubmenu.svelte create mode 100644 frontend/src/lib/components/sidebar/navDetached.svelte.ts diff --git a/frontend/src/lib/components/copilot/AskAiButton.svelte b/frontend/src/lib/components/copilot/AskAiButton.svelte index 95ff47fb52..a141ee3125 100644 --- a/frontend/src/lib/components/copilot/AskAiButton.svelte +++ b/frontend/src/lib/components/copilot/AskAiButton.svelte @@ -5,7 +5,6 @@ import { AIBtnClasses } from './chat/AIButtonStyle' import { prefersSessionHandoff } from './chat/global/gate' import { startSessionWithPrompt } from '$lib/components/sessions/sessionSwitch.svelte' - import { userStore } from '$lib/stores' interface Props { label?: string initialInput?: string @@ -16,7 +15,7 @@ // The label stays short ("Ask AI") for the search bar's inline row; the hover // text is where "new AI session" fits. - const handsOffToSession = $derived(prefersSessionHandoff($userStore?.operator)) + const handsOffToSession = $derived(prefersSessionHandoff()) export function onClick() { // No item to preview here — this carries a question, not a target — so the diff --git a/frontend/src/lib/components/copilot/chat/AiChatLayout.svelte b/frontend/src/lib/components/copilot/chat/AiChatLayout.svelte index 4f3ef368ad..e2b8c9bba9 100644 --- a/frontend/src/lib/components/copilot/chat/AiChatLayout.svelte +++ b/frontend/src/lib/components/copilot/chat/AiChatLayout.svelte @@ -4,7 +4,7 @@ import AiChat from './AIChat.svelte' import SessionsBetaBanner from '$lib/components/sessions/SessionsBetaBanner.svelte' import { zIndexes } from '$lib/zIndexes' - import { userStore, workspaceStore } from '$lib/stores' + import { workspaceStore } from '$lib/stores' import { chatState } from './sharedChatState.svelte' import { loadCopilot } from '$lib/components/copilot/loadCopilot' import { copilotInfo } from '$lib/aiStore' @@ -48,8 +48,8 @@ }: Props = $props() // The desktop rail is fixed-positioned, so the content is offset by a matching - // left padding (in rem, matching the rail). Mobile/operator/borderless: no rail. - let contentPadLeft = $derived(noBorder || $userStore?.operator || isMobile ? 0 : sidebarWidth) + // left padding (in rem, matching the rail). Drawer (mobile or detached)/borderless: no rail. + let contentPadLeft = $derived(noBorder || isMobile ? 0 : sidebarWidth) $effect(() => { chatState.dockedChatAvailable = !disableAi @@ -88,7 +88,7 @@
- · - + + {#if !$userStore?.operator} + · + + {/if} {:else} Try AI Sessions (beta) · diff --git a/frontend/src/lib/components/sidebar/OperatorMenu.svelte b/frontend/src/lib/components/sidebar/OperatorMenu.svelte deleted file mode 100644 index 2d35ce7e10..0000000000 --- a/frontend/src/lib/components/sidebar/OperatorMenu.svelte +++ /dev/null @@ -1,337 +0,0 @@ - - - - {#snippet children({ createMenu })} - (showExtraTriggers = false)} - > - {#snippet triggr({ trigger, pinned })} - {@const iconColor = getContrastTextColor($workspaceColor)} - -
- -
- {/snippet} - {#snippet children({ item, builders })} -
- {#each favoriteLinks ?? [] as favorite (favorite.href)} - - - {#if favorite.kind == 'script'} - - {:else if favorite.kind == 'flow'} - - {:else if favorite.kind == 'app' || favorite.kind == 'raw_app'} - - {:else if favorite.kind == 'asset'} - - {/if} - - - {favorite.label} - - - {/each} -
- - {#each mainMenuLinks as menuLink (menuLink.href ?? menuLink.label)} - - {/each} - -
-
- - - Account settings - - - - - Take the tour - -
- -
- { - if (!document.documentElement.classList.contains('dark')) { - document.documentElement.classList.add('dark') - window.localStorage.setItem('dark-mode', 'dark') - } else { - document.documentElement.classList.remove('dark') - window.localStorage.setItem('dark-mode', 'light') - } - }} - lightMode - class={twMerge( - 'w-full flex gap-3.5 px-2 py-2', - 'transition-colors', - sidebarClasses.text, - 'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary' - )} - {item} - > - {#if darkMode} - - {:else} - - {/if} - Switch theme - - - - {#if $superadmin} - - - Instance settings - - {/if} - - logout()} - class={twMerge( - 'flex flex-row gap-3.5 items-center px-2 py-2 w-full', - 'text-primary text-xs', - 'cursor-pointer', - 'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary' - )} - {item} - > - - Sign out - -
-
- {#snippet renderSecondMenuLinks(menuLinks: OperatorMenuLink[])} - {#each menuLinks as menuLink (menuLink.href ?? menuLink.label)} - - {menuLink.label} - - {/each} - {/snippet} - {#if secondMenuLinks.length || secondMenuTriggerLinks.length || extraTriggerLinks.length} -
- {#if secondMenuLinks.length}
{@render renderSecondMenuLinks(secondMenuLinks)}
{/if} - {#if secondMenuTriggerLinks.length}
{@render renderSecondMenuLinks(secondMenuTriggerLinks)}
{/if} - {#if extraTriggerLinks.length}
- -
{ - // This row expands the list below it instead of acting on the selection, and - // melt keeps the menu open only for a click it sees as defaultPrevented. - // Svelte delegates onclick to the root, which runs after melt's own listener, - // and a capture listener on the item itself would be ordered only by - // registration, so an ancestor's capture phase is what reliably wins. - e.preventDefault() - showExtraTriggers = !showExtraTriggers - }} - > - - - More triggers - -
- {#if showExtraTriggers} - {#each extraTriggerLinks as menuLink (menuLink.href)} - - {menuLink.label} - - {/each} - {/if} -
{/if} -
- {/if} - {#if $enterpriseLicense} - - {/if} -
-
- {/snippet} -
- {/snippet} -
- - diff --git a/frontend/src/lib/components/sidebar/SettingsMenu.svelte b/frontend/src/lib/components/sidebar/SettingsMenu.svelte index e0951f37cd..fed773c84c 100644 --- a/frontend/src/lib/components/sidebar/SettingsMenu.svelte +++ b/frontend/src/lib/components/sidebar/SettingsMenu.svelte @@ -18,7 +18,8 @@ Crown, Gauge, Trash2, - KeyRound + KeyRound, + GraduationCap } from 'lucide-svelte' import { base } from '$app/paths' import { goto } from '$lib/navigation' @@ -38,6 +39,8 @@ import { USER_SETTINGS_HASH, SUPERADMIN_SETTINGS_HASH } from './settings' import { accountSetup } from './accountSetup.svelte' import { EXECUTIONS_HINT } from './executionsHint' + import { sidebarPageAllowed } from './operatorRoutes' + import { TOUR_PARAM, TOUR_PARAM_VALUE } from '$lib/components/tutorials/operatorTour' import { userWorkspaces, workspaceStore, @@ -248,6 +251,15 @@ icon: Settings, action: () => goto(USER_SETTINGS_HASH) }, + ...($userStore?.operator + ? [ + { + displayName: 'Take the tour', + icon: GraduationCap, + action: () => goto(`${base}/?${TOUR_PARAM}=${TOUR_PARAM_VALUE}`) + } + ] + : []), ...(cloudHosted && $isPremiumStore === false ? [ { @@ -307,6 +319,7 @@ label="Workers" href="{base}/workers" icon={ServerCog} + disabled={!sidebarPageAllowed($userStore?.operator, currentWs, 'workers')} {isCollapsed} aiId="sidebar-menu-link-workers" aiDescription="Button to navigate to workers" @@ -352,7 +365,7 @@ label="Audit logs" href="{base}/audit_logs" icon={Eye} - disabled={$userStore?.operator} + disabled={!sidebarPageAllowed($userStore?.operator, currentWs, 'audit_logs')} {isCollapsed} aiId="sidebar-menu-link-audit-logs" aiDescription="Button to navigate to audit logs" diff --git a/frontend/src/lib/components/sidebar/SidebarContent.svelte b/frontend/src/lib/components/sidebar/SidebarContent.svelte index b43bc2988e..075e0f9018 100644 --- a/frontend/src/lib/components/sidebar/SidebarContent.svelte +++ b/frontend/src/lib/components/sidebar/SidebarContent.svelte @@ -72,6 +72,7 @@ import GoogleCloudIcon from '../icons/triggers/GoogleCloudIcon.svelte' import AzureIcon from '../icons/triggers/AzureIcon.svelte' import { leaveCurrentWorkspace } from './leaveWorkspace' + import { sidebarPageAllowed, type OperatorPageKey } from './operatorRoutes' import { markChangelogsOpened, readRecentChangelogs } from './changelogs' const { recent: recentChangelogs, hasNew } = readRecentChangelogs() @@ -188,6 +189,11 @@ return count } + const currentWs = $derived($userWorkspaces?.find((w) => w.id === $workspaceStore)) + function allows(key: OperatorPageKey) { + return sidebarPageAllowed($userStore?.operator, currentWs, key) + } + const itemClass = twMerge( 'text-secondary font-normal w-full block px-4 py-2 text-2xs data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary' ) @@ -205,6 +211,7 @@ label: 'Runs', href: `${base}/runs`, icon: Play, + disabled: !allows('runs'), aiId: 'sidebar-menu-link-runs', aiDescription: 'Button to navigate to runs', onclick: () => { @@ -217,7 +224,7 @@ label: 'Variables', href: `${base}/variables`, icon: DollarSign, - disabled: $userStore?.operator, + disabled: !allows('variables'), aiId: 'sidebar-menu-link-variables', aiDescription: 'Button to navigate to variables' }, @@ -225,7 +232,7 @@ label: 'Resources', href: `${base}/resources`, icon: Boxes, - disabled: $userStore?.operator, + disabled: !allows('resources'), aiId: 'sidebar-menu-link-resources', aiDescription: 'Button to navigate to resources' }, @@ -233,6 +240,7 @@ label: 'Assets', href: `${base}/assets`, icon: Pyramid, + disabled: !allows('assets'), aiId: 'sidebar-menu-link-assets', aiDescription: 'Button to navigate to assets' }, @@ -240,7 +248,7 @@ label: 'Folders', href: `${base}/folders`, icon: FolderOpen, - disabled: $userStore?.operator, + disabled: !allows('folders'), aiId: 'sidebar-menu-link-folders', aiDescription: 'Button to navigate to folders' }, @@ -248,7 +256,7 @@ label: 'Groups', href: `${base}/groups`, icon: Users, - disabled: $userStore?.operator, + disabled: !allows('groups'), aiId: 'sidebar-menu-link-groups', aiDescription: 'Button to navigate to groups' } @@ -259,7 +267,7 @@ label: 'HTTP', href: '/routes', icon: Route, - disabled: $userStore?.operator, + disabled: !allows('triggers'), kind: 'http', aiId: 'sidebar-menu-link-http', aiDescription: 'Button to navigate to HTTP routes' @@ -268,7 +276,7 @@ label: 'WebSockets', href: '/websocket_triggers', icon: Unplug, - disabled: $userStore?.operator, + disabled: !allows('triggers'), kind: 'ws', aiId: 'sidebar-menu-link-ws', aiDescription: 'Button to navigate to websocket triggers' @@ -277,7 +285,7 @@ label: 'Postgres', href: '/postgres_triggers', icon: Database, - disabled: $userStore?.operator, + disabled: !allows('triggers'), kind: 'postgres', aiId: 'sidebar-menu-link-postgres', aiDescription: 'Button to navigate to Postgres triggers' @@ -286,7 +294,7 @@ label: 'Kafka' + ($enterpriseLicense ? '' : ' (EE)'), href: '/kafka_triggers', icon: KafkaIcon, - disabled: $userStore?.operator || !$enterpriseLicense, + disabled: !allows('triggers') || !$enterpriseLicense, kind: 'kafka', aiId: 'sidebar-menu-link-kafka', aiDescription: 'Button to navigate to Kafka triggers' @@ -295,7 +303,7 @@ label: 'NATS' + ($enterpriseLicense ? '' : ' (EE)'), href: '/nats_triggers', icon: NatsIcon, - disabled: $userStore?.operator || !$enterpriseLicense, + disabled: !allows('triggers') || !$enterpriseLicense, kind: 'nats', aiId: 'sidebar-menu-link-nats', aiDescription: 'Button to navigate to NATS triggers' @@ -304,7 +312,7 @@ label: 'SQS' + ($enterpriseLicense ? '' : ' (EE)'), href: '/sqs_triggers', icon: AwsIcon, - disabled: $userStore?.operator || !$enterpriseLicense, + disabled: !allows('triggers') || !$enterpriseLicense, kind: 'sqs', aiId: 'sidebar-menu-link-sqs', aiDescription: 'Button to navigate to SQS triggers' @@ -313,7 +321,7 @@ label: 'GCP Pub/Sub' + ($enterpriseLicense ? '' : ' (EE)'), href: '/gcp_triggers', icon: GoogleCloudIcon, - disabled: $userStore?.operator || !$enterpriseLicense, + disabled: !allows('triggers') || !$enterpriseLicense, kind: 'gcp', aiId: 'sidebar-menu-link-gcp', aiDescription: 'Button to navigate to GCP Pub/Sub triggers' @@ -322,7 +330,7 @@ label: 'Azure Event Grid' + ($enterpriseLicense ? '' : ' (EE)'), href: '/azure_triggers', icon: AzureIcon, - disabled: $userStore?.operator || !$enterpriseLicense, + disabled: !allows('triggers') || !$enterpriseLicense, kind: 'azure', aiId: 'sidebar-menu-link-azure', aiDescription: 'Button to navigate to Azure Event Grid triggers' @@ -331,7 +339,7 @@ label: 'MQTT', href: '/mqtt_triggers', icon: MqttIcon, - disabled: $userStore?.operator, + disabled: !allows('triggers'), kind: 'mqtt', aiId: 'sidebar-menu-link-mqtt', aiDescription: 'Button to navigate to MQTT triggers' @@ -340,7 +348,7 @@ label: 'AMQP', href: '/amqp_triggers', icon: AmqpIcon, - disabled: $userStore?.operator, + disabled: !allows('triggers'), kind: 'amqp', aiId: 'sidebar-menu-link-amqp', aiDescription: 'Button to navigate to AMQP triggers' @@ -349,7 +357,7 @@ label: 'Email', href: '/email_triggers', icon: MailIcon, - disabled: $userStore?.operator, + disabled: !allows('triggers'), kind: 'email', aiId: 'sidebar-menu-link-email', aiDescription: 'Button to navigate to Email triggers' @@ -375,7 +383,7 @@ label: 'Schedules', href: `${base}/schedules`, icon: Calendar, - disabled: !SIDEBAR_SHOW_SCHEDULES || $userStore?.operator, + disabled: !SIDEBAR_SHOW_SCHEDULES || !allows('schedules'), aiId: 'sidebar-menu-link-schedules', aiDescription: 'Button to navigate to schedules' }, @@ -390,7 +398,6 @@ ) // Admins, superadmins, and fork creators reach workspace settings (the latter // for the fork members screen; see isForkOwner / backend authorize_fork_owner_add_user). - const currentWs = $derived($userWorkspaces?.find((w) => w.id === $workspaceStore)) const canManageWorkspace = $derived( $userStore?.is_admin || $superadmin || isForkOwner(currentWs, $userStore?.email) ) @@ -460,7 +467,7 @@ label: 'Workers', href: `${base}/workers`, icon: ServerCog, - disabled: $userStore?.operator, + disabled: !allows('workers'), aiId: 'sidebar-menu-link-workers', aiDescription: 'Button to navigate to workers' }, @@ -509,7 +516,7 @@ label: 'Audit logs', href: `${base}/audit_logs`, icon: Eye, - disabled: $userStore?.operator, + disabled: !allows('audit_logs'), aiId: 'sidebar-menu-link-audit-logs', aiDescription: 'Button to navigate to audit logs' } @@ -551,7 +558,7 @@ {#each triggerMenuLinks as menuLink (menuLink.href ?? menuLink.label)} {/each} - {#if extraTriggerLinks.length > 0 && !$userStore?.operator} + {#if extraTriggerLinks.length > 0 && allows('triggers')} {#snippet triggr({ trigger })} - import { untrack } from 'svelte' - import { melt } from '@melt-ui/svelte' - import type { MenubarMenuBuilders, MenubarMenuElements } from '@melt-ui/svelte' - import { Building, ChevronRight } from 'lucide-svelte' - import { twMerge } from 'tailwind-merge' - import { sidebarClasses } from './MenuButton.svelte' - import WorkspacePickerBody from './WorkspacePickerBody.svelte' - import { MenuItem } from '$lib/components/meltComponents' - import { clearWorkspaceFromStorage } from '$lib/stores' - import { base } from '$lib/base' - - interface Props { - // Submenu builders from the enclosing melt Menu — createSubmenu must be - // called against this specific menu instance (same pattern as DropdownSubmenuItem). - builders: MenubarMenuBuilders - // Melt uses the parent menu's item builder for submenu rows too, so the - // picker body's MenuItems work here unchanged. - item: MenubarMenuElements['item'] - } - - let { builders, item }: Props = $props() - - const { - elements: { subTrigger, subMenu }, - states: { subOpen } - } = untrack(() => builders).createSubmenu() - - - - -{#if $subOpen} -
- - -
- clearWorkspaceFromStorage()} - class={twMerge( - 'w-full flex flex-row gap-2 px-4 py-2 font-normal', - sidebarClasses.text, - 'hover:bg-surface-hover hover:text-primary', - 'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary' - )} - {item} - > - All workspaces - -
-
-{/if} diff --git a/frontend/src/lib/components/sidebar/WorkspaceMenu.svelte b/frontend/src/lib/components/sidebar/WorkspaceMenu.svelte index ddcbd9ca95..84e03819f4 100644 --- a/frontend/src/lib/components/sidebar/WorkspaceMenu.svelte +++ b/frontend/src/lib/components/sidebar/WorkspaceMenu.svelte @@ -55,6 +55,7 @@ showChevron emphasizeLabel disableTitle + labelClass="wm-workspace-name" {trigger} /> diff --git a/frontend/src/lib/components/sidebar/WorkspacePickerBody.svelte b/frontend/src/lib/components/sidebar/WorkspacePickerBody.svelte index fa4468fa6a..58fdbe9eae 100644 --- a/frontend/src/lib/components/sidebar/WorkspacePickerBody.svelte +++ b/frontend/src/lib/components/sidebar/WorkspacePickerBody.svelte @@ -50,8 +50,7 @@ strictWorkspaceSelect?: boolean // Set by a picker embedded in a page that drives its own navigation, so switching // workspaces must not navigate away from it. Separate from strictWorkspaceSelect, - // which only strips the sidebar-specific rows — the operator submenu sets that one - // and still wants the switch to move the page. + // which only strips the sidebar-specific rows. keepPageOnSwitch?: boolean } @@ -117,7 +116,7 @@ // scope header's WorkspaceFamilyPicker remains the primary fork surface. // // strictWorkspaceSelect is used where this list has no scope header beside it - // (standalone pages such as svix webhook creation, the operator menu), so + // (standalone pages such as svix webhook creation), so // there forks must stay directly selectable — list the full hierarchy // unconditionally. const hierarchy = $derived($userWorkspaces ? buildWorkspaceHierarchy($userWorkspaces) : []) diff --git a/frontend/src/lib/components/sidebar/navDetached.svelte.ts b/frontend/src/lib/components/sidebar/navDetached.svelte.ts new file mode 100644 index 0000000000..a5213b0336 --- /dev/null +++ b/frontend/src/lib/components/sidebar/navDetached.svelte.ts @@ -0,0 +1,20 @@ +import { fromStore } from 'svelte/store' +import { userStore } from '$lib/stores' +import { useLocalStorageValue } from '$lib/svelte5Utils.svelte' + +// Empty until the user presses detach or attach once. Until then operators start detached: +// many of them navigate through a workspace's own app, and a docked sidebar would take its width. +const pref = useLocalStorageValue('nav_detached', '', 'string') +const user = fromStore(userStore) + +/** Whether the sidebar is detached: hidden behind a floating button that slides it in over the page. */ +export const navDetached = { + get val(): boolean { + if (pref.val === 'true') return true + if (pref.val === 'false') return false + return !!user.current?.operator + }, + set val(detached: boolean) { + pref.val = String(detached) + } +} diff --git a/frontend/src/lib/components/sidebar/operatorRoutes.ts b/frontend/src/lib/components/sidebar/operatorRoutes.ts index 1abd7aeb93..7898eaf7ed 100644 --- a/frontend/src/lib/components/sidebar/operatorRoutes.ts +++ b/frontend/src/lib/components/sidebar/operatorRoutes.ts @@ -1,61 +1,22 @@ -import { base } from '$lib/base' +import type { OperatorSettings } from '$lib/gen' import type { UserWorkspace } from '$lib/stores' +/** An `operator_settings` key: each admits an operator to one page (`triggers` to all of them). */ +export type OperatorPageKey = keyof OperatorSettings + /** - * 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. + * Whether the sidebar shows the page behind `key`. Everyone but an operator sees every page + * (other gates, such as admin-only entries, still apply); an operator sees only the pages the + * workspace's `operator_settings` turn on. */ -export type OperatorMenuLink = { - label: string - /** `operator_settings` key gating the page. */ - id: string - href: string +export function sidebarPageAllowed( + isOperator: boolean | undefined, + workspace: UserWorkspace | undefined, + key: OperatorPageKey +): boolean { + return !isOperator || workspace?.operator_settings?.[key] === true } -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 diff --git a/frontend/src/lib/components/tutorials/OperatorTour.svelte b/frontend/src/lib/components/tutorials/OperatorTour.svelte index cc7f4ab4a5..51574ab96e 100644 --- a/frontend/src/lib/components/tutorials/OperatorTour.svelte +++ b/frontend/src/lib/components/tutorials/OperatorTour.svelte @@ -1,8 +1,7 @@