mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-28 20:59:09 +00:00
feat(sidebar): §14 sidebar orchestration, filter chips, pinned rail, trailing column (#850)
* feat(sidebar): §14 sidebar orchestration (filter chips, pinned rail, trailing column) - Add All / Up / Down / Updates filter chips with live counts; active chip filters the list; chip-filtered files computed in EditorLayout with useMemo - Surface the PINNED group with a 3px cyan left rail and glow via the sidebarPinnedGroupRail token; reuses the brand token already on the active row - Compact brand row from three stacked elements to a single 44px horizontal bar - Restructure StackRow trailing column as fixed slots: label dots (max 3 + +N overflow), update-dot | git-pending icon (priority order), kebab; add reserved invisible checkbox slot for PR2 bulk mode - Export statusText / statusColor from StackRow and reuse them in StackList remote-results section to remove the duplicate inline logic - Lift filterChip state and chip-filtered files to EditorLayout; remove filterChip from StackListProps to eliminate the dual-path redundancy - Remove unused labels param from buildGroups and the void labels workaround - Wrap filteredFiles in useMemo so filterCounts memo is not defeated on every render * fix(sidebar): move statusText and statusColor to stack-status-utils react-refresh/only-export-components requires component files to export only components. Move the two utility functions and StackRowStatus type to a dedicated stack-status-utils.ts so StackRow.tsx is a pure component module. Update StackList.tsx and EditorLayout.tsx to import from the new source directly.
This commit is contained in:
@@ -73,8 +73,8 @@ import { VulnerabilityScanSheet } from './VulnerabilityScanSheet';
|
||||
import { StackSidebar } from '@/components/sidebar/StackSidebar';
|
||||
import { usePinnedStacks } from '@/hooks/usePinnedStacks';
|
||||
import { useSidebarGroupCollapse } from '@/hooks/useSidebarGroupCollapse';
|
||||
import type { StackRowStatus } from '@/components/sidebar/StackRow';
|
||||
import type { StackMenuCtx } from '@/components/sidebar/sidebar-types';
|
||||
import type { StackRowStatus } from '@/components/sidebar/stack-status-utils';
|
||||
import type { FilterChip, StackMenuCtx } from '@/components/sidebar/sidebar-types';
|
||||
import { StackFileExplorer } from '@/components/files/StackFileExplorer';
|
||||
|
||||
interface ContainerInfo {
|
||||
@@ -1931,9 +1931,9 @@ export default function EditorLayout() {
|
||||
// Stack name is now the same as selectedFile (no extension to strip)
|
||||
const stackName = selectedFile || '';
|
||||
|
||||
// Filter files based on search query
|
||||
const filteredFiles = files.filter(file =>
|
||||
file.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
const filteredFiles = useMemo(
|
||||
() => files.filter(file => file.toLowerCase().includes(searchQuery.toLowerCase())),
|
||||
[files, searchQuery],
|
||||
);
|
||||
|
||||
// Get display name for stack (now just returns the name as-is since no extension)
|
||||
@@ -1947,6 +1947,23 @@ export default function EditorLayout() {
|
||||
if (evictedOldest) toast.info('Pinned. Unpinned oldest (max 10).');
|
||||
}, [evictedOldest]);
|
||||
|
||||
const [filterChip, setFilterChip] = useState<FilterChip>('all');
|
||||
|
||||
const filterCounts = useMemo(() => ({
|
||||
all: filteredFiles.length,
|
||||
up: filteredFiles.filter(f => stackStatuses[f] === 'running').length,
|
||||
down: filteredFiles.filter(f => stackStatuses[f] === 'exited').length,
|
||||
updates: filteredFiles.filter(f => !!stackUpdates[f]).length,
|
||||
}), [filteredFiles, stackStatuses, stackUpdates]);
|
||||
|
||||
const chipFilteredFiles = useMemo(() => {
|
||||
if (filterChip === 'all') return filteredFiles;
|
||||
if (filterChip === 'up') return filteredFiles.filter(f => stackStatuses[f] === 'running');
|
||||
if (filterChip === 'down') return filteredFiles.filter(f => stackStatuses[f] === 'exited');
|
||||
if (filterChip === 'updates') return filteredFiles.filter(f => !!stackUpdates[f]);
|
||||
return filteredFiles;
|
||||
}, [filteredFiles, filterChip, stackStatuses, stackUpdates]);
|
||||
|
||||
const { isCollapsed, toggle: toggleCollapse } = useSidebarGroupCollapse(activeNode?.id);
|
||||
|
||||
const remoteResults = useMemo(() => {
|
||||
@@ -2286,8 +2303,11 @@ export default function EditorLayout() {
|
||||
canCreate={can('stack:create')}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
filterChip={filterChip}
|
||||
filterCounts={filterCounts}
|
||||
onFilterChipChange={setFilterChip}
|
||||
list={{
|
||||
files: filteredFiles ?? [],
|
||||
files: chipFilteredFiles,
|
||||
isLoading,
|
||||
isPaid,
|
||||
selectedFile,
|
||||
@@ -2296,7 +2316,6 @@ export default function EditorLayout() {
|
||||
stackStatuses: stackStatuses as Record<string, StackRowStatus | undefined>,
|
||||
stackUpdates,
|
||||
gitSourcePendingMap,
|
||||
labels,
|
||||
pinnedFiles: pinned,
|
||||
isCollapsed,
|
||||
toggleCollapse,
|
||||
|
||||
@@ -4,17 +4,17 @@ interface SidebarBrandProps {
|
||||
|
||||
export function SidebarBrand({ isDarkMode }: SidebarBrandProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-2.5 px-4 py-3 border-b border-glass-border">
|
||||
<div className="flex items-center gap-2 px-4 py-2 border-b border-glass-border">
|
||||
<img
|
||||
src={isDarkMode ? '/sencho-logo-dark.png' : '/sencho-logo-light.png'}
|
||||
alt="Sencho Logo"
|
||||
className="w-10 h-10 shrink-0"
|
||||
className="w-7 h-7 shrink-0"
|
||||
/>
|
||||
<div className="flex flex-col leading-none">
|
||||
<span className="font-mono text-[9px] tracking-[0.22em] uppercase text-stat-subtitle">
|
||||
SENCHO · v{__APP_VERSION__}
|
||||
</span>
|
||||
<span className="font-serif italic text-xl text-foreground mt-1">Sencho</span>
|
||||
<span className="font-serif italic text-[17px] text-foreground mt-0.5">Sencho</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { FilterChip } from './sidebar-types';
|
||||
|
||||
export interface FilterCounts {
|
||||
all: number;
|
||||
up: number;
|
||||
down: number;
|
||||
updates: number;
|
||||
}
|
||||
|
||||
interface SidebarFilterChipsProps {
|
||||
active: FilterChip;
|
||||
counts: FilterCounts;
|
||||
onChange: (chip: FilterChip) => void;
|
||||
}
|
||||
|
||||
const chips: { id: FilterChip; label: string }[] = [
|
||||
{ id: 'all', label: 'All' },
|
||||
{ id: 'up', label: 'Up' },
|
||||
{ id: 'down', label: 'Down' },
|
||||
{ id: 'updates', label: 'Updates' },
|
||||
];
|
||||
|
||||
export function SidebarFilterChips({ active, counts, onChange }: SidebarFilterChipsProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-1 px-2 pb-1.5 pt-0.5 overflow-x-auto scrollbar-none">
|
||||
{chips.map(({ id, label }) => {
|
||||
const count = counts[id];
|
||||
const isActive = active === id;
|
||||
const isUpdates = id === 'updates';
|
||||
const hasUpdates = isUpdates && count > 0;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={() => onChange(id)}
|
||||
className={cn(
|
||||
'flex items-center gap-1 shrink-0 rounded px-1.5 py-0.5',
|
||||
'font-mono text-[10px] tracking-[0.08em] uppercase leading-none',
|
||||
'border transition-colors duration-150',
|
||||
isActive
|
||||
? 'bg-brand/10 border-brand/30 text-brand'
|
||||
: hasUpdates
|
||||
? 'bg-update/5 border-update/20 text-update hover:bg-update/10'
|
||||
: 'bg-transparent border-glass-border text-stat-subtitle hover:text-stat-title hover:border-glass-border',
|
||||
)}
|
||||
aria-pressed={isActive}
|
||||
>
|
||||
{label}
|
||||
<span className={cn(
|
||||
'tabular-nums',
|
||||
isActive ? 'text-brand/70' : hasUpdates ? 'text-update/70' : 'text-stat-icon',
|
||||
)}>
|
||||
{count}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { sidebarPinnedGroupRail } from './sidebar-styles';
|
||||
|
||||
interface StackGroupProps {
|
||||
id: string;
|
||||
@@ -13,18 +14,22 @@ interface StackGroupProps {
|
||||
}
|
||||
|
||||
export function StackGroup({ id, label, count, collapsed, onToggle, variant = 'default', children }: StackGroupProps) {
|
||||
const labelColor = variant === 'pinned' ? 'text-brand/90' : 'text-stat-subtitle';
|
||||
const isPinned = variant === 'pinned';
|
||||
const labelColor = isPinned ? 'text-brand/90' : 'text-stat-subtitle';
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
className="flex items-center justify-between w-full px-2 py-1 text-left hover:bg-glass-highlight/30 rounded-md"
|
||||
className={cn(
|
||||
'relative flex items-center justify-between w-full py-1 text-left hover:bg-glass-highlight/30 rounded-md',
|
||||
isPinned ? sidebarPinnedGroupRail : 'px-2',
|
||||
)}
|
||||
aria-expanded={!collapsed}
|
||||
aria-controls={`group-${id}-body`}
|
||||
>
|
||||
<span className={cn('font-mono text-[9px] tracking-[0.22em] uppercase', labelColor)}>
|
||||
{label}
|
||||
{isPinned ? '★ ' : ''}{label}
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="font-mono text-[9px] tabular-nums text-stat-icon">{count}</span>
|
||||
|
||||
@@ -4,8 +4,9 @@ import { useStackKeyboardShortcuts } from '@/hooks/useStackKeyboardShortcuts';
|
||||
import { CommandItem, CommandList } from '@/components/ui/command';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import type { Label } from '@/components/label-types';
|
||||
import type { StackRowStatus } from './StackRow';
|
||||
import { StackRow } from './StackRow';
|
||||
import { statusText, statusColor } from './stack-status-utils';
|
||||
import type { StackRowStatus } from './stack-status-utils';
|
||||
import { StackGroup } from './StackGroup';
|
||||
import { StackContextMenu } from './StackContextMenu';
|
||||
import { StackKebabMenu } from './StackKebabMenu';
|
||||
@@ -27,7 +28,6 @@ export interface StackListProps {
|
||||
stackStatuses: Record<string, StackRowStatus | undefined>;
|
||||
stackUpdates: Record<string, boolean>;
|
||||
gitSourcePendingMap: Record<string, boolean>;
|
||||
labels: Label[];
|
||||
pinnedFiles: string[];
|
||||
isCollapsed: (groupKey: string) => boolean;
|
||||
toggleCollapse: (groupKey: string) => void;
|
||||
@@ -53,7 +53,6 @@ function buildGroups(
|
||||
files: string[],
|
||||
pinnedFiles: string[],
|
||||
stackLabelMap: Record<string, Label[]>,
|
||||
labels: Label[],
|
||||
): BuiltGroup[] {
|
||||
const result: BuiltGroup[] = [];
|
||||
|
||||
@@ -93,20 +92,19 @@ function buildGroups(
|
||||
result.push({ kind: 'unlabeled', id: 'unlabeled', label: 'UNLABELED', count: unlabeled.length, files: unlabeled });
|
||||
}
|
||||
|
||||
void labels;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function StackList(props: StackListProps) {
|
||||
const {
|
||||
files, isLoading, isPaid, selectedFile, searchQuery, stackLabelMap, stackStatuses,
|
||||
stackUpdates, gitSourcePendingMap, labels, pinnedFiles, isCollapsed, toggleCollapse,
|
||||
stackUpdates, gitSourcePendingMap, pinnedFiles, isCollapsed, toggleCollapse,
|
||||
isBusy, getDisplayName, onSelectFile, buildMenuCtx, remoteResults, remoteLoading, onSelectRemoteFile,
|
||||
} = props;
|
||||
|
||||
const groups = useMemo(
|
||||
() => buildGroups(files, pinnedFiles, stackLabelMap, labels),
|
||||
[files, pinnedFiles, stackLabelMap, labels],
|
||||
() => buildGroups(files, pinnedFiles, stackLabelMap),
|
||||
[files, pinnedFiles, stackLabelMap],
|
||||
);
|
||||
|
||||
useStackKeyboardShortcuts(selectedFile, buildMenuCtx);
|
||||
@@ -181,11 +179,8 @@ export function StackList(props: StackListProps) {
|
||||
onClick={() => onSelectRemoteFile(nodeId, file)}
|
||||
className="w-full text-left justify-start rounded-lg mb-1 cursor-pointer hover:bg-glass-highlight px-2 py-1.5 flex items-center gap-2"
|
||||
>
|
||||
<span className={`font-mono text-[10px] shrink-0 w-5 ${
|
||||
status === 'running' ? 'text-success' :
|
||||
status === 'exited' ? 'text-destructive' : 'text-stat-icon'
|
||||
}`}>
|
||||
{status === 'running' ? 'UP' : status === 'exited' ? 'DN' : '--'}
|
||||
<span className={`font-mono text-[10px] shrink-0 w-5 ${statusColor(status, false)}`}>
|
||||
{statusText(status)}
|
||||
</span>
|
||||
<span className="flex-1 truncate font-mono text-xs">{file}</span>
|
||||
<ArrowUpRight className="w-3 h-3 text-stat-icon shrink-0" strokeWidth={1.5} />
|
||||
|
||||
@@ -4,9 +4,9 @@ import { Cursor, CursorContainer, CursorFollow, CursorProvider } from '@/compone
|
||||
import { LabelDot } from '@/components/LabelPill';
|
||||
import type { Label } from '@/components/label-types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { sidebarRowActive, sidebarRowBase } from './sidebar-styles';
|
||||
|
||||
export type StackRowStatus = 'running' | 'exited' | 'unknown';
|
||||
import { sidebarRowActive, sidebarRowBase, sidebarRowCheckboxSlot } from './sidebar-styles';
|
||||
import { statusText, statusColor } from './stack-status-utils';
|
||||
import type { StackRowStatus } from './stack-status-utils';
|
||||
|
||||
interface StackRowProps {
|
||||
file: string;
|
||||
@@ -22,19 +22,6 @@ interface StackRowProps {
|
||||
kebabSlot: ReactNode;
|
||||
}
|
||||
|
||||
function statusText(status: StackRowStatus): string {
|
||||
if (status === 'running') return 'UP';
|
||||
if (status === 'exited') return 'DN';
|
||||
return '--';
|
||||
}
|
||||
|
||||
function statusColor(status: StackRowStatus, isBusy: boolean): string {
|
||||
if (isBusy) return 'text-muted-foreground';
|
||||
if (status === 'running') return 'text-success';
|
||||
if (status === 'exited') return 'text-destructive';
|
||||
return 'text-stat-icon';
|
||||
}
|
||||
|
||||
function RowTooltip({ trigger, label }: { trigger: ReactNode; label: string }) {
|
||||
return (
|
||||
<CursorProvider>
|
||||
@@ -49,9 +36,14 @@ function RowTooltip({ trigger, label }: { trigger: ReactNode; label: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
const MAX_VISIBLE_LABELS = 3;
|
||||
|
||||
export function StackRow(props: StackRowProps) {
|
||||
const { file, displayName, status, isBusy, isActive, isPaid, labels, hasUpdate, hasGitPending, onSelect, kebabSlot } = props;
|
||||
|
||||
const visibleLabels = isPaid ? labels.slice(0, MAX_VISIBLE_LABELS) : [];
|
||||
const overflowCount = isPaid ? Math.max(0, labels.length - MAX_VISIBLE_LABELS) : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="stack-row"
|
||||
@@ -61,32 +53,48 @@ export function StackRow(props: StackRowProps) {
|
||||
onClick={() => onSelect(file)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect(file); } }}
|
||||
>
|
||||
{/* Reserved checkbox slot — revealed in bulk mode (PR2) */}
|
||||
<span className={sidebarRowCheckboxSlot} aria-hidden="true" />
|
||||
|
||||
{/* Status pill */}
|
||||
<span className={cn('font-mono text-[10px] shrink-0 w-[22px] flex items-center', statusColor(status, isBusy))}>
|
||||
{isBusy ? <Loader2 className="w-3 h-3 animate-spin" strokeWidth={2} /> : statusText(status)}
|
||||
</span>
|
||||
<span className="flex-1 truncate font-mono text-[13px]">{displayName}</span>
|
||||
{isPaid && labels.length > 0 && (
|
||||
|
||||
{/* Stack name */}
|
||||
<span className="flex-1 truncate font-mono text-[13px] min-w-0">{displayName}</span>
|
||||
|
||||
{/* Trailing: label dots (max 3 + overflow count) */}
|
||||
{visibleLabels.length > 0 && (
|
||||
<span className="flex items-center gap-0.5 shrink-0">
|
||||
{labels.map(l => <LabelDot key={l.id} color={l.color} />)}
|
||||
{visibleLabels.map(l => <LabelDot key={l.id} color={l.color} />)}
|
||||
{overflowCount > 0 && (
|
||||
<span className="font-mono text-[8px] text-stat-icon leading-none">+{overflowCount}</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{hasUpdate && (
|
||||
<RowTooltip
|
||||
trigger={(
|
||||
<span className="relative inline-flex w-2 h-2 shrink-0">
|
||||
<span className="absolute inset-0 rounded-full bg-update opacity-75 animate-ping" />
|
||||
<span className="relative w-2 h-2 rounded-full bg-update" />
|
||||
</span>
|
||||
)}
|
||||
label="Update available"
|
||||
/>
|
||||
)}
|
||||
{hasGitPending && (
|
||||
<RowTooltip
|
||||
trigger={<GitBranch className="w-3 h-3 text-brand" strokeWidth={1.5} />}
|
||||
label="Git source update pending"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Fixed trailing icon slot: update dot takes priority over git pending */}
|
||||
<span className="w-3.5 h-3.5 flex items-center justify-center shrink-0">
|
||||
{hasUpdate ? (
|
||||
<RowTooltip
|
||||
trigger={(
|
||||
<span className="relative inline-flex w-2 h-2">
|
||||
<span className="absolute inset-0 rounded-full bg-update opacity-75 animate-ping" />
|
||||
<span className="relative w-2 h-2 rounded-full bg-update" />
|
||||
</span>
|
||||
)}
|
||||
label="Update available"
|
||||
/>
|
||||
) : hasGitPending ? (
|
||||
<RowTooltip
|
||||
trigger={<GitBranch className="w-3 h-3 text-brand" strokeWidth={1.5} />}
|
||||
label="Git source update pending"
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
|
||||
{/* Kebab — always rightmost */}
|
||||
<div
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
|
||||
@@ -5,8 +5,10 @@ import type { NotificationItem } from '@/components/dashboard/types';
|
||||
import { SidebarActions } from './SidebarActions';
|
||||
import { SidebarActivityTicker } from './SidebarActivityTicker';
|
||||
import { SidebarBrand } from './SidebarBrand';
|
||||
import { SidebarFilterChips, type FilterCounts } from './SidebarFilterChips';
|
||||
import { SidebarSearch } from './SidebarSearch';
|
||||
import { StackList, type StackListProps } from './StackList';
|
||||
import type { FilterChip } from './sidebar-types';
|
||||
|
||||
export interface StackSidebarProps {
|
||||
isDarkMode: boolean;
|
||||
@@ -17,6 +19,9 @@ export interface StackSidebarProps {
|
||||
canCreate: boolean;
|
||||
searchQuery: string;
|
||||
onSearchChange: (v: string) => void;
|
||||
filterChip: FilterChip;
|
||||
filterCounts: FilterCounts;
|
||||
onFilterChipChange: (chip: FilterChip) => void;
|
||||
list: StackListProps;
|
||||
notifications: NotificationItem[];
|
||||
tickerConnected: boolean;
|
||||
@@ -26,7 +31,8 @@ export interface StackSidebarProps {
|
||||
export function StackSidebar(props: StackSidebarProps) {
|
||||
const {
|
||||
isDarkMode, nodeSwitcherSlot, createStackSlot, onScan, isScanning, canCreate,
|
||||
searchQuery, onSearchChange, list, notifications, tickerConnected, onOpenActivity,
|
||||
searchQuery, onSearchChange, filterChip, filterCounts, onFilterChipChange,
|
||||
list, notifications, tickerConnected, onOpenActivity,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -38,6 +44,11 @@ export function StackSidebar(props: StackSidebarProps) {
|
||||
)}
|
||||
<Command shouldFilter={false} className="bg-transparent flex-1 flex flex-col overflow-hidden">
|
||||
<SidebarSearch value={searchQuery} onValueChange={onSearchChange} />
|
||||
<SidebarFilterChips
|
||||
active={filterChip}
|
||||
counts={filterCounts}
|
||||
onChange={onFilterChipChange}
|
||||
/>
|
||||
<ScrollArea className="flex-1 px-2 pb-2">
|
||||
<div data-stacks-loaded={list.isLoading ? 'false' : 'true'}>
|
||||
<StackList {...list} />
|
||||
|
||||
@@ -19,3 +19,12 @@ export const sidebarGroupHeader = cn(
|
||||
'text-[9px] leading-3 tracking-[0.22em] uppercase text-stat-subtitle',
|
||||
'cursor-pointer select-none',
|
||||
);
|
||||
|
||||
export const sidebarRowCheckboxSlot = 'shrink-0 w-4 h-4 opacity-0 pointer-events-none flex-shrink-0';
|
||||
|
||||
export const sidebarPinnedGroupRail = cn(
|
||||
'relative before:absolute before:left-0 before:top-0 before:bottom-0',
|
||||
'before:w-[3px] before:rounded-sm before:bg-brand',
|
||||
'before:shadow-[0_0_6px_var(--brand)]',
|
||||
'pl-[13px]',
|
||||
);
|
||||
|
||||
@@ -52,3 +52,5 @@ export interface StackMenuCtx {
|
||||
}
|
||||
|
||||
export type StackGroupKind = 'pinned' | 'labeled' | 'unlabeled';
|
||||
|
||||
export type FilterChip = 'all' | 'up' | 'down' | 'updates';
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export type StackRowStatus = 'running' | 'exited' | 'unknown';
|
||||
|
||||
export function statusText(status: StackRowStatus): string {
|
||||
if (status === 'running') return 'UP';
|
||||
if (status === 'exited') return 'DN';
|
||||
return '--';
|
||||
}
|
||||
|
||||
export function statusColor(status: StackRowStatus, isBusy: boolean): string {
|
||||
if (isBusy) return 'text-muted-foreground';
|
||||
if (status === 'running') return 'text-success';
|
||||
if (status === 'exited') return 'text-destructive';
|
||||
return 'text-stat-icon';
|
||||
}
|
||||
Reference in New Issue
Block a user