mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-12 16:02:56 +00:00
fix: pin holds the first slot, and the nav table says what it shares
Pinning brings the sidebar's handle back, and from behind it the unpin button sat one slot over from where the pin had just been clicked — so a second click in the same place opened the sidebar instead. An operator is detached by default, which makes that the default state of the feature rather than an edge case. `NAV_PAGES` claimed one table stops a page being called one thing in the nav and another in the header, which holds for the seven entries the sidebar's main group is built from and not for the four grouped separately — `/routes` was "Triggers" in the header and "HTTP" in the nav. The comment now says which is which, and that entry is named for the page rather than its group. `sidebarPageAllowed` gets a table test: it decides what an operator sees, and its `=== true` is what a future `Option`-style default would silently change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f2768de00c
commit
379858a97d
@@ -92,6 +92,36 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
'shadow-[inset_0_-1px_0_0_rgb(var(--color-border-light))] dark:shadow-[inset_0_-1px_0_0_#374151] [html.github-dark_&]:shadow-[inset_0_-1px_0_0_rgb(var(--color-border-light))]'
|
||||
)}
|
||||
>
|
||||
<!-- Pin before the sidebar's handle, so it holds the first slot whether or not the handle is
|
||||
there. Pinning brings the handle back — the band is no longer hidden — and from behind the
|
||||
handle the unpin button would sit one slot over from where the pin was just clicked, which
|
||||
for an operator (detached by default) means the second click opens the sidebar instead. -->
|
||||
{#if onPin}
|
||||
<!-- Same box and offsets as the corner handle it comes to rest over, so the pointer that
|
||||
called the band down is already on this button. -->
|
||||
<button
|
||||
class="flex items-center p-1.5 rounded hover:bg-surface-hover"
|
||||
aria-label="Pin the header on deployed apps"
|
||||
title="Pin the header on deployed apps"
|
||||
onclick={() => onPin?.()}
|
||||
>
|
||||
<PanelTop size={16} class="flex-shrink-0 text-hint" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if onUnpin}
|
||||
<button
|
||||
class="flex items-center p-1.5 rounded hover:bg-surface-hover"
|
||||
aria-label="Unpin the header from deployed apps"
|
||||
title="Unpin the header from deployed apps"
|
||||
onclick={() => onUnpin?.()}
|
||||
>
|
||||
<!-- Dashed while the band is shown, solid while it is away: the pair the sidebar's own
|
||||
toggle uses, so the two controls read as one idea. -->
|
||||
<PanelTopDashed size={16} class="flex-shrink-0 text-hint" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if (navDetached.val || panelled) && !navHidden && !hideNavHandle}
|
||||
<!-- Reveals the hidden sidebar, and only that: hovering slides the card in, clicking holds
|
||||
it there. Attaching it for good belongs to the toggle in the sidebar's own footer, where
|
||||
@@ -115,33 +145,6 @@ The row's height matches the sidebar's own header row, so the two read as one ba
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if onPin}
|
||||
<!-- Same box and offsets as the corner handle it comes to rest over, so the pointer that
|
||||
called the band down is already on this button. -->
|
||||
<button
|
||||
class="flex items-center p-1.5 rounded hover:bg-surface-hover"
|
||||
aria-label="Pin the header on deployed apps"
|
||||
title="Pin the header on deployed apps"
|
||||
onclick={() => onPin?.()}
|
||||
>
|
||||
<PanelTop size={16} class="flex-shrink-0 text-hint" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if onUnpin}
|
||||
<!-- After the sidebar's handle: the sidebar is the outer thing, the band sits inside it. -->
|
||||
<button
|
||||
class="flex items-center p-1.5 rounded hover:bg-surface-hover"
|
||||
aria-label="Unpin the header from deployed apps"
|
||||
title="Unpin the header from deployed apps"
|
||||
onclick={() => onUnpin?.()}
|
||||
>
|
||||
<!-- Dashed while the band is shown, solid while it is away: the pair the sidebar's own
|
||||
toggle uses, so the two controls read as one idea. -->
|
||||
<PanelTopDashed size={16} class="flex-shrink-0 text-hint" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- The breadcrumb yields width grudgingly (shrink-[0.1]): when a page fills the bar with
|
||||
controls, they are what should narrow, not the name of where the user is. It still gives
|
||||
way rather than pushing them off the bar once there is nothing left to take.
|
||||
|
||||
@@ -14,8 +14,13 @@ import {
|
||||
import type { OperatorPageKey } from './operatorRoutes'
|
||||
|
||||
/**
|
||||
* The workspace pages the sidebar links to, and the names the page header's breadcrumb gives them.
|
||||
* One table so a page cannot be called one thing in the nav and another in the header.
|
||||
* The names the page header's breadcrumb gives the workspace's own pages, and the rows the
|
||||
* sidebar's main group is built from.
|
||||
*
|
||||
* Every entry names a page for the breadcrumb. Only the entries `SidebarContent`'s
|
||||
* `MAIN_MENU_EXTRAS` also lists become sidebar links from here — Schedules, HTTP routes, Workers
|
||||
* and Audit logs are grouped differently there and carry their own row, so for those four the
|
||||
* `icon` and `operatorKey` below go unread. Keep the labels in step with those rows by hand.
|
||||
*/
|
||||
export type NavPage = {
|
||||
label: string
|
||||
@@ -35,7 +40,8 @@ export const NAV_PAGES: NavPage[] = [
|
||||
{ label: 'Folders', path: '/folders', icon: FolderOpen, operatorKey: 'folders' },
|
||||
{ label: 'Groups', path: '/groups', icon: Users, operatorKey: 'groups' },
|
||||
{ label: 'Schedules', path: '/schedules', icon: Calendar, operatorKey: 'schedules' },
|
||||
{ label: 'Triggers', path: '/routes', icon: Unplug, operatorKey: 'triggers' },
|
||||
// "Triggers" is the group the sidebar files this under, not the page: the row there reads HTTP.
|
||||
{ label: 'HTTP routes', path: '/routes', icon: Unplug, operatorKey: 'triggers' },
|
||||
{ label: 'Workers', path: '/workers', icon: HardHat, operatorKey: 'workers' },
|
||||
{ label: 'Audit logs', path: '/audit_logs', icon: Eye, operatorKey: 'audit_logs' }
|
||||
]
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { sidebarPageAllowed } from './operatorRoutes'
|
||||
import type { UserWorkspace } from '$lib/stores'
|
||||
|
||||
const ws = (operator_settings: any): UserWorkspace =>
|
||||
({ id: 'w', name: 'w', username: 'u', operator_settings }) as UserWorkspace
|
||||
|
||||
describe('sidebarPageAllowed', () => {
|
||||
it('shows every page to anyone who is not an operator', () => {
|
||||
expect(sidebarPageAllowed(false, ws(null), 'runs')).toBe(true)
|
||||
expect(sidebarPageAllowed(undefined, undefined, 'runs')).toBe(true)
|
||||
})
|
||||
|
||||
it('admits an operator only where the workspace turned the page on', () => {
|
||||
expect(sidebarPageAllowed(true, ws({ runs: true }), 'runs')).toBe(true)
|
||||
expect(sidebarPageAllowed(true, ws({ runs: false }), 'runs')).toBe(false)
|
||||
})
|
||||
|
||||
it('withholds a page the settings do not mention', () => {
|
||||
// `=== true`, not truthiness: a right absent from the jsonb, or settings that were never
|
||||
// written, must not read as granted.
|
||||
expect(sidebarPageAllowed(true, ws({ variables: true }), 'runs')).toBe(false)
|
||||
expect(sidebarPageAllowed(true, ws(null), 'runs')).toBe(false)
|
||||
expect(sidebarPageAllowed(true, undefined, 'runs')).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user