/** * Cluster tab: run one command across many hosts (aliases / environment / * tags filters) and inspect per-host results. Stdout/stderr render in * collapsed
blocks; status renders as a colored badge. */ import { useState } from 'react' import type { SshApi } from '../api.ts' import type { ClusterResult } from '../../protocol.ts' import { errorMessage, tt } from './helpers.ts' import css from './panel.module.css' /** Cluster tab props. */ export interface ClusterTabProps { api: SshApi } /** Split a comma-separated input into a trimmed, non-empty string list. */ function splitList(text: string): string[] { return text.split(',').map(part => part.trim()).filter(part => part !== '') } /** The cluster execution tab. */ export function ClusterTab({ api }: ClusterTabProps) { const [command, setCommand] = useState('') const [aliases, setAliases] = useState('') const [environment, setEnvironment] = useState('') const [tags, setTags] = useState('') const [running, setRunning] = useState(false) const [results, setResults] = useState(null) const [error, setError] = useState(null) const run = async (): Promise => { if (command.trim() === '' || running) return if (!window.confirm(tt('cluster.confirm'))) return setRunning(true) setError(null) try { const aliasList = splitList(aliases) const tagList = splitList(tags) const outcome = await api.cluster({ command: command.trim(), aliases: aliasList.length > 0 ? aliasList : undefined, environment: environment.trim() === '' ? undefined : environment.trim(), tags: tagList.length > 0 ? tagList : undefined, }) setResults(outcome) } catch (cause) { setError(errorMessage(cause)) } finally { setRunning(false) } } return (