refactor(automations): extract list toolbar and drop dead list projection

This commit is contained in:
Neil
2026-09-01 00:06:23 -07:00
parent 074f135ee1
commit f05981dc29
6 changed files with 121 additions and 77 deletions
@@ -0,0 +1,104 @@
import React from 'react'
import { Plus, RefreshCw } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { translate } from '@/i18n/i18n'
import { cn } from '@/lib/utils'
import type { AutomationHostCatalogEntry } from './automation-host-catalog-types'
import type { AutomationListArrowKey } from './automation-list-keyboard-navigation'
import { clampAutomationListSearchQueryInput } from './automation-list-search'
import type { AutomationListFilter } from './automation-list-view'
import { AutomationListSearchField } from './AutomationListSearchField'
import { AutomationListFilterMenu } from './AutomationListFilterMenu'
import type { AutomationTemplate } from './automation-templates'
type AutomationListToolbarProps = {
/** Focus fallback for the list: the controls that produced the rows. */
toolbarRef: React.RefObject<HTMLDivElement | null>
listSearchQuery: string
isListSearchQueryTooLarge: boolean
onListSearchQueryChange: (query: string) => void
onSearchArrowNavigate: (key: AutomationListArrowKey) => void
onSearchEnter: () => void
listFilter: AutomationListFilter
onListFilterChange: (filter: AutomationListFilter) => void
hostEntries: readonly AutomationHostCatalogEntry[]
onRefresh: () => void
isRefreshing: boolean
openCreateDialog: (template?: AutomationTemplate) => void
canCreateAutomation: boolean
}
export function AutomationListToolbar({
toolbarRef,
listSearchQuery,
isListSearchQueryTooLarge,
onListSearchQueryChange,
onSearchArrowNavigate,
onSearchEnter,
listFilter,
onListFilterChange,
hostEntries,
onRefresh,
isRefreshing,
openCreateDialog,
canCreateAutomation
}: AutomationListToolbarProps): React.JSX.Element {
return (
<div className="flex shrink-0 items-end justify-between gap-3 pb-4">
<div ref={toolbarRef} className="flex min-w-0 flex-1 items-end gap-2">
<AutomationListSearchField
className="w-full max-w-xs"
query={listSearchQuery}
isTooLarge={isListSearchQueryTooLarge}
onQueryChange={(query) =>
onListSearchQueryChange(clampAutomationListSearchQueryInput(query))
}
onClear={() => onListSearchQueryChange('')}
onArrowNavigate={onSearchArrowNavigate}
onEnter={onSearchEnter}
/>
<AutomationListFilterMenu
filter={listFilter}
onChange={onListFilterChange}
hostEntries={hostEntries}
/>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon-sm"
aria-label={translate(
'auto.components.automations.AutomationsPage.19a6e30eae',
'Refresh automations'
)}
onClick={onRefresh}
disabled={isRefreshing}
className="shrink-0 border border-border bg-background shadow-none hover:bg-muted/50"
>
<RefreshCw className={cn('size-4', isRefreshing && 'animate-spin')} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate(
'auto.components.automations.AutomationsPage.19a6e30eae',
'Refresh automations'
)}
</TooltipContent>
</Tooltip>
</div>
<Button
type="button"
size="sm"
className="shrink-0"
onClick={() => openCreateDialog()}
disabled={!canCreateAutomation}
data-contextual-tour-target="automations-create"
>
<Plus className="size-4" />
{translate('auto.components.automations.AutomationsPage.newAutomation', 'New Automation')}
</Button>
</div>
)
}
@@ -1,10 +1,6 @@
import React, { useRef } from 'react'
import { Plus, RefreshCw } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import type {
Automation,
AutomationRun,
ExternalAutomationAction,
ExternalAutomationJob,
@@ -19,7 +15,6 @@ import type { RuntimeStatus } from '../../../../shared/runtime-types'
import type { TaskSourceHostAvailability } from '../task-source-context-summary'
import type { AutomationRowAction } from './automation-captured-owner'
import type { AutomationHostTarget } from './automation-host-client'
import { clampAutomationListSearchQueryInput } from './automation-list-search'
import {
createAutomationListEnterHandler,
getAutomationListArrowNavigationTarget,
@@ -27,8 +22,7 @@ import {
} from './automation-list-keyboard-navigation'
import type { AutomationListRow } from './automation-list-row-identity'
import type { AutomationPaneTab } from './automation-page-state'
import { AutomationListSearchField } from './AutomationListSearchField'
import { AutomationListFilterMenu, AutomationListFilterPills } from './AutomationListFilterMenu'
import { AutomationListFilterPills } from './AutomationListFilterMenu'
import { isAutomationListFilterActive, type AutomationListFilter } from './automation-list-view'
import { automationHostFilterStableKey } from '../../../../shared/automation-host-filter'
import type { AutomationTemplate } from './automation-templates'
@@ -48,6 +42,7 @@ import { LIST_TABLE_CONTAINER_CLASS } from '@/lib/list-table-layout'
import { translate } from '@/i18n/i18n'
import { AutomationTemplateEmptyState } from './AutomationTemplateEmptyState'
import { AutomationListTableHeader } from './AutomationListTableHeader'
import { AutomationListToolbar } from './AutomationListToolbar'
const TEMPLATE_EMPTY_STATES: ReadonlySet<string> = new Set(['host-empty', 'all-hosts-empty'])
const EMPTY_AUTOMATION_RUNS: ReadonlyMap<string, AutomationRun> = new Map()
@@ -55,9 +50,6 @@ const EMPTY_AUTOMATION_RUNS: ReadonlyMap<string, AutomationRun> = new Map()
type AutomationsListPanelProps = {
hasListItems: boolean
hasFilteredListItems: boolean
/** Compatibility projections for consumers that still inspect bare rows. */
isListSearchActive?: boolean
filteredAutomations?: readonly Automation[]
listSearchQuery: string
isListSearchQueryTooLarge: boolean
onListSearchQueryChange: (query: string) => void
@@ -289,61 +281,21 @@ export function AutomationsListPanel(props: AutomationsListPanelProps): React.JS
className="flex min-h-0 flex-1 flex-col overflow-hidden px-3 pb-4 md:px-5"
data-contextual-tour-target="automations-list"
>
<div className="flex shrink-0 items-end justify-between gap-3 pb-4">
<div ref={toolbarRef} className="flex min-w-0 flex-1 items-end gap-2">
<AutomationListSearchField
className="w-full max-w-xs"
query={listSearchQuery}
isTooLarge={isListSearchQueryTooLarge}
onQueryChange={(query) =>
onListSearchQueryChange(clampAutomationListSearchQueryInput(query))
}
onClear={() => onListSearchQueryChange('')}
onArrowNavigate={handleSearchArrowNavigate}
onEnter={handleSearchEnter}
/>
<AutomationListFilterMenu
filter={listFilter}
onChange={onListFilterChange}
hostEntries={hostCatalog.entries}
/>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon-sm"
aria-label={translate(
'auto.components.automations.AutomationsPage.19a6e30eae',
'Refresh automations'
)}
onClick={onRefresh}
disabled={isRefreshing}
className="shrink-0 border border-border bg-background shadow-none hover:bg-muted/50"
>
<RefreshCw className={cn('size-4', isRefreshing && 'animate-spin')} />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={6}>
{translate(
'auto.components.automations.AutomationsPage.19a6e30eae',
'Refresh automations'
)}
</TooltipContent>
</Tooltip>
</div>
<Button
type="button"
size="sm"
className="shrink-0"
onClick={() => openCreateDialog()}
disabled={!canCreateAutomation}
data-contextual-tour-target="automations-create"
>
<Plus className="size-4" />
{translate('auto.components.automations.AutomationsPage.newAutomation', 'New Automation')}
</Button>
</div>
<AutomationListToolbar
toolbarRef={toolbarRef}
listSearchQuery={listSearchQuery}
isListSearchQueryTooLarge={isListSearchQueryTooLarge}
onListSearchQueryChange={onListSearchQueryChange}
onSearchArrowNavigate={handleSearchArrowNavigate}
onSearchEnter={handleSearchEnter}
listFilter={listFilter}
onListFilterChange={onListFilterChange}
hostEntries={hostCatalog.entries}
onRefresh={onRefresh}
isRefreshing={isRefreshing}
openCreateDialog={openCreateDialog}
canCreateAutomation={canCreateAutomation}
/>
{listFilterActive || legacyScopeStableKey !== null ? (
<div className="flex flex-wrap items-center gap-1.5 pb-3">
@@ -90,10 +90,8 @@ export function AutomationsPageSurface({
hostCatalog,
hasListItems,
hasFilteredListItems,
isListSearchActive,
isListSearchQueryTooLarge,
filteredRows,
filteredAutomations,
filteredExternalAutomationEntries,
selected,
selectedRow,
@@ -319,7 +317,6 @@ export function AutomationsPageSurface({
hasFilteredListItems={hasFilteredListItems}
listSearchQuery={listSearchQuery}
isListSearchQueryTooLarge={isListSearchQueryTooLarge}
isListSearchActive={isListSearchActive}
onListSearchQueryChange={setListSearchQuery}
listFilter={listFilter}
onListFilterChange={onListFilterChange}
@@ -337,7 +334,6 @@ export function AutomationsPageSurface({
}
}}
filteredRows={filteredRows}
filteredAutomations={filteredAutomations}
filteredExternalAutomationEntries={filteredExternalAutomationEntries}
selectedRowKey={selectedRow?.key ?? null}
selectedExternalKey={local.selectedExternalKey}
@@ -39,7 +39,6 @@ export const RUNTIME_REPO_ID = RUNTIME_REPO_ID_FIXTURE
export const RUNTIME_WORKSPACE_ID = RUNTIME_WORKSPACE_ID_FIXTURE
export type ListPanelProps = {
filteredAutomations: Automation[]
filteredExternalAutomationEntries: ExternalAutomationListEntry[]
selectedExternal: ExternalAutomationListEntry | null
openEditExternalDialog: (
@@ -61,7 +61,6 @@ export function useAutomationListSearch({
selectExternalKey: (externalKey: string | null) => void
}): {
isListSearchQueryTooLarge: boolean
isListSearchActive: boolean
filteredRows: readonly AutomationListRow[]
filteredExternalAutomationEntries: readonly ExternalAutomationListEntry[]
hasListItems: boolean
@@ -200,7 +199,6 @@ export function useAutomationListSearch({
return {
isListSearchQueryTooLarge,
isListSearchActive,
filteredRows,
filteredExternalAutomationEntries,
hasListItems: hostRowCount > 0,
@@ -85,7 +85,6 @@ export function useAutomationsPageListState({
const selected = selectedRow?.automation ?? null
const {
isListSearchQueryTooLarge,
isListSearchActive,
filteredRows,
filteredExternalAutomationEntries,
hasListItems,
@@ -145,11 +144,7 @@ export function useAutomationsPageListState({
selected,
selectedAutomationRunsWithWorkspaceNames,
isListSearchQueryTooLarge,
isListSearchActive,
filteredRows,
// Keep the legacy projection available to integrations that still consume
// bare automations; the table itself uses host-qualified rows.
filteredAutomations: filteredRows.map((row) => row.automation),
filteredExternalAutomationEntries,
hasListItems,
hasFilteredListItems,