Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 86 additions & 22 deletions browser/ui/src/configuration/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
* unsaved-change guard; this component only edits the JSON draft.
*/

import { type ConfigFormProps, Input, type JsonValue, StatusPanel } from '@iii-dev/console-ui'
import {
type ConfigFormProps,
Input,
type JsonValue,
StatusPanel,
} from '@iii-dev/console-ui'
import { type ReactNode, useEffect, useRef, useState } from 'react'
import { ChevronLeftIcon, GlobeIcon, useContainerNarrow } from '../lib/widgets'

Expand Down Expand Up @@ -49,7 +54,9 @@ const FIELD_SECTION: Record<string, SectionId> = {
}

function asObject(value: JsonValue | undefined): JsonObject {
return value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {}
return value && typeof value === 'object' && !Array.isArray(value)
? { ...value }
: {}
}

function stringValue(value: JsonValue | undefined, fallback = ''): string {
Expand All @@ -60,7 +67,10 @@ function numberValue(value: JsonValue | undefined, fallback: number): number {
return typeof value === 'number' ? value : fallback
}

function booleanValue(value: JsonValue | undefined, fallback: boolean): boolean {
function booleanValue(
value: JsonValue | undefined,
fallback: boolean,
): boolean {
return typeof value === 'boolean' ? value : fallback
}

Expand All @@ -70,7 +80,12 @@ function pointer(field: string) {

function fieldError(errors: ConfigFormProps['errors'], field: string) {
const base = pointer(field)
return errors?.get(base) ?? [...(errors?.entries() ?? [])].find(([path]) => path.startsWith(`${base}/`))?.[1]
return (
errors?.get(base) ??
[...(errors?.entries() ?? [])].find(([path]) =>
path.startsWith(`${base}/`),
)?.[1]
)
}

function formatCount(value: number) {
Expand Down Expand Up @@ -131,7 +146,11 @@ function TextField({
}) {
const id = `br-cfg-${field}`
return (
<Field label={<label htmlFor={id}>{label}</label>} hint={hint} error={error}>
<Field
label={<label htmlFor={id}>{label}</label>}
hint={hint}
error={error}
>
<Input
id={id}
name={field}
Expand Down Expand Up @@ -171,7 +190,11 @@ function NumberField({
}) {
const id = `br-cfg-${field}`
return (
<Field label={<label htmlFor={id}>{label}</label>} hint={hint} error={error}>
<Field
label={<label htmlFor={id}>{label}</label>}
hint={hint}
error={error}
>
<Input
id={id}
name={field}
Expand Down Expand Up @@ -247,7 +270,9 @@ function SchemesField({

return (
<Field
label={<label htmlFor="br-cfg-allowed_schemes">Allowed URL schemes</label>}
label={
<label htmlFor="br-cfg-allowed_schemes">Allowed URL schemes</label>
}
hint="Enter a comma-separated list without ://. Keep this list as narrow as your workflows allow."
error={error}
>
Expand All @@ -274,7 +299,13 @@ function SchemesField({
)
}

function SectionHeader({ title, description }: { title: string; description: string }) {
function SectionHeader({
title,
description,
}: {
title: string
description: string
}) {
return (
<div className="br-cfg-section-head">
<div>
Expand All @@ -298,9 +329,18 @@ function ConfigNav({
const height = numberValue(value.viewport_height, DEFAULTS.viewport_height)
const maxSessions = numberValue(value.max_sessions, DEFAULTS.max_sessions)
const headless = booleanValue(value.headless, DEFAULTS.headless)
const consoleBuffer = numberValue(value.console_buffer, DEFAULTS.console_buffer)
const networkBuffer = numberValue(value.network_buffer, DEFAULTS.network_buffer)
const timeout = numberValue(value.default_timeout_ms, DEFAULTS.default_timeout_ms)
const consoleBuffer = numberValue(
value.console_buffer,
DEFAULTS.console_buffer,
)
const networkBuffer = numberValue(
value.network_buffer,
DEFAULTS.network_buffer,
)
const timeout = numberValue(
value.default_timeout_ms,
DEFAULTS.default_timeout_ms,
)
const idle = numberValue(value.idle_stop_ms, DEFAULTS.idle_stop_ms)

const sections: Array<{
Expand Down Expand Up @@ -338,7 +378,7 @@ function ConfigNav({
return (
<nav className="br-cfg-nav" aria-label="Browser configuration sections">
<div className="br-cfg-nav-head">
<p className="br-cfg-nav-label">browser settings</p>
<p className="br-cfg-nav-label">Browser settings</p>
<p>Settings are grouped by when and where they apply.</p>
</div>
<ul className="br-cfg-nav-list">
Expand All @@ -354,7 +394,9 @@ function ConfigNav({
>
<span className="br-cfg-nav-copy">
<span className="br-cfg-nav-name">{section.label}</span>
<span className="br-cfg-nav-description">{section.description}</span>
<span className="br-cfg-nav-description">
{section.description}
</span>
<span className="br-cfg-nav-meta">{section.summary}</span>
</span>
<ChevronLeftIcon className="br-cfg-nav-chevron" />
Expand Down Expand Up @@ -385,7 +427,12 @@ function EditorHeader({
return (
<header className="br-cfg-editor-head">
{narrow ? (
<button type="button" className="br-cfg-back" onClick={onBack} aria-label="Back to configuration sections">
<button
type="button"
className="br-cfg-back"
onClick={onBack}
aria-label="Back to configuration sections"
>
<ChevronLeftIcon />
</button>
) : null}
Expand Down Expand Up @@ -510,13 +557,17 @@ function ConfigEditor({
field="allow_attach"
label="Allow attaching to existing browsers"
hint="Attached tabs can access the real browser profile and its signed-in sessions."
checked={booleanValue(value.allow_attach, DEFAULTS.allow_attach)}
checked={booleanValue(
value.allow_attach,
DEFAULTS.allow_attach,
)}
onChange={(next) => setBoolean('allow_attach', next)}
/>
</div>
{booleanValue(value.allow_attach, DEFAULTS.allow_attach) ? (
<div className="br-cfg-warning" role="note">
Attach mode is enabled. Only connect to browser instances you trust.
Attach mode is enabled. Only connect to browser instances you
trust.
</div>
) : null}
</section>
Expand Down Expand Up @@ -558,8 +609,12 @@ function ConfigEditor({
}}
>
<span>
{numberValue(value.viewport_width, DEFAULTS.viewport_width)} ×{' '}
{numberValue(value.viewport_height, DEFAULTS.viewport_height)}
{numberValue(value.viewport_width, DEFAULTS.viewport_width)}{' '}
×{' '}
{numberValue(
value.viewport_height,
DEFAULTS.viewport_height,
)}
</span>
</div>
<p>Aspect-ratio preview for newly launched sessions.</p>
Expand Down Expand Up @@ -676,11 +731,16 @@ function ConfigEditor({
<SchemesField
value={
Array.isArray(value.allowed_schemes)
? value.allowed_schemes.filter((scheme): scheme is string => typeof scheme === 'string')
? value.allowed_schemes.filter(
(scheme): scheme is string =>
typeof scheme === 'string',
)
: [...DEFAULTS.allowed_schemes]
}
error={fieldError(errors, 'allowed_schemes')}
onChange={(schemes) => onChange({ ...value, allowed_schemes: schemes })}
onChange={(schemes) =>
onChange({ ...value, allowed_schemes: schemes })
}
/>
</section>
</>
Expand Down Expand Up @@ -718,7 +778,9 @@ export function BrowserConfigForm(props: ConfigFormProps) {
useEffect(() => {
if (!focusKey || !domRef.current) return
const field = props.focusField?.[0] ?? focusKey
const target = domRef.current.querySelector<HTMLElement>(`[data-field="${CSS.escape(field)}"]`)
const target = domRef.current.querySelector<HTMLElement>(
`[data-field="${CSS.escape(field)}"]`,
)
target?.focus()
target?.scrollIntoView({ block: 'center' })
}, [focusKey, selection])
Expand All @@ -729,7 +791,9 @@ export function BrowserConfigForm(props: ConfigFormProps) {
return (
<div className={`br-cfg${narrow ? ' narrow' : ''}`} ref={setRoot}>
<div className="br-cfg-workbench">
{showNav ? <ConfigNav value={value} selection={selection} onSelect={choose} /> : null}
{showNav ? (
<ConfigNav value={value} selection={selection} onSelect={choose} />
) : null}
{showEditor ? (
<ConfigEditor
selection={selection}
Expand Down
15 changes: 9 additions & 6 deletions browser/ui/src/function-trigger-message/BrowserViews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,10 @@ export function ConsoleEntryRow({ entry }: { entry: BrowserConsoleEntry }) {
return (
<li className="br-ui-log-row">
<span className="br-ui-log-time">{formatTime(entry.timestamp)}</span>
<Badge variant={levelBadgeVariant(entry.level)} className="br-ui-log-level">
<Badge
variant={levelBadgeVariant(entry.level)}
className="br-ui-log-level"
>
{entry.level}
</Badge>
<span className="br-ui-log-text">
Expand Down Expand Up @@ -304,9 +307,7 @@ export function ConsoleReadView({
export function NetworkEntryRow({ entry }: { entry: BrowserNetworkEntry }) {
return (
<li className="br-ui-log-row">
<span
className={cn('br-ui-net-status', entry.failed && 'is-failed')}
>
<span className={cn('br-ui-net-status', entry.failed && 'is-failed')}>
{entry.status ?? (entry.failed ? 'err' : '...')}
</span>
<span className="br-ui-net-method">{entry.method}</span>
Expand Down Expand Up @@ -338,7 +339,7 @@ export function NetworkReadView({
variant={res.entries.length > 0 ? 'accent' : 'default'}
/>
{req?.failed_only ? (
<Chip className="br-ui-chip-warn">failed only</Chip>
<Chip className="br-ui-chip-warn">Failed only</Chip>
) : null}
{req?.pattern ? <Chip>/{req.pattern}/</Chip> : null}
{res.dropped > 0 ? (
Expand Down Expand Up @@ -513,7 +514,9 @@ export function EvaluateView({
<div className="br-ui-empty-line">· undefined</div>
) : (
<div className="br-ui-json-sm">
<JsonHighlight code={JSON.stringify(res.value, null, 2) ?? 'null'} />
<JsonHighlight
code={JSON.stringify(res.value, null, 2) ?? 'null'}
/>
</div>
)
) : (
Expand Down
6 changes: 3 additions & 3 deletions browser/ui/src/function-trigger-message/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,21 +165,21 @@ function BrowserCallView({ message }: { message: FunctionTriggerMessage }) {
)}
{sessionId ? (
<a href={BROWSER_PAGE_HASH} className="br-ui-call-link">
<ExternalLink size={11} aria-hidden />
<ExternalLink size={16} aria-hidden />
open in browser tab
</a>
) : null}
</div>
{running && message.output == null ? (
<p className="br-ui-call-running">running...</p>
<p className="br-ui-call-running">Running...</p>
) : body ? (
body
) : fallback != null ? (
<div className="br-ui-json">
<JsonHighlight code={formatJson(fallback)} />
</div>
) : (
<p className="br-ui-empty-line">no result</p>
<p className="br-ui-empty-line">No result</p>
)}
</div>
)
Expand Down
6 changes: 4 additions & 2 deletions browser/ui/src/lib/errors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ function denialToInvocation(
? 'Denied'
: 'Trigger failed'
const message =
denial.reason ?? fallbackMessage ?? 'The browser trigger could not complete.'
denial.reason ??
fallbackMessage ??
'The browser trigger could not complete.'
return {
title,
message,
Expand Down Expand Up @@ -344,7 +346,7 @@ function DispatchDeniedView({ denial }: { denial: InfraDispatchDenial }) {
<Badge variant="warn" className="br-ui-err-code">
denied
</Badge>
<span className="br-ui-err-type">dispatch policy</span>
<span className="br-ui-err-type">Dispatch policy</span>
</div>
{fn ? (
<div className="br-ui-err-note">
Expand Down
2 changes: 1 addition & 1 deletion browser/ui/src/lib/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface IconProps {
}

function Svg({
size = 14,
size = 16,
children,
className,
style,
Expand Down
28 changes: 21 additions & 7 deletions browser/ui/src/page/SessionRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ function hostOf(url: string): string {
}
}

export function SessionRail({ sessions, selectedId, loading, onSelect }: SessionRailProps) {
export function SessionRail({
sessions,
selectedId,
loading,
onSelect,
}: SessionRailProps) {
if (sessions.length === 0) {
if (loading) {
return (
Expand All @@ -46,7 +51,10 @@ export function SessionRail({ sessions, selectedId, loading, onSelect }: Session
return (
<div className="br-ui-rail-empty">
<p>No sessions yet.</p>
<p className="dim">Sessions started by agents appear in this list live; new session starts one now.</p>
<p className="dim">
Sessions started by agents appear in this list live; new session
starts one now.
</p>
</div>
)
}
Expand All @@ -65,18 +73,24 @@ export function SessionRail({ sessions, selectedId, loading, onSelect }: Session
className={cn('br-ui-rail-row', selected && 'active')}
>
<span className="br-ui-rail-head">
<Globe size={12} aria-hidden className="br-ui-rail-icon" />
<Globe size={16} aria-hidden className="br-ui-rail-icon" />
<span className="br-ui-rail-title">
{session.title?.trim() || hostOf(session.url) || 'about:blank'}
{session.title?.trim() ||
hostOf(session.url) ||
'about:blank'}
</span>
<span className="br-ui-rail-mode">
{session.headless ? 'headless' : 'headful'}
</span>
<span className="br-ui-rail-mode">{session.headless ? 'headless' : 'headful'}</span>
</span>
<span className="br-ui-rail-url">{session.url}</span>
<span className="br-ui-rail-meta">
<span className="br-ui-rail-status-dot" aria-hidden />
<span>live</span>
<span>Live</span>
<span className="br-ui-rail-meta-separator">·</span>
<span>{formatMtime(Math.floor(session.last_used_ms / 1000))}</span>
<span>
{formatMtime(Math.floor(session.last_used_ms / 1000))}
</span>
</span>
</button>
</li>
Expand Down
Loading
Loading