diff --git a/libs/react/ui/src/components/dashboard/context/dashboard-context.tsx b/libs/react/ui/src/components/dashboard/context/dashboard-context.tsx index 362dab66..9e1e28b4 100644 --- a/libs/react/ui/src/components/dashboard/context/dashboard-context.tsx +++ b/libs/react/ui/src/components/dashboard/context/dashboard-context.tsx @@ -7,7 +7,14 @@ import type {VisibilityState} from '@tanstack/react-table'; import {createContext, type ReactNode, useCallback, useContext, useMemo, useState} from 'react'; -import type {DashboardState, FilterOption, ResourceType, TimePeriod, ViewColumn} from './types'; +import type { + DashboardState, + FilterOption, + ResourceType, + ResourceTypeOption, + TimePeriod, + ViewColumn, +} from './types'; import {updateViewColumnsFromVisibility, viewColumnsToVisibilityState} from './utils'; const DashboardContext = createContext(undefined); @@ -38,6 +45,32 @@ const DEFAULT_FILTERS: FilterOption[] = [ {id: 'running', label: 'Running', checked: false}, ]; +export const RESOURCE_TYPES: Array = [ + 'ci.pipeline', + 'ci.job', + 'ci.step', + 'test.run', + 'test.suite', + 'test.case', +] as const; + +export const RESOURCE_TYPE_LABELS: Record = { + 'ci.pipeline': 'CI Pipeline', + 'ci.job': 'CI Job', + 'ci.step': 'CI Step', + 'test.run': 'Test Run', + 'test.suite': 'Test Suite', + 'test.case': 'Test Case', +}; + +export const RESOURCE_TYPE_OPTIONS: ResourceTypeOption[] = [ + ...RESOURCE_TYPES.map((type) => ({ + id: type, + label: RESOURCE_TYPE_LABELS[type], + disabled: type.startsWith('test.'), + })), +]; + export interface DashboardProviderProps { children: ReactNode; /** @@ -62,7 +95,7 @@ export interface DashboardProviderProps { initialActiveSidebarItem?: string; /** * Initial resource type - * @default 'ci-pipeline' + * @default 'ci.pipeline' */ initialResourceType?: ResourceType; /** @@ -90,7 +123,7 @@ export function DashboardProvider({ initialFilters = DEFAULT_FILTERS, initialTimePeriod = '2days', initialActiveSidebarItem = 'reliability', - initialResourceType = 'ci-pipeline', + initialResourceType = 'ci.pipeline', columnMapping, }: DashboardProviderProps) { // State management diff --git a/libs/react/ui/src/components/dashboard/context/index.ts b/libs/react/ui/src/components/dashboard/context/index.ts index c343fbc5..c8d63883 100644 --- a/libs/react/ui/src/components/dashboard/context/index.ts +++ b/libs/react/ui/src/components/dashboard/context/index.ts @@ -3,8 +3,21 @@ */ export type {DashboardProviderProps} from './dashboard-context'; -export {DashboardProvider, useDashboardContext} from './dashboard-context'; -export type {DashboardState, FilterOption, ResourceType, TimePeriod, ViewColumn} from './types'; +export { + DashboardProvider, + RESOURCE_TYPE_LABELS, + RESOURCE_TYPE_OPTIONS, + RESOURCE_TYPES, + useDashboardContext, +} from './dashboard-context'; +export type { + DashboardState, + FilterOption, + ResourceType, + ResourceTypeOption, + TimePeriod, + ViewColumn, +} from './types'; export { DEFAULT_COLUMN_ID_TO_ACCESSOR_KEY, updateViewColumnsFromVisibility, diff --git a/libs/react/ui/src/components/dashboard/context/types.ts b/libs/react/ui/src/components/dashboard/context/types.ts index 77744433..533e4fdd 100644 --- a/libs/react/ui/src/components/dashboard/context/types.ts +++ b/libs/react/ui/src/components/dashboard/context/types.ts @@ -27,10 +27,22 @@ export interface FilterOption { */ export type TimePeriod = '1hour' | '1day' | '2days' | '7days' | '30days'; +export type ResourceType = + | 'ci.pipeline' + | 'ci.job' + | 'ci.step' + | 'test.run' + | 'test.suite' + | 'test.case'; + /** * Resource type option */ -export type ResourceType = 'ci-pipeline' | 'ci-jobs' | 'ci-steps' | 'runners' | 'suite' | 'cases'; +export interface ResourceTypeOption { + id: ResourceType; + label: string; + disabled?: boolean; +} /** * Dashboard context state diff --git a/libs/react/ui/src/components/dashboard/filters/expression-filter-bar.tsx b/libs/react/ui/src/components/dashboard/filters/expression-filter-bar.tsx deleted file mode 100644 index 9f88d11f..00000000 --- a/libs/react/ui/src/components/dashboard/filters/expression-filter-bar.tsx +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Expression Filter Bar Component - * - * A horizontal button group for filtering by resource type. - */ - -import {Button} from 'components/button'; -import type {ComponentProps} from 'react'; -import {cn} from 'utils/cn'; -import type {ResourceType} from '../context'; - -export interface ResourceTypeOption { - id: ResourceType; - label: string; - disabled?: boolean; -} - -export interface ExpressionFilterBarProps extends Omit, 'children'> { - /** - * Available resource type options - */ - options?: ResourceTypeOption[]; - /** - * Currently selected resource type - */ - value?: ResourceType; - /** - * Callback when resource type changes - */ - onValueChange?: (value: ResourceType) => void; -} - -/** - * Default resource type options - */ -const DEFAULT_OPTIONS: ResourceTypeOption[] = [ - {id: 'ci-pipeline', label: 'CI Pipeline'}, - {id: 'ci-jobs', label: 'CI Jobs'}, - {id: 'ci-steps', label: 'CI Steps'}, - {id: 'runners', label: 'Runners', disabled: true}, - {id: 'suite', label: 'Suite', disabled: true}, - {id: 'cases', label: 'Cases', disabled: true}, -]; - -/** - * Expression Filter Bar - * - * Displays a horizontal button group for selecting resource types. - * Integrates with the dashboard context for state management. - * - * @example - * ```tsx - * - * ``` - */ -export function ExpressionFilterBar({ - options = DEFAULT_OPTIONS, - value = 'ci-pipeline', - onValueChange, - className, - ...props -}: ExpressionFilterBarProps) { - return ( -
-
- {options.map((option) => { - const isActive = value === option.id; - return ( - - ); - })} -
-
- ); -} diff --git a/libs/react/ui/src/components/dashboard/filters/index.ts b/libs/react/ui/src/components/dashboard/filters/index.ts deleted file mode 100644 index f9af5af8..00000000 --- a/libs/react/ui/src/components/dashboard/filters/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Filter components exports - */ - -export type {ExpressionFilterBarProps, ResourceTypeOption} from './expression-filter-bar'; -export {ExpressionFilterBar} from './expression-filter-bar'; diff --git a/libs/react/ui/src/components/dashboard/index.ts b/libs/react/ui/src/components/dashboard/index.ts index 6fae4582..7807538d 100644 --- a/libs/react/ui/src/components/dashboard/index.ts +++ b/libs/react/ui/src/components/dashboard/index.ts @@ -18,14 +18,13 @@ export type { export { DashboardProvider, DEFAULT_COLUMN_ID_TO_ACCESSOR_KEY, + RESOURCE_TYPE_OPTIONS, updateViewColumnsFromVisibility, useDashboardContext, viewColumnsToVisibilityState, } from './context'; export type {DashboardProps} from './dashboard'; export {Dashboard} from './dashboard'; -export type {ExpressionFilterBarProps, ResourceTypeOption} from './filters'; -export {ExpressionFilterBar} from './filters'; export {AnalyticsPage, JobsPage} from './pages'; export type {TableWrapperProps} from './table'; export {TableWrapper} from './table'; diff --git a/libs/react/ui/src/components/dashboard/pages/analytics-page.tsx b/libs/react/ui/src/components/dashboard/pages/analytics-page.tsx index e5b38c3f..266c3ff6 100644 --- a/libs/react/ui/src/components/dashboard/pages/analytics-page.tsx +++ b/libs/react/ui/src/components/dashboard/pages/analytics-page.tsx @@ -14,7 +14,6 @@ import {type KpiCardProps, KpiCardsGroup} from '../components/kpi-card'; import {MobileSidebar} from '../components/mobile-sidebar'; import {defaultSidebarItems, Sidebar} from '../components/sidebar'; import {useDashboardContext} from '../context'; -import {ExpressionFilterBar} from '../filters'; import {TableWrapper} from '../table'; import {PageToolbar, ToolbarActions} from '../toolbar'; @@ -118,8 +117,6 @@ export function AnalyticsPage() { updateColumnVisibility, activeSidebarItem, setActiveSidebarItem, - resourceType, - setResourceType, } = useDashboardContext(); const isDesktop = useMediaQuery('(min-width: 1024px)'); @@ -169,8 +166,6 @@ export function AnalyticsPage() { secondaryAction={{label: 'Dismiss', onClick: () => undefined}} /> - - diff --git a/libs/react/ui/src/components/dashboard/toolbar/filter-button.tsx b/libs/react/ui/src/components/dashboard/toolbar/filter-button.tsx index 752f449c..96e65c39 100644 --- a/libs/react/ui/src/components/dashboard/toolbar/filter-button.tsx +++ b/libs/react/ui/src/components/dashboard/toolbar/filter-button.tsx @@ -1,114 +1,144 @@ import {Button} from 'components/button'; import { DropdownMenu, - DropdownMenuCheckboxItem, DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, DropdownMenuLabel, - DropdownMenuSeparator, DropdownMenuTrigger, } from 'components/dropdown-menu'; -import {Icon} from 'components/icon'; import {Kbd} from 'components/kbd'; +import type {HTMLAttributes} from 'react'; import {useEffect, useState} from 'react'; import {cn} from 'utils/cn'; +import {RESOURCE_TYPE_LABELS, RESOURCE_TYPE_OPTIONS} from '../context'; +import type {ResourceType} from '../context/types'; -export interface FilterOption { - id: string; - label: string; - checked: boolean; +export interface FilterButtonProps extends HTMLAttributes { + value: ResourceType; + onValueChange: (value: ResourceType) => void; } -export interface FilterButtonProps { - filters?: FilterOption[]; - onFiltersChange?: (filters: FilterOption[]) => void; - className?: string; -} +export function FilterButton({value, onValueChange, className, ...props}: FilterButtonProps) { + const [open, setOpen] = useState(false); + const filterOptions = RESOURCE_TYPE_OPTIONS.filter((opt) => !opt.disabled); + const normalizedValue = ( + filterOptions.some((opt) => opt.id === value) ? value : (filterOptions[0]?.id ?? value) + ) as ResourceType; + const selectedLabel = RESOURCE_TYPE_LABELS[normalizedValue] ?? normalizedValue; + const selectedIndex = filterOptions.findIndex((opt) => opt.id === normalizedValue); -const defaultFilters: FilterOption[] = [ - {id: 'success', label: 'Success', checked: false}, - {id: 'failed', label: 'Failed', checked: false}, - {id: 'neutral', label: 'Neutral', checked: false}, - {id: 'flaked', label: 'Flaked', checked: false}, - {id: 'running', label: 'Running', checked: false}, -]; + const handleFilterChange = (filterId: ResourceType) => { + onValueChange(filterId); + setOpen(false); + }; -export function FilterButton({ - filters: controlledFilters, - onFiltersChange, - className, -}: FilterButtonProps) { - const [internalFilters, setInternalFilters] = useState(defaultFilters); - const [open, setOpen] = useState(false); - const filters = controlledFilters ?? internalFilters; + const indicator = (index: number) => { + if (index === 0) { + return ; + } + return ; + }; - const handleFilterChange = (filterId: string, checked: boolean) => { - const updatedFilters = filters.map((f) => (f.id === filterId ? {...f, checked} : f)); - if (onFiltersChange) { - onFiltersChange(updatedFilters); - } else { - setInternalFilters(updatedFilters); + const calculatePaddingLeft = (index: number) => { + switch (index) { + case 0: + return 10; + case 1: + return 28; + case 2: + return 48; + default: + return 0; } }; - const activeCount = filters.filter((f) => f.checked).length; + const calculateLeftPosition = (index: number) => { + switch (index) { + case 0: + return 0; + case 1: + return 16; + case 2: + return 34; + default: + return 0; + } + }; - // Keyboard shortcut handler for "F" key useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { - // Check if key is 'f' or 'F' - if (event.key !== 'f' && event.key !== 'F') { - return; + if ( + event.key === 'ArrowDown' && + (event.metaKey || event.ctrlKey) && + event.target instanceof HTMLElement && + !['INPUT', 'TEXTAREA'].includes(event.target.tagName) && + !event.target.isContentEditable + ) { + event.preventDefault(); + setOpen(true); } - - // Ignore if event is from input, textarea, or contentEditable - const target = event.target as HTMLElement; - if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { - return; - } - - // Open the dropdown - event.preventDefault(); - setOpen(true); }; window.addEventListener('keydown', handleKeyDown); - return () => { - window.removeEventListener('keydown', handleKeyDown); - }; + return () => window.removeEventListener('keydown', handleKeyDown); }, []); return ( - - Status - - {filters.map((filter) => ( - handleFilterChange(filter.id, checked)} - closeOnSelect={false} - > - {filter.label} - - ))} + + CI Structure + + + {filterOptions.map((option, index) => { + return ( + handleFilterChange(option.id)} + style={{ + paddingLeft: calculatePaddingLeft(index), + }} + className={cn( + 'relative hover:text-foreground-neutral-base', + selectedIndex === index && 'text-foreground-neutral-base', + )} + > + {index !== 0 && ( + <> + + + + )} + + {indicator(index)} + {option.label} + + + ); + })} + ); diff --git a/libs/react/ui/src/components/dashboard/toolbar/toolbar-actions.tsx b/libs/react/ui/src/components/dashboard/toolbar/toolbar-actions.tsx index 13deeda8..71f6e345 100644 --- a/libs/react/ui/src/components/dashboard/toolbar/toolbar-actions.tsx +++ b/libs/react/ui/src/components/dashboard/toolbar/toolbar-actions.tsx @@ -65,11 +65,12 @@ export function ToolbarActions({ children, ...props }: ToolbarActionsProps) { - const {setSearchQuery, filters, setFilters, columns, setColumns} = useDashboardContext(); + const {setSearchQuery, resourceType, setResourceType, columns, setColumns} = + useDashboardContext(); return (
- {showFilter && } + {showFilter && } {showSearch && } {showView && } {children}