diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx index ad124c888be..8a2cbf5569c 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/AgentTemplateControl.tsx @@ -1217,12 +1217,11 @@ export const AgentTemplateControl = memo(function AgentTemplateControl({ } onCancel={cancelSection} onSave={saveSection} disabled={disabled || !sectionDirty} dirty={sectionDirty} - width={880} + width={mh.advancedDrawerWidth} > } footer={ -
- - {footerNote} - -
- - -
+
+ +
} // The body itself doesn't scroll — the content (a full-height flex row) gives each // panel its own overflow, so the left and right panels scroll independently. - styles={{body: {padding: 16, overflow: "hidden"}}} + // Tighter at the sides; the vertical 16 is what the rail's `bleed` negates. + styles={{body: {padding: "16px 12px", overflow: "hidden"}}} > {children} diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/BuildKitSection.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/BuildKitSection.tsx index a0b73cb8284..07a39d99d06 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/BuildKitSection.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/BuildKitSection.tsx @@ -7,11 +7,9 @@ */ import type {ReactNode} from "react" -import {ConfigAccordionSection, Tag} from "@agenta/ui/components/presentational" +import {Tag} from "@agenta/ui/components/presentational" import {Switch, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@agenta/ui/ui" -import {Warning, Wrench} from "@phosphor-icons/react" - -import {RailField} from "../../../drawers/shared/RailField" +import {Warning} from "@phosphor-icons/react" import type {ItemDescriptor} from "./itemDescriptors" import {ItemRow} from "./ItemRow" @@ -47,8 +45,6 @@ export interface BuildKitSectionProps { onSetAllTools: (next: boolean) => void /** Sandbox permission overlay, rendered read-only as `key → value` rows. */ permissions?: Record | null - /** Collapsed in the app (it is background information); stories open it. @default false */ - defaultOpen?: boolean } /** The build-kit block. Switchable tools get a switch each; the rest, and permissions, are read-only. */ @@ -60,7 +56,6 @@ export function BuildKitSection({ onToggleTool, onSetAllTools, permissions, - defaultOpen = false, }: BuildKitSectionProps) { // Per-tool switches only mean anything while the kit as a whole is on. const toolsDisabled = Boolean(disabled) || !enabled @@ -70,57 +65,46 @@ export function BuildKitSection({ // Nothing to switch means no bulk action — the button would be a dead control. const hasSwitchableTools = tools.some((tool) => tool.toggle) return ( - } - title="Playground build kit" - summary={ - - - Removed on commit +
+
+
+ Playground build kit + +
+ + Tools and permissions the assistant builds with here. None of it reaches the + published agent. - } - extra={ - - } - > - - These playground-only tools and permissions help the assistant build and revise this - agent. None of this is part of the published agent. - +
{!enabled ? (
The assistant can no longer create files, run code, or edit the agent here.
) : null} {tools.length > 0 ? ( - - Tools - - {enabledCount} of {tools.length} enabled - - {hasSwitchableTools ? ( - - ) : null} +
+
+ Tools + + {enabledCount} of {tools.length} enabled - } - > + {hasSwitchableTools ? ( + + ) : null} +
{tools.map(({key, descriptor, toggle}) => ( ))} - +
) : null} {permissions && Object.keys(permissions).length > 0 ? ( - +
+ Sandbox permissions
{Object.entries(permissions).map(([key, value]) => (
))}
- +
) : null} - +
) } diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/ItemRow.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/ItemRow.tsx index 4970c63182d..e834d2cb4a8 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/ItemRow.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/ItemRow.tsx @@ -156,7 +156,7 @@ export function ItemRow({
diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useBuildKit.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useBuildKit.tsx index d5a87f81da2..0c8ef874a87 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useBuildKit.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useBuildKit.tsx @@ -7,6 +7,7 @@ * revision) plus the user's build-kit state — the master on/off and the platform ops switched off * individually — and returns: * - `hasBuildKitOverlay`: whether to render the build-kit block / extend the Advanced section, + * - `buildKitEnabled`: the master on/off, for callers that flag the panel while it is live, * - `buildKitSection`: the drawer block (one tool list — platform tools with a switch each, the * Agenta-owned embeds locked on — plus sandbox permissions) under the master enable switch, * - `permissionOverrideHint`: the inline warning to show above SandboxPermissionControl when the @@ -186,6 +187,8 @@ export function useBuildKit({ return { hasBuildKitOverlay, + // The master on/off, so the Advanced rail can flag the panel while the overlay is live. + buildKitEnabled, buildKitSection, permissionOverrideHint, } diff --git a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx index 3712ce51e9e..1ef2d9f25d0 100644 --- a/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx +++ b/web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx @@ -2,7 +2,7 @@ * useModelHarness — the Model + Advanced sections (the panel's most stateful part). One * hook because the model/connection state feeds both; returns each section's summary + bodies. */ -import {useCallback, useEffect, useMemo, type ReactNode} from "react" +import {useCallback, useEffect, useMemo, useState, type ReactNode} from "react" import { customSecretsAtom, @@ -21,12 +21,13 @@ import {normalizeProviderFamily} from "@agenta/shared/utils" import {ConfigAccordionSection} from "@agenta/ui/components/presentational" import {useDrillInUI} from "@agenta/ui/drill-in" import {SelectLLMProviderBase} from "@agenta/ui/select-llm-provider" -import {Cube, ShieldCheck} from "@phosphor-icons/react" +import {Cube, ShieldCheck, Wrench} from "@phosphor-icons/react" import {atom, useAtomValue, useSetAtom} from "jotai" import {useHasChangedUnder, useRevertUnder} from "../../../drawers/shared/ChangedPathsContext" import {useFocusPaths, useHasFocusUnder} from "../../../drawers/shared/FocusPathsContext" -import {RailField} from "../../../drawers/shared/RailField" +import {FieldLayoutProvider, RailField} from "../../../drawers/shared/RailField" +import {SectionRail, type SectionRailItem} from "../../../drawers/shared/SectionRail" import {ClaudePermissionsControl} from "../ClaudePermissionsControl" import type {PickerSelection} from "../connectionPicker" import { @@ -69,6 +70,13 @@ const vaultLoadedAtom = atom((get) => Array.isArray(get(vaultSecretsQueryAtom).d // Shared with the chat composer's model palette so a hidden harness stays hidden everywhere. +/** One Advanced rail panel: its nav item, an optional header, and the controls it shows. */ +interface AdvancedPanel { + item: SectionRailItem + header?: {title: string; caption: string; extra?: ReactNode} + body: ReactNode +} + export function useModelHarness({ schema, config, @@ -755,12 +763,107 @@ export function useModelHarness({ ) - // The stacked sections carry their own dividers; drop the trailing one on whichever section - // renders last (they're conditional, so target the last child rather than a fixed section). - const advancedDrawerBody = ( + // One rail panel per Advanced group, schema-gated: no group, no rail item and no panel. + const advancedPanels: AdvancedPanel[] = ( + [ + hasPermissionsGroup && { + item: { + value: "permissions", + label: "Permissions", + icon: , + status: permissionsChanged ? ("warning" as const) : undefined, + }, + header: { + title: "Permissions", + caption: "What the agent may do on its own before it must ask.", + extra: revertAction(permissionsChanged ? revertPermissions : null), + }, + body: permissionsBody, + }, + hasExecutionGroup && { + item: { + value: "execution", + label: "Execution", + icon: , + status: sandboxChanged ? ("warning" as const) : undefined, + }, + header: { + title: "Execution environment", + caption: + "Where the agent's tools and code run, and what that sandbox may touch.", + extra: revertAction(revertSandbox), + }, + body: executionBody, + }, + hasBuildKitOverlay && { + item: { + value: "build-kit", + label: "Build kit", + icon: , + }, + // The block carries its own title + enable switch, so it needs no panel header. + body: buildKitSection, + }, + ] as (AdvancedPanel | false)[] + ).filter((panel): panel is AdvancedPanel => Boolean(panel)) + + // Land on the first panel that owns an uncommitted change, else the first one. + const initialAdvancedPanel = + advancedPanels.find((panel) => panel.item.status === "warning" && panel.header)?.item + .value ?? + advancedPanels[0]?.item.value ?? + "" + const [advancedPanelValue, setAdvancedPanelValue] = useState(initialAdvancedPanel) + const activeAdvancedPanel = + advancedPanels.find((panel) => panel.item.value === advancedPanelValue) ?? advancedPanels[0] + + const activeAdvancedPanelBody = ( +
+ {activeAdvancedPanel?.header ? ( +
+
+ + {activeAdvancedPanel.header.title} + + + {activeAdvancedPanel.header.caption} + +
+ {activeAdvancedPanel.header.extra} +
+ ) : null} + {activeAdvancedPanel?.body} +
+ ) + + // One panel needs no nav — a single-item rail is chrome around nothing. + const advancedRailBody = + advancedPanels.length > 1 ? ( + panel.item)} + value={activeAdvancedPanel?.item.value ?? ""} + onChange={setAdvancedPanelValue} + > + {activeAdvancedPanelBody} + + ) : ( + activeAdvancedPanelBody + ) + + // Under a focus filter the body is already narrowed, so it keeps the stack over the rail. + const advancedDrawerBody = focus.active ? (
{advancedControls}
+ ) : ( + +
{advancedRailBody}
+
) return { @@ -779,5 +882,7 @@ export function useModelHarness({ hasAdvanced, advancedSummary, advancedDrawerBody, + // Rail + one panel at a time: no wider than the Model drawer. + advancedDrawerWidth: 560, } } diff --git a/web/packages/agenta-entity-ui/src/drawers/shared/RailField.tsx b/web/packages/agenta-entity-ui/src/drawers/shared/RailField.tsx index 2af8451449f..4349ac9ee4c 100644 --- a/web/packages/agenta-entity-ui/src/drawers/shared/RailField.tsx +++ b/web/packages/agenta-entity-ui/src/drawers/shared/RailField.tsx @@ -9,7 +9,7 @@ * The content is capped at `max-w-prose` so inputs in a section panel keep a readable width instead * of stretching the full drawer. Styling uses antd semantic tokens (`--ag-color*`) only — dark-safe. */ -import type {ReactNode} from "react" +import {createContext, useContext, type ReactNode} from "react" import {cn} from "@agenta/ui/styles" import { @@ -27,6 +27,19 @@ import {ArrowCounterClockwise, Info} from "@phosphor-icons/react" import {useChangedDetail, useChangedPath, useRevertPath} from "./ChangedPathsContext" import {useIsPathVisible} from "./FocusPathsContext" +/** How a {@link RailField} lays its label out: two-column rail, or label above the control. */ +const FieldLayoutContext = createContext<"rail" | "stacked">("rail") + +export function FieldLayoutProvider({ + layout, + children, +}: { + layout: "rail" | "stacked" + children: ReactNode +}) { + return {children} +} + export interface RailFieldProps { label: ReactNode /** Vertical alignment of the label against the content. @default "top" */ @@ -120,12 +133,53 @@ function ChangedDetail({ } export function RailField({label, align = "top", path, wide, children}: RailFieldProps) { + const layout = useContext(FieldLayoutContext) const changed = useChangedPath(path) const detail = useChangedDetail(path) const revert = useRevertPath(path) // Focus filter (see FocusPathsContext): each row self-filters on its own `path`. const visible = useIsPathVisible(path) if (!visible) return null + const stacked = layout === "stacked" + // The change shows as emphasis + a colorInfo dotted underline, so both states share one box. + const labelNode = changed ? ( + + + {/* A real button, so the keyboard can reach the change detail and its Restore. */} + + + {/* antd Popover chrome: elevated panel with 12px inner padding. */} + + + + + ) : ( + label + ) + + // Stacked = the trigger drawers' flat field stack; `align`/`wide` are rail-only knobs. + if (stacked) { + return ( +
+ + {labelNode} + +
{children}
+
+ ) + } + return (
- {/* The label carries the change via emphasis (colorTextSecondary → colorText) plus a - colorInfo dotted underline — no marker glyph, so changed and unchanged rows share - the same box. */} - {changed ? ( - - - {/* role=button makes the Radix aria-haspopup/expanded attrs valid here. */} - - {label} - - - {/* antd Popover chrome: elevated panel with 12px inner padding. */} - - - - - ) : ( - label - )} + {labelNode}
+
{items.map((item) => { const active = item.value === value @@ -62,7 +67,7 @@ export function SectionRail({ variant="ghost" disabled={disabled} onClick={() => onChange(item.value)} - className={`h-8 w-full rounded-md px-2.5 text-xs transition-colors ${ + className={`h-8 w-full rounded-md px-2 text-xs transition-colors ${ item.count != null || item.status ? "flex items-center justify-between" : "justify-start" @@ -74,7 +79,14 @@ export function SectionRail({ : "text-[var(--ag-colorTextSecondary)] hover:bg-[var(--ag-colorFillTertiary)] hover:text-[var(--ag-colorText)] disabled:text-[var(--ag-colorTextSecondary)]" }`} > - {item.label} + + {item.icon ? ( + + {item.icon} + + ) : null} + {item.label} + {item.status ? ( -
+
{children}
diff --git a/web/packages/agenta-entity-ui/src/drawers/shared/index.ts b/web/packages/agenta-entity-ui/src/drawers/shared/index.ts index 3c4b63f92fe..2fc47ed24cb 100644 --- a/web/packages/agenta-entity-ui/src/drawers/shared/index.ts +++ b/web/packages/agenta-entity-ui/src/drawers/shared/index.ts @@ -3,7 +3,7 @@ * config drawers. Re-exported here so app-layer drawers (e.g. the agent-home template setup drawer) * can share the exact same field/rail layout instead of re-implementing it. */ -export {RailField, railInfoLabel, type RailFieldProps} from "./RailField" +export {RailField, FieldLayoutProvider, railInfoLabel, type RailFieldProps} from "./RailField" export {SectionRail, type SectionRailItem, type SectionRailProps} from "./SectionRail" export {DrawerFooter} from "./DrawerFooter" export {RowRemoveButton} from "./RowRemoveButton" diff --git a/web/packages/agenta-entity-ui/src/gatewayTool/components/schemaFormControls.tsx b/web/packages/agenta-entity-ui/src/gatewayTool/components/schemaFormControls.tsx index e6ad866e474..0e98a7e7b89 100644 --- a/web/packages/agenta-entity-ui/src/gatewayTool/components/schemaFormControls.tsx +++ b/web/packages/agenta-entity-ui/src/gatewayTool/components/schemaFormControls.tsx @@ -7,7 +7,7 @@ * Every control takes plain `value`/`onChange`/`disabled` so it works both under antd * `Form.Item` cloning and as an explicitly controlled leaf. */ -import {useId, useRef, useState} from "react" +import {useEffect, useId, useMemo, useRef, useState} from "react" import {dayjs} from "@agenta/shared/utils" import { @@ -19,6 +19,9 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, Input, + Popover, + PopoverAnchor, + PopoverContent, Switch, Select, SelectContent, @@ -234,8 +237,10 @@ export function MultiSelect({ } /** - * Free chip input (antd `Select mode="tags"` with `open={false}`): type + Enter adds a - * chip, Backspace on an empty input removes the last one. + * Free chip input (antd `Select mode="tags"`): type + Enter chips, Backspace on an empty input + * removes the last. `options` suggest; any typed value still lands. + * + * Suggestions use the `Combobox` pattern, not a `` the keyboard can't reach. */ export function ChipsInput({ value, @@ -250,16 +255,49 @@ export function ChipsInput({ placeholder?: string disabled?: boolean id?: string - /** Suggested values, offered as native datalist completions (antd tags-mode `options`). */ + /** Suggested values, offered in a dropdown. Typing a value not in the list still works. */ options?: string[] }) { const [draft, setDraft] = useState("") - const listId = useId() + const [open, setOpen] = useState(false) + const [activeIndex, setActiveIndex] = useState(0) + const boxRef = useRef(null) const inputRef = useRef(null) + const listRef = useRef(null) const selected = value ?? [] - const commit = () => { - const trimmed = draft.trim() + // Stable ids for the input's aria-controls/aria-activedescendant (WAI-ARIA combobox). + const rid = useId() + const listId = `${rid}-listbox` + const optionId = (index: number) => `${rid}-opt-${index}` + + // What's left to suggest: never a value already chipped, narrowed by what's typed so far. + const matches = useMemo(() => { + const taken = value ?? [] + const q = draft.trim().toLowerCase() + return (options ?? []).filter( + (o) => !taken.includes(o) && (!q || o.toLowerCase().includes(q)), + ) + }, [options, value, draft]) + + const hasSuggestions = Boolean(options?.length) + const showList = open && !disabled && matches.length > 0 + + /** Did this dismiss attempt come from the field itself (the anchor), rather than off it? */ + const fromBox = (e: {detail: {originalEvent: Event}}) => { + const target = e.detail.originalEvent.target + return target instanceof Node && Boolean(boxRef.current?.contains(target)) + } + + // A fresh query re-aims at the first row; the old index could point past the new list. + useEffect(() => setActiveIndex(0), [draft, open]) + useEffect(() => { + if (showList) + listRef.current?.querySelector("[data-active=true]")?.scrollIntoView({block: "nearest"}) + }, [activeIndex, showList]) + + const add = (raw: string) => { + const trimmed = raw.trim() setDraft("") if (!trimmed || selected.includes(trimmed)) return onChange?.([...selected, trimmed]) @@ -269,13 +307,24 @@ export function ChipsInput({ onChange?.(next.length ? next : undefined) } - return ( + const box = (
inputRef.current?.focus()} + onMouseDown={(e) => { + if (disabled) return + // Focus by hand: browser focus opens a list the same click dismisses. + if (e.target !== inputRef.current) { + e.preventDefault() + inputRef.current?.focus() + } + if (hasSuggestions) setOpen(true) + }} className={cn( selectTriggerVariants({variant: "default", size: "default"}), MULTI_BOX_CLS, + // The right inset only exists to clear the caret; without suggestions there is none. + !hasSuggestions && "pr-1", "cursor-text", disabled && "cursor-not-allowed bg-disabled-bg text-disabled border-disabled-border", @@ -296,34 +345,100 @@ export function ChipsInput({ variant="ghost" disabled={disabled} aria-label={placeholder} + role={hasSuggestions ? "combobox" : undefined} + aria-expanded={hasSuggestions ? showList : undefined} + aria-controls={hasSuggestions ? listId : undefined} + aria-autocomplete={hasSuggestions ? "list" : undefined} + aria-activedescendant={showList ? optionId(activeIndex) : undefined} placeholder={selected.length === 0 ? placeholder : undefined} value={draft} - onChange={(e) => setDraft(e.target.value)} + onChange={(e) => { + setDraft(e.target.value) + if (hasSuggestions) setOpen(true) + }} + onFocus={() => hasSuggestions && setOpen(true)} onKeyDown={(e) => { - if (e.key === "Enter") { + if (hasSuggestions && (e.key === "ArrowDown" || e.key === "ArrowUp")) { + e.preventDefault() + if (!showList) return setOpen(true) + const dir = e.key === "ArrowDown" ? 1 : -1 + setActiveIndex((i) => (i + dir + matches.length) % matches.length) + } else if (e.key === "Enter") { + e.preventDefault() + add(showList ? (matches[activeIndex] ?? draft) : draft) + setOpen(false) + } else if (e.key === "Escape" && showList) { + // Swallowed, or the drawer hosting the field would close along with the list. e.preventDefault() - commit() + e.stopPropagation() + setOpen(false) } else if (e.key === "Backspace" && draft === "" && selected.length > 0) { removeAt(selected[selected.length - 1]) } }} - onBlur={commit} - list={options?.length ? listId : undefined} - // antd's tags-mode search input is width-auto; an 80px floor pushed the caret - // onto a second line as soon as the chips filled the row. + onBlur={() => { + add(draft) + setOpen(false) + }} + // Width-auto like antd's tags input: a min-width floor wraps the caret. className="h-6 min-w-[4px] flex-1 px-1" /> - {options?.length ? ( - - {options - .filter((o) => !selected.includes(o)) - .map((o) => ( - - ) : null} + {hasSuggestions && ( + + )}
) + + if (!hasSuggestions) return box + + return ( + + {box} + e.preventDefault()} + onCloseAutoFocus={(e) => e.preventDefault()} + // The input is in the ANCHOR, so Radix reads clicks in the field as outside. + onPointerDownOutside={(e) => fromBox(e) && e.preventDefault()} + onFocusOutside={(e) => fromBox(e) && e.preventDefault()} + className="w-[var(--radix-popover-trigger-width)] p-1 font-portal" + > +
+ {matches.map((o, index) => ( +
setActiveIndex(index)} + // Keeps the input focused, so `onBlur` doesn't chip the draft first. + onMouseDown={(e) => e.preventDefault()} + onClick={() => { + add(o) + setOpen(false) + inputRef.current?.focus() + }} + className="box-border flex min-h-control w-full cursor-pointer select-none items-center rounded-control-sm px-3 py-1 text-field-md data-[active=true]:bg-muted" + > + {o} +
+ ))} +
+
+
+ ) } /** diff --git a/web/storybook/stories/entity-ui/BuildKitSection.stories.tsx b/web/storybook/stories/entity-ui/BuildKitSection.stories.tsx index 85296cd0af8..b1990bdd000 100644 --- a/web/storybook/stories/entity-ui/BuildKitSection.stories.tsx +++ b/web/storybook/stories/entity-ui/BuildKitSection.stories.tsx @@ -1,7 +1,5 @@ import {useState} from "react" -import {RailField} from "@agenta/entity-ui/drawers/shared" -import {ConfigAccordionSection} from "@agenta/ui/components/presentational" import {Warning, Wrench} from "@phosphor-icons/react" import type {Meta, StoryObj} from "@storybook/nextjs" import {Switch as AntSwitch, Tag as AntTag, Tooltip as AntTooltip, Typography} from "antd" @@ -28,7 +26,7 @@ import {ItemRow} from "../../../packages/agenta-entity-ui/src/DrillInView/Schema // // The antd half replays the pre-migration markup from // `git show feat/storybook-data-seam:web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useBuildKit.tsx` -// inside the same (already-migrated) `ConfigAccordionSection` / `RailField` / `ItemRow` chrome. +// inside the same (already-migrated) plain-panel / `ItemRow` chrome. const meta = { title: "@agenta/entity-ui/DrillIn/BuildKitSection", component: BuildKitSection, @@ -97,33 +95,33 @@ const AntdBuildKitSection = ({ enabled?: boolean disabled?: boolean }) => ( - } - title="Playground build kit" - summary={ - - - Removed on commit - - } - extra={} - > - - {CAPTION} - +
+
+
+ Playground build kit + +
+ + {CAPTION} + +
{!enabled ? (
{DISABLED_NOTE}
) : null} - +
+ + Platform tools + {PLATFORM_OPS.map((op) => ( ))} - - +
+
+ + Sandbox permissions +
{Object.entries(PERMISSIONS).map(([key, value]) => (
))}
- - +
+
) /** Pre-migration antd markup for the override hint. */ @@ -156,8 +154,6 @@ const BASE = { onToggleTool: () => undefined, onSetAllTools: () => undefined, permissions: PERMISSIONS, - // The app renders this collapsed; every story opens it so the body is visible/measured. - defaultOpen: true, } const Live = ({ @@ -228,13 +224,11 @@ export const PermissionsOnly: Story = { onToggleTool: () => undefined, onSetAllTools: () => undefined, permissions: PERMISSIONS, - defaultOpen: true, }, render: () => (
undefined} tools={[]} onToggleTool={() => undefined}