diff --git a/src/ui/src/components/data-table/data-table.tsx b/src/ui/src/components/data-table/data-table.tsx index 7eb857f3d..dba29926b 100644 --- a/src/ui/src/components/data-table/data-table.tsx +++ b/src/ui/src/components/data-table/data-table.tsx @@ -80,6 +80,8 @@ export interface DataTableProps { onRowDoubleClick?: (row: TData) => void; /** For middle-click: returns URL for new tab, or undefined to call onRowClick */ getRowHref?: (row: TData) => string | undefined; + /** Native tooltip (title attribute) for a row, shown on hover */ + getRowTitle?: (row: TData) => string | undefined; selectedRowId?: string; /** Override default header cell padding/styling for all columns (default: "px-4 py-3") */ headerClassName?: string; @@ -141,6 +143,7 @@ function DataTableInner({ onFocusedRowChange, onRowDoubleClick, getRowHref, + getRowTitle, selectedRowId, headerClassName: tableHeaderClassName, theadClassName, @@ -565,6 +568,7 @@ function DataTableInner({ onRowClick={onRowClick} onRowDoubleClick={onRowDoubleClick} getRowHref={getRowHref} + getRowTitle={getRowTitle} selectedRowId={selectedRowId} getRowId={getRowId} rowClassName={rowClassName} diff --git a/src/ui/src/components/data-table/virtual-table-body.tsx b/src/ui/src/components/data-table/virtual-table-body.tsx index cb6a44d42..758ebfe04 100644 --- a/src/ui/src/components/data-table/virtual-table-body.tsx +++ b/src/ui/src/components/data-table/virtual-table-body.tsx @@ -38,6 +38,8 @@ export interface VirtualTableBodyProps { onRowDoubleClick?: (item: TData, index: number) => void; /** Returns URL for middle-click "open in new tab", or undefined to fall back to onRowClick */ getRowHref?: (item: TData) => string | undefined; + /** Native tooltip (title attribute) for a row, shown on hover */ + getRowTitle?: (item: TData) => string | undefined; selectedRowId?: string; getRowId?: (item: TData) => string; rowClassName?: string | ((item: TData, index: number) => string); @@ -61,6 +63,7 @@ function VirtualTableBodyInner({ onRowClick, onRowDoubleClick, getRowHref, + getRowTitle, selectedRowId, getRowId, rowClassName, @@ -113,6 +116,10 @@ function VirtualTableBodyInner({ (e: React.MouseEvent) => { if (!onRowClick && !getRowHref) return; + // If the user dragged to select text, don't trigger navigation. + const selection = window.getSelection(); + if (selection && selection.toString().length > 0) return; + // Skip the second click of a multi-click sequence when dblclick is handled, // otherwise two competing startViewTransition calls cause the dblclick's // router.push to never execute. @@ -276,6 +283,7 @@ function VirtualTableBodyInner({ data-interactive={isInteractive || undefined} aria-rowindex={virtualRow.index + 2} aria-selected={isSelected ? true : undefined} + title={getRowTitle?.(rowData)} tabIndex={tabIndex} className={cn( "data-table-row border-b border-zinc-200 dark:border-zinc-800", diff --git a/src/ui/src/components/filter-bar/filter-bar-dropdown.tsx b/src/ui/src/components/filter-bar/filter-bar-dropdown.tsx index ac77c3767..9e39ebd7e 100644 --- a/src/ui/src/components/filter-bar/filter-bar-dropdown.tsx +++ b/src/ui/src/components/filter-bar/filter-bar-dropdown.tsx @@ -446,7 +446,10 @@ function SuggestionItemInner({ suggestion, onSelect }: SuggestionItemProps {suggestion.type === "field" ? ( {suggestion.label} ) : ( - {suggestion.label} + + {suggestion.field.prefix} + {suggestion.label.slice(suggestion.field.prefix.length)} + )} {suggestion.hint && {suggestion.hint}} diff --git a/src/ui/src/features/occupancy/components/occupancy-column-defs.tsx b/src/ui/src/features/occupancy/components/occupancy-column-defs.tsx index 7631ac5ff..3b1cbde7d 100644 --- a/src/ui/src/features/occupancy/components/occupancy-column-defs.tsx +++ b/src/ui/src/features/occupancy/components/occupancy-column-defs.tsx @@ -69,7 +69,7 @@ function PriorityBadge({ value, colorClass }: { value: number; colorClass: strin ); } -function buildWorkflowsUrl(row: OccupancyFlatRow, groupBy: OccupancyGroupBy, searchChips: SearchChip[]): string { +export function buildWorkflowsUrl(row: OccupancyFlatRow, groupBy: OccupancyGroupBy, searchChips: SearchChip[]): string { const params: string[] = []; if (row._type === "parent") { params.push(`f=${groupBy}:${encodeURIComponent(row.key)}`); @@ -200,7 +200,8 @@ export function createOccupancyColumns( return ( e.stopPropagation()} > {original.key} diff --git a/src/ui/src/features/occupancy/components/occupancy-data-table.tsx b/src/ui/src/features/occupancy/components/occupancy-data-table.tsx index f496eb07a..71ae7165e 100644 --- a/src/ui/src/features/occupancy/components/occupancy-data-table.tsx +++ b/src/ui/src/features/occupancy/components/occupancy-data-table.tsx @@ -17,6 +17,7 @@ "use client"; import { useMemo, useCallback, memo } from "react"; +import { useRouter } from "next/navigation"; import { DataTable } from "@/components/data-table/data-table"; import { TableEmptyState } from "@/components/data-table/table-empty-state"; import { TableLoadingSkeleton, TableErrorState } from "@/components/data-table/table-states"; @@ -31,12 +32,11 @@ import { asOccupancyColumnIds, OCCUPANCY_COLUMN_SIZE_CONFIG, } from "@/features/occupancy/lib/occupancy-columns"; -import { createOccupancyColumns } from "@/features/occupancy/components/occupancy-column-defs"; +import { createOccupancyColumns, buildWorkflowsUrl } from "@/features/occupancy/components/occupancy-column-defs"; import { useOccupancyTableStore } from "@/features/occupancy/stores/occupancy-table-store"; import "@/features/occupancy/styles/occupancy.css"; const FIXED_COLUMNS = Array.from(MANDATORY_COLUMN_IDS); -const isOccupancyRowInteractive = (row: OccupancyFlatRow) => row._type === "parent"; function flattenForTable(groups: OccupancyGroup[], expandedKeys: Set): OccupancyFlatRow[] { return groups.flatMap((group, groupIndex) => { @@ -88,6 +88,7 @@ export const OccupancyDataTable = memo(function OccupancyDataTable({ error, onRetry, }: OccupancyDataTableProps) { + const router = useRouter(); const compactMode = useCompactMode(); const rowHeight = compactMode ? TABLE_ROW_HEIGHTS.COMPACT : TABLE_ROW_HEIGHTS.NORMAL; @@ -114,9 +115,30 @@ export const OccupancyDataTable = memo(function OccupancyDataTable({ const handleRowClick = useCallback( (row: OccupancyFlatRow) => { - if (isOccupancyRowInteractive(row)) onToggleExpand(row.key); + if (row._type === "parent") { + onToggleExpand(row.key); + } else { + router.push(buildWorkflowsUrl(row, groupBy, searchChips)); + } }, - [onToggleExpand], + [onToggleExpand, router, groupBy, searchChips], + ); + + const getRowHref = useCallback( + (row: OccupancyFlatRow) => { + if (row._type === "child") return buildWorkflowsUrl(row, groupBy, searchChips); + return undefined; + }, + [groupBy, searchChips], + ); + + const getRowTitle = useCallback( + (row: OccupancyFlatRow) => { + if (row._type !== "child") return undefined; + if (groupBy === "pool") return `View ${row.key}'s workflows`; + return `View workflows for ${row.key}`; + }, + [groupBy], ); // Zebra striping keyed on group index so parent + children share the same stripe @@ -171,8 +193,9 @@ export const OccupancyDataTable = memo(function OccupancyDataTable({ isLoading={isLoading} emptyContent={emptyContent} onRowClick={handleRowClick} + getRowHref={getRowHref} + getRowTitle={getRowTitle} rowClassName={rowClassName} - isRowInteractive={isOccupancyRowInteractive} /> ); diff --git a/src/ui/src/features/occupancy/styles/occupancy.css b/src/ui/src/features/occupancy/styles/occupancy.css index 32eb84280..f757d60c4 100644 --- a/src/ui/src/features/occupancy/styles/occupancy.css +++ b/src/ui/src/features/occupancy/styles/occupancy.css @@ -35,7 +35,7 @@ } .occupancy-row--child { - @apply cursor-default text-sm; + @apply cursor-pointer text-sm; } .occupancy-row:not(.occupancy-row--child):hover {