From e27b3b0959cf149f5f2d6ff9ea3202fcbb1eb4fd Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Mon, 3 Aug 2026 08:21:19 -0400 Subject: [PATCH 01/25] Convert node & rebalance lists to data view, refine rebalance display Assisted-by: IBM Bob Signed-off-by: Michael Edgar --- api/src/main/webui/src/api/hooks/useNodes.ts | 99 +-- .../main/webui/src/api/hooks/useRebalances.ts | 90 +- .../webui/src/api/hooks/useResourceList.ts | 6 +- api/src/main/webui/src/api/types.ts | 38 +- .../common/ResourceListDataView.tsx | 36 +- .../kafka/nodes/NodeStatusLabel.tsx | 6 +- .../components/kafka/nodes/NodesDataView.tsx | 335 ++++++++ .../kafka/nodes/RebalancesDataView.tsx | 325 ++++++++ .../kafka/nodes/RebalancesTable.tsx | 2 +- .../kafka/topics/AdvancedSearch.tsx | 2 +- api/src/main/webui/src/i18n/messages/en.json | 63 +- .../webui/src/pages/kafka/KafkaLayout.tsx | 4 +- .../pages/kafka/nodes/NodesOverviewTab.tsx | 781 ++---------------- .../webui/src/pages/kafka/nodes/NodesPage.tsx | 2 +- .../pages/kafka/nodes/NodesRebalancesTab.tsx | 442 ++-------- .../pages/kafka/overview/KafkaOverview.tsx | 4 +- 16 files changed, 906 insertions(+), 1329 deletions(-) create mode 100644 api/src/main/webui/src/components/kafka/nodes/NodesDataView.tsx create mode 100644 api/src/main/webui/src/components/kafka/nodes/RebalancesDataView.tsx diff --git a/api/src/main/webui/src/api/hooks/useNodes.ts b/api/src/main/webui/src/api/hooks/useNodes.ts index be4509ff3..c1d053c0e 100644 --- a/api/src/main/webui/src/api/hooks/useNodes.ts +++ b/api/src/main/webui/src/api/hooks/useNodes.ts @@ -5,96 +5,33 @@ import { useQuery } from '@tanstack/react-query'; import { apiClient } from '../client'; import { - NodesResponse, NodeConfigResponse, - BrokerStatus, - ControllerStatus, - NodeRoles, + Node, + NodeListMeta, } from '../types'; +import { ResourceListParams, useResourceList } from './useResourceList'; /** - * Fetch all nodes for a Kafka cluster + * Fetch all nodes for a Kafka cluster. + * + * Filter keys (pass via params.filters): + * nodePool – string array, matched with 'in' + * roles – string array, matched with 'in' + * broker.status – string array, matched with 'in' + * controller.status – string array, matched with 'in' */ export function useNodes( kafkaId: string | undefined, - params?: { - pageSize?: number; - pageCursor?: string; - sort?: string; - sortDir?: 'asc' | 'desc'; - nodePool?: string[]; - roles?: NodeRoles[]; - brokerStatus?: BrokerStatus[]; - controllerStatus?: ControllerStatus[]; - fields?: string[]; - } + params?: ResourceListParams, ) { - return useQuery({ - queryKey: [ - 'nodes', - kafkaId, - params?.pageSize, - params?.pageCursor, - params?.sort, - params?.sortDir, - params?.nodePool, - params?.roles, - params?.brokerStatus, - params?.controllerStatus, - params?.fields, - ], - queryFn: async () => { - if (!kafkaId) { - throw new Error('Kafka ID is required'); - } - - const searchParams = new URLSearchParams(); - - if (params?.pageSize) { - searchParams.set('page[size]', params.pageSize.toString()); - } - - // Handle cursor-based pagination - if (params?.pageCursor) { - if (params.pageCursor.startsWith('after:')) { - searchParams.set('page[after]', params.pageCursor.slice(6)); - } else if (params.pageCursor.startsWith('before:')) { - searchParams.set('page[before]', params.pageCursor.slice(7)); - } - } - - if (params?.sort) { - const sortPrefix = params.sortDir === 'desc' ? '-' : ''; - searchParams.set('sort', `${sortPrefix}${params.sort}`); - } - - if (params?.nodePool && params.nodePool.length > 0) { - searchParams.set('filter[nodePool]', `in,${params.nodePool.join(',')}`); - } - - if (params?.roles && params.roles.length > 0) { - searchParams.set('filter[roles]', `in,${params.roles.join(',')}`); - } - - if (params?.brokerStatus && params.brokerStatus.length > 0) { - searchParams.set('filter[broker.status]', `in,${params.brokerStatus.join(',')}`); - } - - if (params?.controllerStatus && params.controllerStatus.length > 0) { - searchParams.set('filter[controller.status]', `in,${params.controllerStatus.join(',')}`); - } - - if (params?.fields) { - searchParams.set('fields[nodes]', params.fields.join(',')); - } - - const queryString = searchParams.toString(); - const path = `/api/kafkas/${kafkaId}/nodes${queryString ? `?${queryString}` : ''}`; - - return apiClient.get(path); + return useResourceList( + 'nodes', + `/api/kafkas/${kafkaId}/nodes`, + { + ...params, + enabled: !!kafkaId && (params?.enabled ?? true), }, - enabled: !!kafkaId, - }); + ); } /** diff --git a/api/src/main/webui/src/api/hooks/useRebalances.ts b/api/src/main/webui/src/api/hooks/useRebalances.ts index 5bcf424c6..70cb1a085 100644 --- a/api/src/main/webui/src/api/hooks/useRebalances.ts +++ b/api/src/main/webui/src/api/hooks/useRebalances.ts @@ -3,88 +3,36 @@ */ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import escape from '../utils/escape'; import { apiClient } from '../client'; import { - RebalancesResponse, RebalanceResponse, - RebalanceStatus, - RebalanceMode, + Rebalance, } from '../types'; +import { ResourceListParams, useResourceList } from './useResourceList'; + +const REBALANCE_FIELDS = 'name,namespace,creationTimestamp,status,mode,brokers,optimizationResult,conditions'; /** - * Fetch all rebalances for a Kafka cluster + * Fetch all rebalances for a Kafka cluster. + * + * Filter keys (pass via params.filters): + * name – string, matched with 'like' + * status – string array, matched with 'in' + * mode – string array, matched with 'in' */ export function useRebalances( kafkaId: string | undefined, - params?: { - pageSize?: number; - pageCursor?: string; - sort?: string; - sortDir?: 'asc' | 'desc'; - name?: string; - status?: RebalanceStatus[]; - mode?: RebalanceMode[]; - } + params?: ResourceListParams, ) { - return useQuery({ - queryKey: [ - 'rebalances', - kafkaId, - params?.pageSize, - params?.pageCursor, - params?.sort, - params?.sortDir, - params?.name, - params?.status, - params?.mode, - ], - queryFn: async () => { - if (!kafkaId) { - throw new Error('Kafka ID is required'); - } - - const searchParams = new URLSearchParams(); - - // Always include these fields - searchParams.set( - 'fields[kafkaRebalances]', - 'name,namespace,creationTimestamp,status,mode,brokers,optimizationResult,conditions' - ); - - if (params?.pageSize) { - searchParams.set('page[size]', params.pageSize.toString()); - } - - // Handle cursor-based pagination - if (params?.pageCursor) { - searchParams.set('page[after]', params.pageCursor); - } - - if (params?.sort) { - const sortPrefix = params.sortDir === 'desc' ? '-' : ''; - searchParams.set('sort', `${sortPrefix}${params.sort}`); - } - - if (params?.name) { - searchParams.set('filter[name]', `like,*${escape(params.name)}*`); - } - - if (params?.status && params.status.length > 0) { - searchParams.set('filter[status]', `in,${params.status.join(',')}`); - } - - if (params?.mode && params.mode.length > 0) { - searchParams.set('filter[mode]', `in,${params.mode.join(',')}`); - } - - const queryString = searchParams.toString(); - const path = `/api/kafkas/${kafkaId}/rebalances${queryString ? `?${queryString}` : ''}`; - - return apiClient.get(path); + return useResourceList( + 'kafkaRebalances', + `/api/kafkas/${kafkaId}/rebalances`, + { + fields: REBALANCE_FIELDS, + ...params, + enabled: !!kafkaId && (params?.enabled ?? true), }, - enabled: !!kafkaId, - }); + ); } /** diff --git a/api/src/main/webui/src/api/hooks/useResourceList.ts b/api/src/main/webui/src/api/hooks/useResourceList.ts index d9c1dee40..78981a08a 100644 --- a/api/src/main/webui/src/api/hooks/useResourceList.ts +++ b/api/src/main/webui/src/api/hooks/useResourceList.ts @@ -5,7 +5,7 @@ import { useQuery } from '@tanstack/react-query'; import escape from '../utils/escape'; import { apiClient } from '../client'; -import { ListResponse, Resource } from '../types'; +import { AbstractMeta, ListResponse, Resource } from '../types'; export interface ResourceListPageParams { size?: number | null; @@ -73,7 +73,7 @@ function updatePageParams(page: ResourceListPageParams, searchParams: URLSearchP } } -export function useResourceList( +export function useResourceList( resourceType: string, path: string, params?: ResourceListParams, @@ -115,7 +115,7 @@ export function useResourceList( const queryString = searchParams.toString(); const url = path + (queryString ? `?${queryString}` : ''); - return apiClient.get>(url); + return apiClient.get>(url); }, enabled: params?.enabled, refetchInterval: params?.refreshInterval, diff --git a/api/src/main/webui/src/api/types.ts b/api/src/main/webui/src/api/types.ts index 739617fdf..1141b06ca 100644 --- a/api/src/main/webui/src/api/types.ts +++ b/api/src/main/webui/src/api/types.ts @@ -16,8 +16,8 @@ export type PaginationMeta = { rangeTruncated: boolean; }; -export interface ListResponse { - meta?: AbstractMeta & { +export interface ListResponse { + meta?: M & { page: PaginationMeta; }; links?: { @@ -100,6 +100,7 @@ export interface KafkaCluster extends Resource { status?: string; kafkaVersion?: string; creationTimestamp?: string; + cruiseControlEnabled?: boolean; listeners?: KafkaClusterListener[]; conditions?: KafkaClusterCondition[]; }; @@ -298,12 +299,20 @@ export interface NodePoolMeta { export type NodePools = Record; -export type Statuses = Record< +export type NodeStatuses = Record< 'brokers' | 'controllers' | 'combined', Record >; -export interface Node { +export interface NodeListMeta extends MetaWithPrivileges { + summary: { + nodePools: NodePools; + statuses: NodeStatuses; + leaderId?: string; + }; +} + +export interface Node extends Resource { id: string; type: 'nodes'; meta?: MetaWithPrivileges; @@ -332,27 +341,6 @@ export interface Node { }; } -export interface NodesResponse { - data: Node[]; - meta: { - summary: { - nodePools: NodePools; - statuses: Statuses; - leaderId?: string; - }; - page: { - total: number; - pageNumber?: number; - }; - }; - links: { - first: string | null; - prev: string | null; - next: string | null; - last: string | null; - }; -} - export interface NodeConfigResponse { data: { id?: string; diff --git a/api/src/main/webui/src/components/common/ResourceListDataView.tsx b/api/src/main/webui/src/components/common/ResourceListDataView.tsx index 36c582782..756cd798d 100644 --- a/api/src/main/webui/src/components/common/ResourceListDataView.tsx +++ b/api/src/main/webui/src/components/common/ResourceListDataView.tsx @@ -14,6 +14,7 @@ import { DataViewState, useDataViewSort, DataViewTextFilterProps, + ExpandableContent, } from '@patternfly/react-data-view'; /* * The following import is a work-around for @@ -183,8 +184,17 @@ export interface ResourceListDataViewColumnMapper { ): DataViewTh[]; } +export interface ResourceListDataViewRowResult { + row: DataViewTr; + expandedRows?: ExpandableContent[]; +} + +function isRowResult(v: DataViewTr | ResourceListDataViewRowResult): v is ResourceListDataViewRowResult { + return v !== null && typeof v === 'object' && !Array.isArray(v) && 'expandedRows' in v; +} + export interface ResourceListDataViewRowMapper { - (entity: T): DataViewTr; + (entity: T): DataViewTr | ResourceListDataViewRowResult; } export interface ResourceListDataViewProps { @@ -434,17 +444,17 @@ export function ResourceListDataView({ return columnProvider.callback(sortBy, direction, onSort); }, [sortBy, direction, onSort, columnProvider]); - // Determine the active state, errors, and table rows for DataView - const [ activeState, errors, rows ] = useMemo(() => { + // Determine the active state, errors, table rows, and expanded rows for DataView + const [ activeState, errors, rows, expandedRows ] = useMemo(() => { if (resourceResult.isLoading) { - return [ DataViewState.loading, undefined, [] ]; + return [ DataViewState.loading, undefined, [], [] ]; } if (resourceResult?.error) { const e = resourceResult.error; if (e instanceof ApiError) { - return [ DataViewState.error, e.errors, [] ]; + return [ DataViewState.error, e.errors, [], [] ]; } const errObjects = [{ @@ -452,18 +462,18 @@ export function ResourceListDataView({ detail: e.toString(), }]; - return [ DataViewState.error, errObjects, [] ]; + return [ DataViewState.error, errObjects, [], [] ]; } if (listResponse?.data && listResponse?.data.length === 0) { - return [ DataViewState.empty, [], [] ]; + return [ DataViewState.empty, [], [], [] ]; } - return [ - undefined, - [], - listResponse?.data?.map(entry => rowProvider.callback(entry)) ?? [] - ]; + const results = listResponse?.data?.map(entry => rowProvider.callback(entry)) ?? []; + const tableRows = results.map(r => isRowResult(r) ? r.row : r); + const allExpandedRows = results.flatMap(r => isRowResult(r) ? (r.expandedRows ?? []) : []); + + return [ undefined, [], tableRows, allExpandedRows ]; }, [ resourceResult.isLoading, resourceResult.error, listResponse, rowProvider ]); useEffect(() => { @@ -630,6 +640,8 @@ export function ResourceListDataView({ ouiaId={`${ouiaIdPrefix}-table`} columns={columns} rows={rows} + isExpandable={expandedRows.length > 0} + expandedRows={expandedRows} headStates={{ [DataViewState.loading]: headLoading }} diff --git a/api/src/main/webui/src/components/kafka/nodes/NodeStatusLabel.tsx b/api/src/main/webui/src/components/kafka/nodes/NodeStatusLabel.tsx index 01124e949..26129c0de 100644 --- a/api/src/main/webui/src/components/kafka/nodes/NodeStatusLabel.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/NodeStatusLabel.tsx @@ -18,7 +18,7 @@ import { InProgressIcon, PendingIcon, } from '@patternfly/react-icons'; -import type { BrokerStatus, ControllerStatus, NodeRoles, Statuses } from '@/api/types'; +import type { BrokerStatus, ControllerStatus, NodeRoles, NodeStatuses } from '@/api/types'; // Icon component for new process (recovery status) const NewProcessIcon = () => ( @@ -39,7 +39,7 @@ const NewProcessIcon = () => ( * Role labels with counts */ export const useRoleLabels = ( - statuses?: Statuses + statuses?: NodeStatuses ): Record => { const { t } = useTranslation(); @@ -301,4 +301,4 @@ export const useControllerStatusLabelsWithCount = ( ): Record => { const labels = useControllerStatusLabels(); return generateStatusLabelsWithCount(labels, statuses); -}; \ No newline at end of file +}; diff --git a/api/src/main/webui/src/components/kafka/nodes/NodesDataView.tsx b/api/src/main/webui/src/components/kafka/nodes/NodesDataView.tsx new file mode 100644 index 000000000..fd9e842ca --- /dev/null +++ b/api/src/main/webui/src/components/kafka/nodes/NodesDataView.tsx @@ -0,0 +1,335 @@ +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router'; +import { DataViewTd } from '@patternfly/react-data-view'; +import { ThProps } from '@patternfly/react-table'; +import { UseQueryResult } from '@tanstack/react-query'; +import { + ClipboardCopy, + Content, + Flex, + FlexItem, + Label, + Tooltip, +} from '@patternfly/react-core'; +import { HelpIcon } from '@patternfly/react-icons'; +import { ChartDonutUtilization } from '@patternfly/react-charts/victory'; +import { Node, ListResponse, BrokerStatus, ControllerStatus, NodeListMeta } from '@/api/types'; +import { ResourceListParams } from '@/api/hooks/useResourceList'; +import { + ResourceListDataView, + ResourceListDataViewColumnMapper, + ResourceListDataViewRowMapper, + ResourceListDataViewRowResult, +} from '@/components/common/ResourceListDataView'; +import { + useRoleLabels, + useBrokerStatusLabels, + useControllerStatusLabels, +} from './NodeStatusLabel'; +import { formatNumber, formatBytes } from '@/utils/format'; +import type { ChartDatum } from '@/components/kafka/overview/utils/types'; + +const columnNames = ['id', 'roles', 'status', 'replicas', 'rack', 'nodePool'] as const; + +interface NodesDataViewProps { + kafkaId: string; + nodeResult: UseQueryResult, Error>; + onDataViewChange: (params: ResourceListParams) => void; +} + +export function NodesDataView({ + kafkaId, + nodeResult, + onDataViewChange, +}: NodesDataViewProps) { + const { t } = useTranslation(); + const roleLabels = useRoleLabels(); + const brokerStatusLabels = useBrokerStatusLabels(); + const controllerStatusLabels = useControllerStatusLabels(); + + const nodePoolFilterOptions = useMemo(() => { + const nodePools = nodeResult.data?.meta?.summary?.nodePools; + if (!nodePools) return []; + return Object.entries(nodePools).map(([name, meta]) => ({ + value: name, + label: ( + <> + + {name} + + {meta.count} + + +
+ {t('nodes.filter.nodePoolRoles', { roles: meta.roles.join(', ') })} +
+ + ), + })); + }, [nodeResult.data?.meta, t]); + + const handleSort = useCallback(( + onSort: ((event: React.MouseEvent, sortBy: string, direction: 'asc' | 'desc') => void) | undefined, + event: React.MouseEvent, + columnIndex: number, + direction: 'asc' | 'desc', + ) => { + onSort?.(event, columnNames[columnIndex], direction); + }, []); + + const colMapper: ResourceListDataViewColumnMapper = useCallback( + (sortBy, direction, onSort) => [ + { + cell: t('nodes.nodeId'), + props: { + sort: { + sortBy: { + index: sortBy ? columnNames.indexOf(sortBy as typeof columnNames[number]) : undefined, + direction, + }, + columnIndex: 0, + onSort: (event, columnIndex, dir) => handleSort(onSort, event, columnIndex, dir), + } as ThProps['sort'], + }, + }, + { cell: t('nodes.roles') }, + { cell: t('nodes.status') }, + { cell: t('nodes.kafkaVersion') }, + { + cell: ( + <> + {t('nodes.replicas')}{' '} + + + + + ), + props: { modifier: 'fitContent', style: { textAlign: 'right' } }, + }, + { + cell: ( + <> + {t('nodes.leaders')}{' '} + + + + + ), + props: { modifier: 'fitContent', style: { textAlign: 'right' } }, + }, + { + cell: ( + <> + {t('nodes.rack')}{' '} + + + + + ), + }, + { cell: t('nodes.nodePool') }, + ], + [t, handleSort], + ); + + const colProvider = useMemo(() => ({ + dependencies: [t, handleSort], + callback: colMapper, + }), [colMapper, t, handleSort]); + + const rowMapper: ResourceListDataViewRowMapper = useCallback( + (node): ResourceListDataViewRowResult => { + const diskCapacity = node.attributes.storageCapacity; + const diskUsage = node.attributes.storageUsed; + const usedCapacity = + diskUsage != null && diskCapacity != null + ? diskUsage / diskCapacity + : undefined; + + return { + row: { + id: node.id, + row: [ + { + // id on the first cell is how DataViewTableBasic matches expandedRows entries + id: node.id, + cell: ( + <> + {node.meta?.privileges?.includes('GET') === true ? ( + + {node.id} + + ) : ( + node.id + )} + {node.attributes.metadataState?.status === 'leader' && ( + + )} + + ), + props: { dataLabel: t('nodes.nodeId'), modifier: 'nowrap' }, + } as DataViewTd, + { + cell: ( + <>{node.attributes.roles?.map((role) => ( +
{roleLabels[role].label}
+ ))} + ), + props: { dataLabel: t('nodes.roles'), modifier: 'nowrap' }, + }, + { + cell: ( + <> +
+ {node.attributes.broker && brokerStatusLabels[node.attributes.broker.status]} +
+
+ {node.attributes.controller && controllerStatusLabels[node.attributes.controller.status]} +
+ + ), + props: { dataLabel: t('nodes.status'), modifier: 'nowrap' }, + }, + { + cell: node.attributes.kafkaVersion, + props: { dataLabel: t('nodes.kafkaVersion'), modifier: 'nowrap' }, + }, + { + cell: typeof node.attributes.broker?.leaderCount === 'number' && + typeof node.attributes.broker?.replicaCount === 'number' + ? formatNumber(node.attributes.broker.leaderCount + node.attributes.broker.replicaCount) + : '-', + props: { dataLabel: t('nodes.replicas'), modifier: 'fitContent', style: { textAlign: 'right' } }, + }, + { + cell: typeof node.attributes.broker?.leaderCount === 'number' + ? formatNumber(node.attributes.broker.leaderCount) + : '-', + props: { dataLabel: t('nodes.leaders'), modifier: 'fitContent', style: { textAlign: 'right' } }, + }, + { + cell: node.attributes.rack || 'n/a', + props: { dataLabel: t('nodes.rack'), modifier: 'nowrap' }, + }, + { + cell: node.attributes.nodePool || 'n/a', + props: { dataLabel: t('nodes.nodePool'), modifier: 'nowrap' }, + }, + ], + }, + expandedRows: [{ + rowId: node.id as unknown as number, + columnId: 0, + content: ( + + + + {t('nodes.hostName')} + + + {node.attributes.host || 'n/a'} + + + + + + + {t('nodes.diskUsage')} + + {usedCapacity !== undefined && ( +
+ + datum.x ? `${datum.x}: ${datum.y.toFixed(1)}%` : null + } + legendData={[ + { name: `Used capacity: ${formatBytes(diskUsage!)}` }, + { name: `Available: ${formatBytes(diskCapacity! - diskUsage!)}` }, + ]} + legendOrientation="vertical" + legendPosition="bottom" + padding={{ bottom: 75, left: 20, right: 20, top: 20 }} + title={`${(usedCapacity * 100).toFixed(1)}%`} + subTitle={`of ${formatBytes(diskCapacity!)}`} + thresholds={[{ value: 60 }, { value: 90 }]} + height={300} + width={230} + /> +
+ )} +
+ + + {t('nodes.kafkaVersion')} + +
{node.attributes.kafkaVersion ?? 'Unknown'}
+
+
+ ), + }], + }; + }, + [kafkaId, t, roleLabels, brokerStatusLabels, controllerStatusLabels], + ); + + const rowProvider = useMemo(() => ({ + dependencies: [kafkaId, t, roleLabels, brokerStatusLabels, controllerStatusLabels], + callback: rowMapper, + }), [rowMapper, kafkaId, t, roleLabels, brokerStatusLabels, controllerStatusLabels]); + + + return ( + ({ + value: role, + label: roleLabels[role].label, + })), + }, + 'broker.status': { + type: 'checkbox', + title: t('nodes.filter.brokerStatus'), + placeholder: t('nodes.filter.statusPlaceholder'), + options: (Object.keys(brokerStatusLabels) as BrokerStatus[]).map((status) => ({ + value: status, + label: brokerStatusLabels[status], + })), + }, + 'controller.status': { + type: 'checkbox', + title: t('nodes.filter.controllerStatus'), + placeholder: t('nodes.filter.statusPlaceholder'), + options: (Object.keys(controllerStatusLabels) as ControllerStatus[]).map((status) => ({ + value: status, + label: controllerStatusLabels[status], + })), + }, + }} + columnProvider={colProvider} + rowProvider={rowProvider} + /> + ); +} diff --git a/api/src/main/webui/src/components/kafka/nodes/RebalancesDataView.tsx b/api/src/main/webui/src/components/kafka/nodes/RebalancesDataView.tsx new file mode 100644 index 000000000..b17cefaab --- /dev/null +++ b/api/src/main/webui/src/components/kafka/nodes/RebalancesDataView.tsx @@ -0,0 +1,325 @@ +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router'; +import { DataViewTd } from '@patternfly/react-data-view'; +import { ThProps, ActionsColumn } from '@patternfly/react-table'; +import { UseQueryResult } from '@tanstack/react-query'; +import { + Badge, + Button, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Flex, + FlexItem, + List, + ListItem, + Popover, +} from '@patternfly/react-core'; +import { HelpIcon } from '@patternfly/react-icons'; +import { Rebalance, ListResponse } from '@/api/types'; +import { ResourceListParams } from '@/api/hooks/useResourceList'; +import { + ResourceListDataView, + ResourceListDataViewColumnMapper, + ResourceListDataViewRowMapper, + ResourceListDataViewRowResult, +} from '@/components/common/ResourceListDataView'; +import { StatusLabel } from '@/components/StatusLabel'; +import { createRebalanceStatusConfig } from '@/components/StatusLabel/configs'; +import { hasPrivilege } from '@/utils/privileges'; +import { formatDateTime } from '@/utils/dateTime'; + +const columnNames = ['name', 'status', 'lastUpdated', 'dataToMove', 'partitionsToMove', 'leadershipUpdates', 'estTimeToComplete'] as const; + +function getLastUpdated(rebalance: Rebalance): string { + const statusCondition = rebalance.attributes.conditions?.find( + (c) => c.type === rebalance.attributes.status, + ); + return statusCondition?.lastTransitionTime || rebalance.attributes.creationTimestamp || ''; +} + +interface RebalancesDataViewProps { + kafkaId: string; + rebalanceResult: UseQueryResult, Error>; + onDataViewChange: (params: ResourceListParams) => void; + onApprove: (rebalance: Rebalance) => void; + onStop: (rebalance: Rebalance) => void; + onRefresh: (rebalance: Rebalance) => void; + onViewDetails: (rebalance: Rebalance) => void; +} + +export function RebalancesDataView({ + kafkaId, + rebalanceResult, + onDataViewChange, + onApprove, + onStop, + onRefresh, + onViewDetails, +}: RebalancesDataViewProps) { + const { t } = useTranslation(); + const statusConfig = useMemo(() => createRebalanceStatusConfig(t), [t]); + + const handleSort = useCallback(( + onSort: ((event: React.MouseEvent, sortBy: string, direction: 'asc' | 'desc') => void) | undefined, + event: React.MouseEvent, + columnIndex: number, + direction: 'asc' | 'desc', + ) => { + onSort?.(event, columnNames[columnIndex], direction); + }, []); + + const colMapper: ResourceListDataViewColumnMapper = useCallback( + (sortBy, direction, onSort) => [ + { + cell: t('rebalancing.rebalanceName'), + props: { + width: 30, + sort: { + sortBy: { + index: sortBy ? columnNames.indexOf(sortBy as typeof columnNames[number]) : undefined, + direction, + }, + columnIndex: 0, + onSort: (event, columnIndex, dir) => handleSort(onSort, event, columnIndex, dir), + } as ThProps['sort'], + }, + }, + { + cell: t('rebalancing.status'), + props: { + sort: { + sortBy: { + index: sortBy ? columnNames.indexOf(sortBy as typeof columnNames[number]) : undefined, + direction, + }, + columnIndex: 1, + onSort: (event, columnIndex, dir) => handleSort(onSort, event, columnIndex, dir), + } as ThProps['sort'], + }, + }, + { cell: t('rebalancing.dataToMove'), props: { modifier: 'nowrap' } }, + { cell: t('rebalancing.partitionsToMove'), props: { modifier: 'nowrap' } }, + { cell: t('rebalancing.leadershipUpdates'), props: { modifier: 'nowrap' } }, + /* { cell: t('rebalancing.estTimeToComplete'), props: { modifier: 'nowrap' } }, */ + { + cell: t('rebalancing.lastUpdated'), + props: { + sort: { + sortBy: { + index: sortBy ? columnNames.indexOf(sortBy as typeof columnNames[number]) : undefined, + direction, + }, + columnIndex: 2, + onSort: (event, columnIndex, dir) => handleSort(onSort, event, columnIndex, dir), + } as ThProps['sort'], + }, + }, + { cell: '' }, // actions column + ], + [t, handleSort], + ); + + const colProvider = useMemo(() => ({ + dependencies: [t, handleSort], + callback: colMapper, + }), [colMapper, t, handleSort]); + + const rowMapper: ResourceListDataViewRowMapper = useCallback( + (rebalance): ResourceListDataViewRowResult => { + const canUpdate = hasPrivilege('UPDATE', rebalance); + const lastUpdated = getLastUpdated(rebalance); + + return { + row: { + id: rebalance.id, + row: [ + { + id: rebalance.id, + cell: ( + + ), + props: { dataLabel: t('rebalancing.rebalanceName') }, + } as DataViewTd, + { + cell: ( + + ), + props: { dataLabel: t('rebalancing.status') }, + }, + { + cell: rebalance.attributes.optimizationResult?.dataToMoveMB != null + ? `${rebalance.attributes.optimizationResult.dataToMoveMB} MB` + : '–', + props: { dataLabel: t('rebalancing.dataToMove') }, + }, + { + cell: rebalance.attributes.optimizationResult?.numReplicaMovements ?? '–', + props: { dataLabel: t('rebalancing.partitionsToMove') }, + }, + { + cell: rebalance.attributes.optimizationResult?.numLeaderMovements ?? '–', + props: { dataLabel: t('rebalancing.leadershipUpdates') }, + }, + /* { + cell: '–', + props: { dataLabel: t('rebalancing.estTimeToComplete') }, + }, */ + { + cell: formatDateTime({ value: lastUpdated }), + props: { dataLabel: t('rebalancing.lastUpdated') }, + }, + { + cell: ( + onApprove(rebalance), + isDisabled: !canUpdate || !rebalance.meta?.allowedActions?.includes('approve'), + }, + { + title: t('rebalancing.refresh'), + onClick: () => onRefresh(rebalance), + isDisabled: !canUpdate || !rebalance.meta?.allowedActions?.includes('refresh'), + }, + { + title: t('rebalancing.stop'), + onClick: () => onStop(rebalance), + isDisabled: !canUpdate || !rebalance.meta?.allowedActions?.includes('stop'), + }, + ]} + /> + ), + props: { isActionCell: true }, + }, + ], + }, + expandedRows: [{ + rowId: rebalance.id as unknown as number, + columnId: 0, + content: ( + + + + + {t('rebalancing.autoApprovalEnabled')} + + {rebalance.meta?.autoApproval === true ? 'true' : 'false'} + + + + + + + {t('rebalancing.mode')}{' '} + {t('rebalancing.rebalanceMode')}} + bodyContent={ +
+ + + {t('rebalancing.fullMode')}{' '} + {t('rebalancing.fullModeDescription')} + + + {t('rebalancing.addBrokersMode')}{' '} + {t('rebalancing.addBrokersModeDescription')} + + + {t('rebalancing.removeBrokersMode')}{' '} + {t('rebalancing.removeBrokersModeDescription')} + + +
+ } + > + +
+
+ + {rebalance.attributes.mode === 'full' ? ( + t('rebalancing.fullMode') + ) : ( + <> + {rebalance.attributes.mode === 'add-brokers' + ? t('rebalancing.addBrokersMode') + : t('rebalancing.removeBrokersMode')}{' '} + {rebalance.attributes.brokers?.length + ? rebalance.attributes.brokers.map((b, index) => ( + + + {t('rebalancing.broker', { b })} + + {index < (rebalance.attributes.brokers?.length || 0) - 1 && ', '} + + )) + : ''} + + )} + +
+
+
+
+ ), + }], + }; + }, + [kafkaId, t, statusConfig, onApprove, onStop, onRefresh, onViewDetails], + ); + + const rowProvider = useMemo(() => ({ + dependencies: [kafkaId, t, statusConfig, onApprove, onStop, onRefresh, onViewDetails], + callback: rowMapper, + }), [rowMapper, kafkaId, t, statusConfig, onApprove, onStop, onRefresh, onViewDetails]); + + return ( + ({ + value: s, + label: , + })), + }, + mode: { + type: 'checkbox', + title: t('rebalancing.mode'), + placeholder: t('rebalancing.filter.modePlaceholder'), + options: [ + { value: 'full', label: t('rebalancing.fullMode') }, + { value: 'add-brokers', label: t('rebalancing.addBrokersMode') }, + { value: 'remove-brokers', label: t('rebalancing.removeBrokersMode') }, + ], + }, + }} + columnProvider={colProvider} + rowProvider={rowProvider} + /> + ); +} diff --git a/api/src/main/webui/src/components/kafka/nodes/RebalancesTable.tsx b/api/src/main/webui/src/components/kafka/nodes/RebalancesTable.tsx index eb276c3c4..285152217 100644 --- a/api/src/main/webui/src/components/kafka/nodes/RebalancesTable.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/RebalancesTable.tsx @@ -146,7 +146,7 @@ export function RebalancesTable({ isInline onClick={() => handleRebalanceClick(rebalance)} > - {t('rebalancing.crBadge')} {rebalance.attributes.name} + {rebalance.attributes.name} diff --git a/api/src/main/webui/src/components/kafka/topics/AdvancedSearch.tsx b/api/src/main/webui/src/components/kafka/topics/AdvancedSearch.tsx index a2a63b909..49314c797 100644 --- a/api/src/main/webui/src/components/kafka/topics/AdvancedSearch.tsx +++ b/api/src/main/webui/src/components/kafka/topics/AdvancedSearch.tsx @@ -232,7 +232,7 @@ export function AdvancedSearch({
- + diff --git a/api/src/main/webui/src/i18n/messages/en.json b/api/src/main/webui/src/i18n/messages/en.json index 12121b758..9fa393ddc 100644 --- a/api/src/main/webui/src/i18n/messages/en.json +++ b/api/src/main/webui/src/i18n/messages/en.json @@ -11,7 +11,6 @@ "create": "Create", "search": "Search", "filterByName": "Filter by name", - "filter": "Filter", "clear": "Clear", "apply": "Apply", "close": "Close", @@ -32,7 +31,12 @@ "noResultsFound": "No results found", "noResultsFoundDescription": "No results match the filter criteria. Clear all filters and try again.", "clearAllFilters": "Clear all filters", - "refreshDataTooltip": "Refresh data
Last update: {{lastRefresh}}" + "refreshDataTooltip": "Refresh data
Last update: {{lastRefresh}}", + "filter": { + "label": "Filter", + "namePlaceholder": "Filter by name", + "statusPlaceholder": "Filter by status" + } }, "about": { "buttonLabel": "About", @@ -430,10 +434,10 @@ "nodePool": "Node Pool", "allNodePoolsPlaceholder": "All node pools", "roles": "Roles", - "status": "Status", - "statusPlaceholder": "All statuses", "replicas": "Total Replicas", "replicasTooltip": "The overall count of partition replicas hosted by the broker. Replicas provide fault tolerance and data availability.", + "leaders": "Leader partitions", + "leadersTooltip": "The number of partition replicas for which this broker is currently the elected leader. Leaders handle all reads and writes for their partitions.", "diskUsage": "Disk usage", "kafkaVersion": "Kafka version", "leadController": "Lead controller", @@ -443,6 +447,17 @@ "broker": "Broker", "controller": "Controller" }, + "status": "Status", + "filter": { + "nodePool": "Node pool", + "nodePoolPlaceholder": "Filter by node pool", + "nodePoolRoles": "Roles: {{roles}}", + "role": "Role", + "rolePlaceholder": "Filter by role", + "statusPlaceholder": "Filter by status", + "brokerStatus": "Broker status", + "controllerStatus": "Controller status" + }, "brokerStatus": { "running": { "label": "Running", @@ -474,42 +489,27 @@ } }, "controllerStatus": { - "quorumLeader": "QuorumLeader", + "quorumLeader": "Quorum Leader", "quorumLeaderPopoverText": "The leader of the metadata quorum (also known as the Active Controller). It handles all metadata requests from Kafka brokers.", - "quorumFollower": "QuorumFollower", + "quorumFollower": "Quorum Follower", "quorumFollowerPopoverText": "Follower controllers replicate metadata written by the quorum leader (Active Controller) and serve as hot standbys in case of leader failure.", - "quorumFollowerLagged": "QuorumFollowerLagged", + "quorumFollowerLagged": "Quorum Follower Lagged", "quorumFollowerLaggedPopoverText": "Follower controllers replicate metadata written by the quorum leader (Active Controller). This follower is lagging behind the leader. Quorum followers must stay up-to-date to prevent data loss if the leader fails.", "unknown": "Unknown", "unknownPopoverText": "The controller's state is unknown" }, "tabs": { "overview": "Overview", - "rebalances": "Rebalances" + "rebalances": "Rebalance" }, "distribution": { - "title": "Partition distribution", "totalNodes": "Total nodes", "totalNodesTooltip": "Total number of Kafka nodes across all roles (broker, controller, or combined).", "controllerRole": "Controller role", "brokerRole": "Broker role", "leadController": "Lead controller", "leadControllerTooltip": "The Lead Controller (also known as the Active Controller) is the primary metadata manager in a KRaft-based Kafka cluster. It maintains a global view of the cluster, processes metadata updates, and distributes them to all other nodes.", - "leadControllerValue": "Node {{leadController}}", - "partitionsDistributionOfTotal": "Node Partition Distribution", - "partitionsDistributionOfTotalTooltip": "The percentage distribution of partitions across brokers in the cluster. Consider rebalancing if the distribution is uneven to ensure efficient resource utilization.", - "distributionToggles": "Distribution filter toggles", - "allLabel": "All ({{count}})", - "leadersLabel": "Leaders ({{count}})", - "followersLabel": "Followers ({{count}})", - "distributionChartDescription": "Bar chart showing partition distribution across brokers", - "distributionChartTitle": "Partition distribution", - "brokerNodeVoronoiFollowers": "Broker {{name}}: {{value}} followers", - "brokerNodeVoronoiLeaders": "Broker {{name}}: {{value}} leaders", - "brokerNodeVoronoiAll": "Broker {{name}}: {{value}} replicas", - "brokerNodeCount": "Broker {{node}}: {{count}} ({{percentage}}%)", - "brokerNodeCountMissing": "Broker {{node}}: N/A", - "metricsUnavailable": "Metrics are not available for this cluster" + "leadControllerValue": "Node {{leadController}}" }, "statusLabels": { "healthyTooltip": "Number of healthy nodes", @@ -517,13 +517,20 @@ } }, "rebalancing": { - "title": "Rebalances", - "rebalanceName": "Rebalance", + "title": "Rebalance", + "rebalanceName": "Name", "status": "Status", "lastUpdated": "Last updated", + "dataToMove": "Data to move", + "partitionsToMove": "Partitions to move", + "leadershipUpdates": "Leadership updates", + "estTimeToComplete": "Est. time to complete", "cruiseControlEnabled": "Cruise Control is enabled", "learnMoreAboutCruiseControl": "Learn more about Cruise Control enablement", "cruiseControlLink": "https://strimzi.io/docs/operators/latest/deploying#cruise-control-concepts-str", + "cruiseControlNotEnabled": "Cruise Control is not enabled", + "cruiseControlNotEnabledDescription": "Cruise Control is not enabled for this cluster. Add Cruise Control to your Kafka custom resource to get started with rebalancing.", + "cruiseControlGetStarted": "Get started", "totalRebalances": "Total Rebalances", "proposalReady": "Proposal Ready", "rebalancing": "Rebalancing", @@ -542,7 +549,6 @@ "confirmStopDescription": "Stopping will halt the current rebalancing process. You can start a new rebalance later. Are you sure you want to proceed?", "confirmRefreshTitle": "Refresh Rebalance", "confirmRefreshDescription": "Refresh the optimization proposal to the latest cluster metrics. Are you sure you want to proceed?", - "crBadge": "CR", "broker": "Broker {{b}}", "fullMode": "Full", "fullModeDescription": "Moves replicas across all brokers. This is the default mode", @@ -556,6 +562,9 @@ "noRebalances": "No Kafka cluster rebalances found", "noRebalancesDescription": "Configure a KafkaRebalance resource to generate optimization proposals and initiate rebalances for your Kafka cluster.", "noRebalancesAction": "Learn more about Kafka rebalancing", + "filter": { + "modePlaceholder": "Filter by mode" + }, "statuses": { "new": { "label": "New", diff --git a/api/src/main/webui/src/pages/kafka/KafkaLayout.tsx b/api/src/main/webui/src/pages/kafka/KafkaLayout.tsx index 2e995a4f8..dad8df039 100644 --- a/api/src/main/webui/src/pages/kafka/KafkaLayout.tsx +++ b/api/src/main/webui/src/pages/kafka/KafkaLayout.tsx @@ -230,9 +230,7 @@ export function KafkaLayout() { )} {isNodesPage && ( - - {t('kafka.nodes')} - + {t('kafka.nodes')} )} {isNodesPage && nodesTab && nodesTab !== 'nodes' && ( diff --git a/api/src/main/webui/src/pages/kafka/nodes/NodesOverviewTab.tsx b/api/src/main/webui/src/pages/kafka/nodes/NodesOverviewTab.tsx index 016c69ce1..35575ebfe 100644 --- a/api/src/main/webui/src/pages/kafka/nodes/NodesOverviewTab.tsx +++ b/api/src/main/webui/src/pages/kafka/nodes/NodesOverviewTab.tsx @@ -1,310 +1,83 @@ /** - * Nodes Overview Tab - Shows node distribution chart and nodes table + * Nodes Overview Tab - Shows cluster node summary and nodes table */ +import { useState, useCallback } from 'react'; import { useParams } from 'react-router'; import { useTranslation } from 'react-i18next'; import { - PageSection, - Grid, - GridItem, Card, CardBody, DescriptionList, + DescriptionListDescription, DescriptionListGroup, DescriptionListTerm, - DescriptionListDescription, - Tooltip, + Grid, + GridItem, Icon, - EmptyState, - EmptyStateBody, - Spinner, - Title, - CardHeader, - CardTitle, - ToggleGroup, - ToggleGroupItem, - Toolbar, - ToolbarContent, - ToolbarItem, - ToolbarGroup, - Pagination, - PaginationVariant, - Select, - SelectOption, - SelectList, - MenuToggle, - MenuToggleElement, - Flex, - FlexItem, - Label, - Button, + PageSection, + Tooltip, } from '@patternfly/react-core'; import { CheckCircleIcon, ExclamationTriangleIcon, HelpIcon, - FilterIcon, } from '@patternfly/react-icons'; import { useNodes } from '@/api/hooks/useNodes'; -import { useState, useRef, useEffect, useMemo } from 'react'; -import { - Chart, - ChartAxis, - ChartBar, - ChartStack, - ChartVoronoiContainer, - ChartThemeColor, -} from '@patternfly/react-charts/victory'; +import { ResourceListParams } from '@/api/hooks/useResourceList'; import { formatNumber } from '@/utils/format'; -import { NodesTable } from '@/components/kafka/nodes/NodesTable'; -import { useTableState } from '@/hooks'; -import type { BrokerStatus, ControllerStatus, NodeRoles } from '@/api/types'; -import { - useBrokerStatusLabels, - useControllerStatusLabels, - useRoleLabels, -} from '@/components/kafka/nodes/NodeStatusLabel'; - -type DistributionFilter = 'all' | 'leaders' | 'followers'; +import { NodesDataView } from '@/components/kafka/nodes/NodesDataView'; export function NodesOverviewTab() { const { t } = useTranslation(); const { kafkaId } = useParams<{ kafkaId: string }>(); - - // Fetch nodes for distribution chart (all nodes) - const { data: chartData, isLoading: chartLoading, error: chartError } = useNodes(kafkaId, { - pageSize: 100, - }); - - // Table state (pagination + sorting) - const table = useTableState({ - initialSortColumn: 'id', - initialSortDirection: 'asc', - }); - - // Filter state - const [filterNodePools, setFilterNodePools] = useState([]); - const [filterRoles, setFilterRoles] = useState([]); - const [filterBrokerStatuses, setFilterBrokerStatuses] = useState([]); - const [filterControllerStatuses, setFilterControllerStatuses] = useState([]); - - // Filter menu states - const [nodePoolFilterOpen, setNodePoolFilterOpen] = useState(false); - const [roleFilterOpen, setRoleFilterOpen] = useState(false); - const [statusFilterOpen, setStatusFilterOpen] = useState(false); - - // Fetch nodes for table with pagination and filters - const { data: tableData, isLoading: tableLoading } = useNodes(kafkaId, { - pageSize: table.pageSize, - pageCursor: table.pageCursor, - sort: table.sortBy, - sortDir: table.sortDirection, - nodePool: filterNodePools.length > 0 ? filterNodePools : undefined, - roles: filterRoles.length > 0 ? filterRoles : undefined, - brokerStatus: filterBrokerStatuses.length > 0 ? filterBrokerStatuses : undefined, - controllerStatus: filterControllerStatuses.length > 0 ? filterControllerStatuses : undefined, - }); - - // Update table state when data changes - useEffect(() => { - table.setData(tableData); - }, [tableData, table]); - const [filter, setFilter] = useState('all'); - const [chartWidth, setChartWidth] = useState(600); - const chartContainerRef = useRef(null); + // Table data driven by NodesDataView + const [tableParams, setTableParams] = useState({}); + const nodeResult = useNodes(kafkaId, tableParams); - // Get labels - const roleLabels = useRoleLabels(chartData?.meta?.summary?.statuses); - const brokerStatusLabels = useBrokerStatusLabels(); - const controllerStatusLabels = useControllerStatusLabels(); - - // Update chart width on resize - useEffect(() => { - const updateWidth = () => { - if (chartContainerRef.current) { - setChartWidth(chartContainerRef.current.offsetWidth); - } - }; - - updateWidth(); - window.addEventListener('resize', updateWidth); - return () => window.removeEventListener('resize', updateWidth); + const handleDataViewChange = useCallback((params: ResourceListParams) => { + setTableParams(params); }, []); - const nodes = chartData?.data || []; - const summary = chartData?.meta?.summary; - const leadControllerId = summary?.leaderId || ''; - const totalItems = tableData?.meta?.page?.total || 0; - const currentPage = tableData?.meta?.page?.pageNumber || 1; + const summary = nodeResult.data?.meta?.summary; - // Calculate node counts - const totalNodes = Object.values(summary?.statuses?.combined || {}).reduce( - (sum, count) => sum + Number(count), - 0 - ); + const leadControllerId = summary?.leaderId ?? ''; - const brokersTotal = Object.values(summary?.statuses?.brokers || {}).reduce( + const totalNodes = Object.values(summary?.statuses?.combined ?? {}).reduce( (sum, count) => sum + Number(count), - 0 + 0, ); - const brokersWarning = Object.keys(summary?.statuses?.brokers || {}).some( - (key) => key !== 'Running' - ); - - const controllersTotal = Object.values(summary?.statuses?.controllers || {}).reduce( + const brokersTotal = Object.values(summary?.statuses?.brokers ?? {}).reduce( (sum, count) => sum + Number(count), - 0 + 0, ); - - const controllersWarning = Object.keys(summary?.statuses?.controllers || {}).some( - (key) => key !== 'QuorumLeader' && key !== 'QuorumFollower' + const brokersWarning = Object.keys(summary?.statuses?.brokers ?? {}).some( + (key) => key !== 'Running', ); - // Build distribution data from broker nodes - const distributionData: Record = {}; - nodes - .filter((n: typeof nodes[number]) => n.attributes.roles?.includes('broker')) - .forEach((node: typeof nodes[number]) => { - distributionData[node.id] = { - leaders: node.attributes.broker?.leaderCount || 0, - followers: node.attributes.broker?.replicaCount || 0, - }; - }); - - const allCount = Object.values(distributionData).reduce( - (acc, v) => v.followers + v.leaders + acc, - 0 - ); - const leadersCount = Object.values(distributionData).reduce( - (acc, v) => v.leaders + acc, - 0 + const controllersTotal = Object.values(summary?.statuses?.controllers ?? {}).reduce( + (sum, count) => sum + Number(count), + 0, ); - const followersCount = Object.values(distributionData).reduce( - (acc, v) => v.followers + acc, - 0 + const controllersWarning = Object.keys(summary?.statuses?.controllers ?? {}).some( + (key) => key !== 'QuorumLeader' && key !== 'QuorumFollower', ); - const getCount = (nodeData: { leaders: number; followers: number }) => { - switch (filter) { - case 'leaders': - return nodeData.leaders; - case 'followers': - return nodeData.followers; - default: - return nodeData.leaders + nodeData.followers; - } - }; - - const getPercentage = (count: number) => { - const total = filter === 'leaders' ? leadersCount : filter === 'followers' ? followersCount : allCount; - return total > 0 ? ((count / total) * 100).toFixed(2) : '0.00'; - }; - - // Node pool options for filter - const nodePoolOptions = useMemo(() => { - if (!summary?.nodePools) return []; - return Object.entries(summary.nodePools).map(([poolName, poolMeta]) => { - const typedPoolMeta = poolMeta as { roles: string[]; count: number }; - return { - value: poolName, - label: poolName, - count: typedPoolMeta.count, - description: `Roles: ${typedPoolMeta.roles.join(', ')}`, - }; - }); - }, [summary]); - - // Role options for filter - const roleOptions: { value: NodeRoles; label: string; count: number }[] = [ - { - value: 'broker', - label: t('nodes.nodeRoles.broker'), - count: summary?.statuses?.brokers - ? Object.values(summary.statuses.brokers).reduce((sum, count) => sum + Number(count), 0) - : 0, - }, - { - value: 'controller', - label: t('nodes.nodeRoles.controller'), - count: summary?.statuses?.controllers - ? Object.values(summary.statuses.controllers).reduce((sum, count) => sum + Number(count), 0) - : 0, - }, - ]; - - // Status options for filter (grouped) - const brokerStatusOptions = useMemo(() => { - if (!summary?.statuses?.brokers) return []; - return Object.keys(brokerStatusLabels).map((status) => ({ - value: status as BrokerStatus, - label: status, - count: summary.statuses.brokers[status as BrokerStatus] || 0, - })); - }, [summary, brokerStatusLabels]); - - const controllerStatusOptions = useMemo(() => { - if (!summary?.statuses?.controllers) return []; - return Object.keys(controllerStatusLabels).map((status) => ({ - value: status as ControllerStatus, - label: status, - count: summary.statuses.controllers[status as ControllerStatus] || 0, - })); - }, [summary, controllerStatusLabels]); - - // Clear all filters - const clearAllFilters = () => { - setFilterNodePools([]); - setFilterRoles([]); - setFilterBrokerStatuses([]); - setFilterControllerStatuses([]); - table.resetPagination(); - }; - - // Check if any filters are active - const hasActiveFilters = - filterNodePools.length > 0 || - filterRoles.length > 0 || - filterBrokerStatuses.length > 0 || - filterControllerStatuses.length > 0; - - if (chartLoading) { - return ( - - - - - {t('common.loading')} - - - - ); - } - - if (chartError) { - return ( - - - - {t('common.error')} - - {chartError.message} - - - ); - } - return ( - + - + - + {t('nodes.distribution.totalNodes')}{' '} @@ -314,487 +87,53 @@ export function NodesOverviewTab() { {formatNumber(totalNodes)} + - - {t('nodes.distribution.controllerRole')} - + {t('nodes.distribution.controllerRole')} {controllersWarning ? ( - - - - ) : ( - - - - )} -   {formatNumber(controllersTotal)} - - - - - {t('nodes.distribution.brokerRole')} - - - {brokersWarning ? ( - - - + ) : ( - - - + )} -   {formatNumber(brokersTotal)} + {' '}{formatNumber(controllersTotal)} + - + {t('nodes.distribution.leadController')}{' '} - {t('nodes.distribution.leadControllerValue', { - leadController: leadControllerId, - })} + {t('nodes.distribution.leadControllerValue', { leadController: leadControllerId })} + + + + + {t('nodes.distribution.brokerRole')} + + {brokersWarning ? ( + + ) : ( + + )} + {' '}{formatNumber(brokersTotal)} - - - - - {t('nodes.distribution.partitionsDistributionOfTotal')}{' '} - - - - - - {allCount > 0 ? ( - - - setFilter('all')} - /> - setFilter('leaders')} - /> - setFilter('followers')} - /> - -
- { - switch (filter) { - case 'followers': - return t('nodes.distribution.brokerNodeVoronoiFollowers', { - name: datum.name, - value: datum.y, - }); - case 'leaders': - return t('nodes.distribution.brokerNodeVoronoiLeaders', { - name: datum.name, - value: datum.y, - }); - default: - return t('nodes.distribution.brokerNodeVoronoiAll', { - name: datum.name, - value: datum.y, - }); - } - }} - constrainToVisibleArea - /> - } - legendOrientation="horizontal" - legendPosition="bottom" - legendData={Object.keys(distributionData).map((node) => { - const count = getCount(distributionData[node]); - const percentage = getPercentage(count); - return { - name: t('nodes.distribution.brokerNodeCount', { - node, - count, - percentage, - }), - }; - })} - height={100} - padding={{ - bottom: 70, - left: 0, - right: 0, - top: 30, - }} - width={chartWidth} - > - - - {Object.entries(distributionData).map(([node, data], idx) => ( - - ))} - - -
-
- ) : ( - -
{t('nodes.distribution.metricsUnavailable')}
-
- )} -
-
- - - - {t('nodes.title')} - - - - - - {/* Node Pool Filter */} - - - - - {/* Role Filter */} - - - - - {/* Status Filter (Grouped) */} - - - - - - - {}} - onPerPageSelect={table.handlePerPageChange} - onNextClick={table.handleNextPage} - onPreviousClick={table.handlePrevPage} - variant={PaginationVariant.top} - isCompact - /> - - - {/* Filter chips */} - {hasActiveFilters && ( - - - {filterNodePools.length > 0 && ( - - {t('nodes.nodePool')}:{' '} - {filterNodePools.map((pool) => ( - - ))} - - )} - {filterRoles.length > 0 && ( - - {t('nodes.roles')}:{' '} - {filterRoles.map((role) => ( - - ))} - - )} - {filterBrokerStatuses.length > 0 && ( - - Broker Status:{' '} - {filterBrokerStatuses.map((status) => ( - - ))} - - )} - {filterControllerStatuses.length > 0 && ( - - Controller Status:{' '} - {filterControllerStatuses.map((status) => ( - - ))} - - )} - - - - - - )} - - - - {(tableData?.data || []).length > 0 && ( - - - - {}} - onPerPageSelect={table.handlePerPageChange} - onNextClick={table.handleNextPage} - onPreviousClick={table.handlePrevPage} - variant={PaginationVariant.bottom} - isCompact - /> - - - - )} - - + +
diff --git a/api/src/main/webui/src/pages/kafka/nodes/NodesPage.tsx b/api/src/main/webui/src/pages/kafka/nodes/NodesPage.tsx index dcb4045b1..3543ad67a 100644 --- a/api/src/main/webui/src/pages/kafka/nodes/NodesPage.tsx +++ b/api/src/main/webui/src/pages/kafka/nodes/NodesPage.tsx @@ -29,7 +29,7 @@ export function NodesPage() { const location = useLocation(); // Fetch nodes data for status labels - const { data, isLoading } = useNodes(kafkaId, { pageSize: 1 }); + const { data, isLoading } = useNodes(kafkaId, { page: { size: 1 } }); const summary = data?.meta?.summary; const totalNodes = data?.meta?.page?.total || 0; diff --git a/api/src/main/webui/src/pages/kafka/nodes/NodesRebalancesTab.tsx b/api/src/main/webui/src/pages/kafka/nodes/NodesRebalancesTab.tsx index 30a360ba0..a40fe3805 100644 --- a/api/src/main/webui/src/pages/kafka/nodes/NodesRebalancesTab.tsx +++ b/api/src/main/webui/src/pages/kafka/nodes/NodesRebalancesTab.tsx @@ -2,134 +2,77 @@ * Nodes Rebalances Tab - Shows Kafka rebalances */ -import { useState, useEffect } from 'react'; +import { useState, useCallback } from 'react'; import { useParams } from 'react-router'; import { useTranslation } from 'react-i18next'; import { PageSection, - Alert, - AlertActionCloseButton, - AlertActionLink, - Grid, - GridItem, - Toolbar, - ToolbarContent, - ToolbarItem, - ToolbarGroup, - SearchInput, - Select, - SelectOption, - SelectList, - MenuToggle, - MenuToggleElement, - Pagination, Button, EmptyState, + EmptyStateActions, EmptyStateBody, - Title, - PaginationVariant, + EmptyStateFooter, } from '@patternfly/react-core'; -import { FilterIcon } from '@patternfly/react-icons'; +import { BalanceScaleIcon } from '@patternfly/react-icons'; import { useRebalances, usePatchRebalance } from '@/api/hooks/useRebalances'; -import { RebalancesTable } from '@/components/kafka/nodes/RebalancesTable'; -import { RebalancesCountCard } from '@/components/kafka/nodes/RebalancesCountCard'; +import { useKafkaCluster } from '@/api/hooks/useKafkaClusters'; +import { ResourceListParams } from '@/api/hooks/useResourceList'; +import { RebalancesDataView } from '@/components/kafka/nodes/RebalancesDataView'; import { RebalanceConfirmationModal } from '@/components/kafka/nodes/RebalanceConfirmationModal'; -import { Rebalance, RebalanceStatus, RebalanceMode } from '@/api/types'; -import { useTableState } from '@/hooks'; -import { useShowLearning } from '@/hooks/useShowLearning'; +import { RebalanceModal } from '@/components/kafka/nodes/RebalanceModal'; +import { Rebalance } from '@/api/types'; export function NodesRebalancesTab() { const { t } = useTranslation(); const { kafkaId } = useParams<{ kafkaId: string }>(); - const showLearning = useShowLearning(); + const { data: clusterData } = useKafkaCluster(kafkaId, { fields: 'cruiseControlEnabled' }); + const cruiseControlEnabled = clusterData?.data?.attributes?.cruiseControlEnabled ?? false; - // Alert state - const [isAlertVisible, setIsAlertVisible] = useState(true); + // Table params driven by RebalancesDataView + const [dataParams, setDataParams] = useState({}); + const rebalanceResult = useRebalances(kafkaId, dataParams); - // Table state (pagination + sorting) - const table = useTableState({ - initialSortColumn: 'name', - initialSortDirection: 'asc', - }); - - // Filter state - const [filterName, setFilterName] = useState(''); - const [filterStatuses, setFilterStatuses] = useState([]); - const [filterModes, setFilterModes] = useState([]); - const [searchValue, setSearchValue] = useState(''); - const [isStatusSelectOpen, setIsStatusSelectOpen] = useState(false); - const [isModeSelectOpen, setIsModeSelectOpen] = useState(false); + const handleDataViewChange = useCallback((params: ResourceListParams) => { + setDataParams(params); + }, []); // Confirmation modal state const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); const [pendingAction, setPendingAction] = useState<'approve' | 'stop' | 'refresh'>('approve'); const [pendingRebalance, setPendingRebalance] = useState(null); - // Fetch rebalances with filters - const { data, isLoading } = useRebalances(kafkaId!, { - pageSize: table.pageSize, - pageCursor: table.pageCursor, - sort: table.sortBy, - sortDir: table.sortDirection, - name: filterName || undefined, - status: filterStatuses.length > 0 ? filterStatuses : undefined, - mode: filterModes.length > 0 ? filterModes : undefined, - }); - - // Update table state when data changes - useEffect(() => { - table.setData(data); - }, [data, table]); + // Detail modal state + const [isDetailModalOpen, setIsDetailModalOpen] = useState(false); + const [selectedRebalance, setSelectedRebalance] = useState(null); - // Mutation for rebalance actions const { mutate: patchRebalance } = usePatchRebalance(kafkaId!); - const handleFilterNameChange = (name: string) => { - setFilterName(name); - table.resetPagination(); - }; - - const handleFilterStatusChange = (statuses: RebalanceStatus[]) => { - setFilterStatuses(statuses); - table.resetPagination(); - }; - - const handleFilterModeChange = (modes: RebalanceMode[]) => { - setFilterModes(modes); - table.resetPagination(); - }; - - const handleClearAllFilters = () => { - setFilterName(''); - setFilterStatuses([]); - setFilterModes([]); - table.resetPagination(); - }; - - const handleApprove = (rebalance: Rebalance) => { + const handleApprove = useCallback((rebalance: Rebalance) => { setPendingRebalance(rebalance); setPendingAction('approve'); setIsConfirmModalOpen(true); - }; + }, []); - const handleStop = (rebalance: Rebalance) => { + const handleStop = useCallback((rebalance: Rebalance) => { setPendingRebalance(rebalance); setPendingAction('stop'); setIsConfirmModalOpen(true); - }; + }, []); - const handleRefresh = (rebalance: Rebalance) => { + const handleRefresh = useCallback((rebalance: Rebalance) => { setPendingRebalance(rebalance); setPendingAction('refresh'); setIsConfirmModalOpen(true); - }; + }, []); + + const handleViewDetails = useCallback((rebalance: Rebalance) => { + setSelectedRebalance(rebalance); + setIsDetailModalOpen(true); + }, []); const handleConfirmAction = () => { if (pendingRebalance) { - patchRebalance({ - rebalanceId: pendingRebalance.id, - action: pendingAction, - }); + patchRebalance({ rebalanceId: pendingRebalance.id, action: pendingAction }); } setIsConfirmModalOpen(false); setPendingRebalance(null); @@ -140,303 +83,40 @@ export function NodesRebalancesTab() { setPendingRebalance(null); }; - // Calculate status counts - const statusCounts = data?.data?.reduce( - (acc, rebalance) => { - const status = rebalance.attributes.status; - if (status === 'ProposalReady') acc.proposalReady += 1; - if (status === 'Rebalancing') acc.rebalancing += 1; - if (status === 'Ready') acc.ready += 1; - if (status === 'Stopped') acc.stopped += 1; - return acc; - }, - { proposalReady: 0, rebalancing: 0, ready: 0, stopped: 0 } - ) || { proposalReady: 0, rebalancing: 0, ready: 0, stopped: 0 }; - - const totalCount = data?.meta?.page?.total || 0; - const page = data?.meta?.page?.pageNumber || 1; - - const allStatuses: RebalanceStatus[] = [ - 'New', - 'PendingProposal', - 'ProposalReady', - 'Stopped', - 'Rebalancing', - 'NotReady', - 'Ready', - 'ReconciliationPaused', - ]; - - const allModes: RebalanceMode[] = ['full', 'add-brokers', 'remove-brokers']; - - // Empty state when no rebalances exist - if (!isLoading && totalCount === 0 && !filterName && filterStatuses.length === 0 && filterModes.length === 0) { - return ( - - - {showLearning && isAlertVisible && ( - - setIsAlertVisible(false)} />} - actionLinks={ - - {t('rebalancing.learnMoreAboutCruiseControl')} - - } - /> - - )} - - - - <FilterIcon /> {t('rebalancing.noRebalances')} - - {t('rebalancing.noRebalancesDescription')} - - - - - - ); - } - - // Empty state when filters don't match - if (!isLoading && data?.data?.length === 0 && (filterName || filterStatuses.length > 0 || filterModes.length > 0)) { + if (!cruiseControlEnabled) { return ( - - {isAlertVisible && ( - - setIsAlertVisible(false)} />} - actionLinks={ - - {t('rebalancing.learnMoreAboutCruiseControl')} - - } - /> - - )} - - - - <FilterIcon /> {t('common.noResultsFound')} - - {t('common.noResultsFoundDescription')} - - - - + + + ); } return ( - - {isAlertVisible && ( - - setIsAlertVisible(false)} />} - actionLinks={ - - {t('rebalancing.learnMoreAboutCruiseControl')} - - } - /> - - )} - - - - - - - - - setSearchValue(value)} - onSearch={(_, value) => { - handleFilterNameChange(value); - setSearchValue(value); - }} - onClear={() => { - handleFilterNameChange(''); - setSearchValue(''); - }} - aria-label={t('rebalancing.rebalanceName')} - /> - - - - - - - - - - - {(filterName || filterStatuses.length > 0 || filterModes.length > 0) && ( - - - - )} - - - - {}} - onPerPageSelect={table.handlePerPageChange} - onNextClick={table.handleNextPage} - onPreviousClick={table.handlePrevPage} - variant={PaginationVariant.top} - isCompact - /> - - - - - - - - - - {}} - onPerPageSelect={table.handlePerPageChange} - onNextClick={table.handleNextPage} - onPreviousClick={table.handlePrevPage} - variant={PaginationVariant.bottom} - isCompact - /> - - - - - + + + { setIsDetailModalOpen(false); setSelectedRebalance(null); }} + /> ); -} \ No newline at end of file +} diff --git a/api/src/main/webui/src/pages/kafka/overview/KafkaOverview.tsx b/api/src/main/webui/src/pages/kafka/overview/KafkaOverview.tsx index 371e7540d..7f7696324 100644 --- a/api/src/main/webui/src/pages/kafka/overview/KafkaOverview.tsx +++ b/api/src/main/webui/src/pages/kafka/overview/KafkaOverview.tsx @@ -80,8 +80,8 @@ function KafkaOverviewContent() { // Fetch nodes to get broker counts and for charts const { data: nodesData } = useNodes(kafkaId, { - fields: ['roles', 'broker'], - pageSize: 100, + fields: 'roles,broker', + page: { size: 100 }, }); // Calculate broker counts From eee51a5450d95bc0746eae8728cb162a7effbc99 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Mon, 10 Aug 2026 16:18:00 -0400 Subject: [PATCH 02/25] Remove node row expansion, add storage and partition dist. charts Signed-off-by: Michael Edgar --- .../components/kafka/nodes/NodeChartsCard.tsx | 90 ++++++++ .../components/kafka/nodes/NodesDataView.tsx | 202 ++++++------------ .../kafka/nodes/RebalancesDataView.tsx | 1 - .../kafka/nodes/RebalancesTable.tsx | 1 - .../nodes/charts/ChartNodeStorageUsage.tsx | 110 ++++++++++ .../charts/ChartPartitionDistribution.tsx | 104 +++++++++ api/src/main/webui/src/i18n/messages/en.json | 19 +- .../pages/kafka/nodes/NodesOverviewTab.tsx | 7 +- 8 files changed, 395 insertions(+), 139 deletions(-) create mode 100644 api/src/main/webui/src/components/kafka/nodes/NodeChartsCard.tsx create mode 100644 api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx create mode 100644 api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx diff --git a/api/src/main/webui/src/components/kafka/nodes/NodeChartsCard.tsx b/api/src/main/webui/src/components/kafka/nodes/NodeChartsCard.tsx new file mode 100644 index 000000000..1a76cb2e9 --- /dev/null +++ b/api/src/main/webui/src/components/kafka/nodes/NodeChartsCard.tsx @@ -0,0 +1,90 @@ +import { useTranslation } from 'react-i18next'; +import { + Card, + CardBody, + CardHeader, + CardTitle, + Divider, + Stack, + StackItem, + Tooltip, +} from '@patternfly/react-core'; +import { ChartLineIcon, HelpIcon } from '@patternfly/react-icons'; +import { UseQueryResult } from '@tanstack/react-query'; +import { ListResponse, Node, NodeListMeta } from '@/api/types'; +import { ChartSkeletonLoader } from '@/components/kafka/overview/ChartSkeletonLoader'; +import { ChartNodeStorageUsage } from './charts/ChartNodeStorageUsage'; +import { ChartPartitionDistribution } from './charts/ChartPartitionDistribution'; + +export interface NodeChartsCardProps { + nodeResult: UseQueryResult, Error>; +} + +export function NodeChartsCard({ nodeResult }: NodeChartsCardProps) { + const { t } = useTranslation(); + const nodes = nodeResult.data?.data ?? []; + + return ( + + + + + {t('nodes.charts.title')} + + + + + {nodeResult.isLoading ? ( + <> + + + + + + + + + + + ) : ( + <> + +
+ {t('nodes.charts.storageUsage')} +
+
+ {t('nodes.charts.storageUsageSubtitle')}{' '} + + + +
+
+ + + + + + + + + +
+ {t('nodes.charts.partitionDistribution')} +
+
+ {t('nodes.charts.partitionDistributionSubtitle')}{' '} + + + +
+
+ + + + + )} +
+
+
+ ); +} diff --git a/api/src/main/webui/src/components/kafka/nodes/NodesDataView.tsx b/api/src/main/webui/src/components/kafka/nodes/NodesDataView.tsx index fd9e842ca..f90e3b108 100644 --- a/api/src/main/webui/src/components/kafka/nodes/NodesDataView.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/NodesDataView.tsx @@ -1,19 +1,15 @@ import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router'; -import { DataViewTd } from '@patternfly/react-data-view'; import { ThProps } from '@patternfly/react-table'; import { UseQueryResult } from '@tanstack/react-query'; import { - ClipboardCopy, - Content, Flex, FlexItem, Label, Tooltip, } from '@patternfly/react-core'; import { HelpIcon } from '@patternfly/react-icons'; -import { ChartDonutUtilization } from '@patternfly/react-charts/victory'; import { Node, ListResponse, BrokerStatus, ControllerStatus, NodeListMeta } from '@/api/types'; import { ResourceListParams } from '@/api/hooks/useResourceList'; import { @@ -27,8 +23,7 @@ import { useBrokerStatusLabels, useControllerStatusLabels, } from './NodeStatusLabel'; -import { formatNumber, formatBytes } from '@/utils/format'; -import type { ChartDatum } from '@/components/kafka/overview/utils/types'; +import { formatNumber } from '@/utils/format'; const columnNames = ['id', 'roles', 'status', 'replicas', 'rack', 'nodePool'] as const; @@ -143,137 +138,74 @@ export function NodesDataView({ const rowMapper: ResourceListDataViewRowMapper = useCallback( (node): ResourceListDataViewRowResult => { - const diskCapacity = node.attributes.storageCapacity; - const diskUsage = node.attributes.storageUsed; - const usedCapacity = - diskUsage != null && diskCapacity != null - ? diskUsage / diskCapacity - : undefined; - return { - row: { - id: node.id, - row: [ - { - // id on the first cell is how DataViewTableBasic matches expandedRows entries - id: node.id, - cell: ( - <> - {node.meta?.privileges?.includes('GET') === true ? ( - - {node.id} - - ) : ( - node.id - )} - {node.attributes.metadataState?.status === 'leader' && ( - - )} - - ), - props: { dataLabel: t('nodes.nodeId'), modifier: 'nowrap' }, - } as DataViewTd, - { - cell: ( - <>{node.attributes.roles?.map((role) => ( -
{roleLabels[role].label}
- ))} - ), - props: { dataLabel: t('nodes.roles'), modifier: 'nowrap' }, - }, - { - cell: ( - <> -
- {node.attributes.broker && brokerStatusLabels[node.attributes.broker.status]} -
-
- {node.attributes.controller && controllerStatusLabels[node.attributes.controller.status]} -
- - ), - props: { dataLabel: t('nodes.status'), modifier: 'nowrap' }, - }, - { - cell: node.attributes.kafkaVersion, - props: { dataLabel: t('nodes.kafkaVersion'), modifier: 'nowrap' }, - }, - { - cell: typeof node.attributes.broker?.leaderCount === 'number' && - typeof node.attributes.broker?.replicaCount === 'number' - ? formatNumber(node.attributes.broker.leaderCount + node.attributes.broker.replicaCount) - : '-', - props: { dataLabel: t('nodes.replicas'), modifier: 'fitContent', style: { textAlign: 'right' } }, - }, - { - cell: typeof node.attributes.broker?.leaderCount === 'number' - ? formatNumber(node.attributes.broker.leaderCount) - : '-', - props: { dataLabel: t('nodes.leaders'), modifier: 'fitContent', style: { textAlign: 'right' } }, - }, - { - cell: node.attributes.rack || 'n/a', - props: { dataLabel: t('nodes.rack'), modifier: 'nowrap' }, - }, - { - cell: node.attributes.nodePool || 'n/a', - props: { dataLabel: t('nodes.nodePool'), modifier: 'nowrap' }, - }, - ], - }, - expandedRows: [{ - rowId: node.id as unknown as number, - columnId: 0, - content: ( - - - - {t('nodes.hostName')} - - - {node.attributes.host || 'n/a'} - - - - - - - {t('nodes.diskUsage')} - - {usedCapacity !== undefined && ( -
- - datum.x ? `${datum.x}: ${datum.y.toFixed(1)}%` : null - } - legendData={[ - { name: `Used capacity: ${formatBytes(diskUsage!)}` }, - { name: `Available: ${formatBytes(diskCapacity! - diskUsage!)}` }, - ]} - legendOrientation="vertical" - legendPosition="bottom" - padding={{ bottom: 75, left: 20, right: 20, top: 20 }} - title={`${(usedCapacity * 100).toFixed(1)}%`} - subTitle={`of ${formatBytes(diskCapacity!)}`} - thresholds={[{ value: 60 }, { value: 90 }]} - height={300} - width={230} - /> -
+ row: [ + { + cell: ( + <> + {node.meta?.privileges?.includes('GET') === true ? ( + + {node.id} + + ) : ( + node.id + )} + {node.attributes.metadataState?.status === 'leader' && ( + )} -
- - - {t('nodes.kafkaVersion')} - -
{node.attributes.kafkaVersion ?? 'Unknown'}
-
-
- ), - }], + + ), + props: { dataLabel: t('nodes.nodeId'), modifier: 'nowrap' }, + }, + { + cell: ( + <>{node.attributes.roles?.map((role) => ( +
{roleLabels[role].label}
+ ))} + ), + props: { dataLabel: t('nodes.roles'), modifier: 'nowrap' }, + }, + { + cell: ( + <> +
+ {node.attributes.broker && brokerStatusLabels[node.attributes.broker.status]} +
+
+ {node.attributes.controller && controllerStatusLabels[node.attributes.controller.status]} +
+ + ), + props: { dataLabel: t('nodes.status'), modifier: 'nowrap' }, + }, + { + cell: node.attributes.kafkaVersion, + props: { dataLabel: t('nodes.kafkaVersion'), modifier: 'nowrap' }, + }, + { + cell: typeof node.attributes.broker?.leaderCount === 'number' && + typeof node.attributes.broker?.replicaCount === 'number' + ? formatNumber(node.attributes.broker.leaderCount + node.attributes.broker.replicaCount) + : '-', + props: { dataLabel: t('nodes.replicas'), modifier: 'fitContent', style: { textAlign: 'right' } }, + }, + { + cell: typeof node.attributes.broker?.leaderCount === 'number' + ? formatNumber(node.attributes.broker.leaderCount) + : '-', + props: { dataLabel: t('nodes.leaders'), modifier: 'fitContent', style: { textAlign: 'right' } }, + }, + { + cell: node.attributes.rack || 'n/a', + props: { dataLabel: t('nodes.rack'), modifier: 'nowrap' }, + }, + { + cell: node.attributes.nodePool || 'n/a', + props: { dataLabel: t('nodes.nodePool'), modifier: 'nowrap' }, + }, + ], }; }, [kafkaId, t, roleLabels, brokerStatusLabels, controllerStatusLabels], diff --git a/api/src/main/webui/src/components/kafka/nodes/RebalancesDataView.tsx b/api/src/main/webui/src/components/kafka/nodes/RebalancesDataView.tsx index b17cefaab..ef4c963b0 100644 --- a/api/src/main/webui/src/components/kafka/nodes/RebalancesDataView.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/RebalancesDataView.tsx @@ -5,7 +5,6 @@ import { DataViewTd } from '@patternfly/react-data-view'; import { ThProps, ActionsColumn } from '@patternfly/react-table'; import { UseQueryResult } from '@tanstack/react-query'; import { - Badge, Button, DescriptionList, DescriptionListDescription, diff --git a/api/src/main/webui/src/components/kafka/nodes/RebalancesTable.tsx b/api/src/main/webui/src/components/kafka/nodes/RebalancesTable.tsx index 285152217..cc71bdc19 100644 --- a/api/src/main/webui/src/components/kafka/nodes/RebalancesTable.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/RebalancesTable.tsx @@ -26,7 +26,6 @@ import { DescriptionListGroup, DescriptionListTerm, DescriptionListDescription, - Badge, Popover, List, ListItem, diff --git a/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx new file mode 100644 index 000000000..92afe0022 --- /dev/null +++ b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx @@ -0,0 +1,110 @@ +import { useTranslation } from 'react-i18next'; +import { + Chart, + ChartAxis, + ChartBar, + ChartLegend, + ChartStack, + ChartThemeColor, + ChartTooltip, +} from '@patternfly/react-charts/victory'; +import { Alert } from '@patternfly/react-core'; +import { Node } from '@/api/types'; +import { formatBytes } from '@/utils/format'; +import { useChartWidth } from '@/components/kafka/overview/utils/useChartWidth'; +import { getPadding } from '@/components/kafka/overview/utils/chartConsts'; + +interface ChartNodeStorageUsageProps { + nodes: Node[]; +} + +export function ChartNodeStorageUsage({ nodes }: ChartNodeStorageUsageProps) { + const { t } = useTranslation(); + const [containerRef, width] = useChartWidth(); + + const storageNodes = nodes.filter( + (n) => n.attributes.storageUsed != null && n.attributes.storageCapacity != null, + ).sort((n1, n2) => parseInt(n2.id) - parseInt(n1.id)); + + if (storageNodes.length === 0) { + return ( + + ); + } + + const usedData = storageNodes.map((n) => ({ + name: t('nodes.charts.storageUsageSeriesUsed'), + x: `Node ${n.id}`, + y: n.attributes.storageUsed as number, + label: `Node ${n.id}\n${t('nodes.charts.storageUsageSeriesUsed')}: ${formatBytes(n.attributes.storageUsed as number)}`, + })); + + const availableData = storageNodes.map((n) => { + const available = (n.attributes.storageCapacity as number) - (n.attributes.storageUsed as number); + return { + name: t('nodes.charts.storageUsageSeriesAvailable'), + x: `Node ${n.id}`, + y: available, + label: `Node ${n.id}\n${t('nodes.charts.storageUsageSeriesAvailable')}: ${formatBytes(available)}`, + }; + }); + + const legendData = [ + { name: t('nodes.charts.storageUsageSeriesUsed') }, + { name: t('nodes.charts.storageUsageSeriesAvailable') }, + ]; + + // Compute 5 evenly-spaced, round tick values from 0 to maxCapacity. + // Victory's tickCount hint does not produce round values for byte ranges, + // so we derive explicit tickValues instead. + const maxCapacity = Math.max(...storageNodes.map((n) => n.attributes.storageCapacity as number)); + const tickStep = maxCapacity / 4; + // Round step up to a power-of-1024 boundary so labels stay in one unit. + const unitBoundary = Math.pow(1024, Math.floor(Math.log(tickStep) / Math.log(1024))); + const roundedStep = Math.ceil(tickStep / unitBoundary) * unitBoundary; + const tickValues = [0, 1, 2, 3, 4].map((i) => i * roundedStep); + + const barWidth = 20; + const legendRows = 1; + const padding = { ...getPadding(legendRows), left: 80 }; + // Each node row gets 60px; top/bottom padding keeps outer bars off the edge. + const chartHeight = storageNodes.length * 60 + padding.top + padding.bottom; + + return ( +
+ + } + height={chartHeight} + padding={padding} + domainPadding={{ x: [30, 25] }} + themeColor={ChartThemeColor.multiOrdered} + width={width} + legendAllowWrap={true} + > + formatBytes(d)} /> + + + } + /> + } + /> + + +
+ ); +} diff --git a/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx b/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx new file mode 100644 index 000000000..124b21a02 --- /dev/null +++ b/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx @@ -0,0 +1,104 @@ +import { useTranslation } from 'react-i18next'; +import { + Chart, + ChartAxis, + ChartBar, + ChartLegend, + ChartStack, + ChartThemeColor, + ChartTooltip, +} from '@patternfly/react-charts/victory'; +import { Alert } from '@patternfly/react-core'; +import { Node } from '@/api/types'; +import { formatNumber } from '@/utils/format'; +import { useChartWidth } from '@/components/kafka/overview/utils/useChartWidth'; +import { getPadding } from '@/components/kafka/overview/utils/chartConsts'; + +interface ChartPartitionDistributionProps { + nodes: Node[]; +} + +export function ChartPartitionDistribution({ nodes }: ChartPartitionDistributionProps) { + const { t } = useTranslation(); + const [containerRef, width] = useChartWidth(); + + const brokerNodes = nodes.filter((n) => n.attributes.broker != null) + .sort((n1, n2) => parseInt(n2.id) - parseInt(n1.id)); + + if (brokerNodes.length === 0) { + return ( + + ); + } + + // Bottom segment: leader partitions + const leadersData = brokerNodes.map((n) => { + const broker = n.attributes.broker!; + return { + name: t('nodes.charts.partitionDistributionSeriesLeaders'), + x: `Node ${n.id}`, + y: broker.leaderCount, + label: `Node ${n.id}\n${t('nodes.charts.partitionDistributionSeriesLeaders')}: ${formatNumber(broker.leaderCount)}`, + }; + }); + + // Top segment: follower replicas only (excludes leaders) + const replicasData = brokerNodes.map((n) => { + const broker = n.attributes.broker!; + return { + name: t('nodes.charts.partitionDistributionSeriesReplicas'), + x: `Node ${n.id}`, + y: broker.replicaCount, + label: `Node ${n.id}\n${t('nodes.charts.partitionDistributionSeriesReplicas')}: ${formatNumber(broker.replicaCount)}`, + }; + }); + + const legendData = [ + { name: t('nodes.charts.partitionDistributionSeriesLeaders') }, + { name: t('nodes.charts.partitionDistributionSeriesReplicas') }, + ]; + + const barWidth = 20; + const legendRows = 1; + const padding = { ...getPadding(legendRows), left: 80 }; + // Each node row gets 60px; top/bottom padding keeps outer bars off the edge. + const chartHeight = brokerNodes.length * 60 + padding.top + padding.bottom; + + return ( +
+ + } + height={chartHeight} + padding={padding} + domainPadding={{ x: [30, 25] }} + themeColor={ChartThemeColor.multiOrdered} + width={width} + legendAllowWrap={true} + > + + + + } + /> + } + /> + + +
+ ); +} diff --git a/api/src/main/webui/src/i18n/messages/en.json b/api/src/main/webui/src/i18n/messages/en.json index 9fa393ddc..7a267c6eb 100644 --- a/api/src/main/webui/src/i18n/messages/en.json +++ b/api/src/main/webui/src/i18n/messages/en.json @@ -514,6 +514,23 @@ "statusLabels": { "healthyTooltip": "Number of healthy nodes", "unhealthyTooltip": "Number of unhealthy nodes" + }, + "charts": { + "title": "Node charts", + "storageUsage": "Node storage usage", + "storageUsageSubtitle": "Total storage used per node", + "storageUsageTooltip": "Used and available storage per node.", + "storageUsageNoData": "No storage usage data available", + "storageUsageAriaTitle": "Node storage usage chart", + "storageUsageSeriesUsed": "Used", + "storageUsageSeriesAvailable": "Available", + "partitionDistribution": "Partition distribution", + "partitionDistributionSubtitle": "Balance of partition leaders and replicas across brokers", + "partitionDistributionTooltip": "Total replicas and leader partitions per broker node.", + "partitionDistributionNoData": "No partition data available", + "partitionDistributionAriaTitle": "Partition distribution chart", + "partitionDistributionSeriesLeaders": "Leaders", + "partitionDistributionSeriesReplicas": "Replicas" } }, "rebalancing": { @@ -806,4 +823,4 @@ "logout": "Logout", "anonymous": "Anonymous" } -} \ No newline at end of file +} diff --git a/api/src/main/webui/src/pages/kafka/nodes/NodesOverviewTab.tsx b/api/src/main/webui/src/pages/kafka/nodes/NodesOverviewTab.tsx index 35575ebfe..ead8070b3 100644 --- a/api/src/main/webui/src/pages/kafka/nodes/NodesOverviewTab.tsx +++ b/api/src/main/webui/src/pages/kafka/nodes/NodesOverviewTab.tsx @@ -27,6 +27,7 @@ import { useNodes } from '@/api/hooks/useNodes'; import { ResourceListParams } from '@/api/hooks/useResourceList'; import { formatNumber } from '@/utils/format'; import { NodesDataView } from '@/components/kafka/nodes/NodesDataView'; +import { NodeChartsCard } from '@/components/kafka/nodes/NodeChartsCard'; export function NodesOverviewTab() { const { t } = useTranslation(); @@ -69,7 +70,7 @@ export function NodesOverviewTab() { - + + + + + ); From 77c9df3592a62c98defcb3471b6696695f9ca51e Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Tue, 11 Aug 2026 12:00:20 -0400 Subject: [PATCH 03/25] Refine node charts and rebalance row expansion display Signed-off-by: Michael Edgar --- .../kafka/nodes/RebalancesDataView.tsx | 18 +++++++++-- .../nodes/charts/ChartNodeStorageUsage.tsx | 24 ++++++++++---- .../charts/ChartPartitionDistribution.tsx | 32 ++++++++++++------- api/src/main/webui/src/index.css | 28 +++++++++++++++- 4 files changed, 82 insertions(+), 20 deletions(-) diff --git a/api/src/main/webui/src/components/kafka/nodes/RebalancesDataView.tsx b/api/src/main/webui/src/components/kafka/nodes/RebalancesDataView.tsx index ef4c963b0..4e13debde 100644 --- a/api/src/main/webui/src/components/kafka/nodes/RebalancesDataView.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/RebalancesDataView.tsx @@ -16,7 +16,7 @@ import { ListItem, Popover, } from '@patternfly/react-core'; -import { HelpIcon } from '@patternfly/react-icons'; +import { AngleRightIcon, HelpIcon } from '@patternfly/react-icons'; import { Rebalance, ListResponse } from '@/api/types'; import { ResourceListParams } from '@/api/hooks/useResourceList'; import { @@ -72,6 +72,10 @@ export function RebalancesDataView({ const colMapper: ResourceListDataViewColumnMapper = useCallback( (sortBy, direction, onSort) => [ + { + // expander column, + cell: '' + }, { cell: t('rebalancing.rebalanceName'), props: { @@ -137,13 +141,23 @@ export function RebalancesDataView({ row: [ { id: rebalance.id, + cell: ( + + ), + } as DataViewTd, + { cell: ( ), props: { dataLabel: t('rebalancing.rebalanceName') }, - } as DataViewTd, + }, { cell: ( + } legendPosition="bottom-left" legendComponent={ } - height={chartHeight} padding={padding} domainPadding={{ x: [30, 25] }} themeColor={ChartThemeColor.multiOrdered} width={width} legendAllowWrap={true} > - formatBytes(d)} /> + formatBytes(d)} + style={{ axisLabel: { padding: 75 } }} + /> - + + } legendPosition="bottom-left" legendComponent={ } - height={chartHeight} padding={padding} domainPadding={{ x: [30, 25] }} themeColor={ChartThemeColor.multiOrdered} width={width} legendAllowWrap={true} > - + - + } /> } /> diff --git a/api/src/main/webui/src/index.css b/api/src/main/webui/src/index.css index bbe875d1a..fca6f3909 100644 --- a/api/src/main/webui/src/index.css +++ b/api/src/main/webui/src/index.css @@ -32,4 +32,30 @@ code { .pf-v6-c-nav__link a.pf-m-current { color: var(--pf-v6-c-nav__link--m-current--Color); background-color: var(--pf-v6-c-nav__link--m-current--BackgroundColor); -} \ No newline at end of file +} + +/* Expandable rows: rotate the expand icon when the row is expanded */ +.pf-v6-c-table__compound-expansion-toggle button[aria-expanded="true"] .expand-icon { + transform: rotate(90deg); +} + +/* Expandable rows: make the expand button fill the entire cell */ +.pf-v6-c-table__compound-expansion-toggle .pf-v6-c-table__button { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; +} + +/* Expandable rows: centre the icon inside the TableText wrapper */ +.pf-v6-c-table__compound-expansion-toggle .pf-v6-c-table__text { + display: flex; + align-items: center; + justify-content: center; +} + +/* Center align table cell contents */ +.pf-v6-c-table__td { + vertical-align: middle; +} From 220aba3fffef33a9f28f7e835097244637cf4585 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Tue, 4 Aug 2026 05:36:26 -0400 Subject: [PATCH 04/25] Add Cruise Control proposal and progress status to rebalance model Signed-off-by: Michael Edgar --- .gitignore | 3 +- api/pom.xml | 30 +++++ .../console/api/KafkaRebalancesResource.java | 6 +- .../console/api/model/KafkaRebalance.java | 53 ++++++++- .../api/model/rebalance/BrokerLoadImpact.java | 12 ++ .../api/service/KafkaRebalanceService.java | 104 ++++++++++++++++-- api/src/main/resources/application.properties | 7 ++ .../main/webui/src/api/hooks/useRebalances.ts | 3 +- api/src/main/webui/src/api/types.ts | 12 ++ 9 files changed, 218 insertions(+), 12 deletions(-) create mode 100644 api/src/main/java/com/github/streamshub/console/api/model/rebalance/BrokerLoadImpact.java diff --git a/.gitignore b/.gitignore index 5b4cf75cd..cf63cfcdb 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ release.properties # Systemtests systemtests/screenshots/ systemtests/config.yaml -systemtests/tracing/ \ No newline at end of file +systemtests/tracing/ +/.playwright-mcp/ diff --git a/api/pom.xml b/api/pom.xml index 2779561b2..ceed752fb 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -109,6 +109,11 @@ quarkus-quinoa ${quarkus-quinoa.version} + + io.quarkiverse.openapi.generator + quarkus-openapi-generator + 2.23.0 + org.apache.kafka @@ -306,6 +311,7 @@ build + generate-code @@ -319,6 +325,30 @@ org.apache.maven.plugins maven-dependency-plugin + + unpack + initialize + + unpack + + + + + + com.linkedin.cruisecontrol + cruise-control + 2.5.146 + jar + true + ${project.build.directory}/schema/cruise-control + yaml/** + + + + analyze diff --git a/api/src/main/java/com/github/streamshub/console/api/KafkaRebalancesResource.java b/api/src/main/java/com/github/streamshub/console/api/KafkaRebalancesResource.java index 981f61283..b7b11724d 100644 --- a/api/src/main/java/com/github/streamshub/console/api/KafkaRebalancesResource.java +++ b/api/src/main/java/com/github/streamshub/console/api/KafkaRebalancesResource.java @@ -142,7 +142,7 @@ public Response listRebalances( listParams, KafkaRebalance::fromCursor); - var rebalanceList = rebalanceService.listRebalances(listSupport); + var rebalanceList = rebalanceService.listRebalances(fields, listSupport); var responseEntity = new KafkaRebalance.RebalanceDataList(rebalanceList, listSupport); return Response.ok(responseEntity).build(); @@ -187,6 +187,7 @@ public Response getRebalance( KafkaRebalance.Fields.REPLICA_MOVEMENT_STRATEGIES, KafkaRebalance.Fields.SESSION_ID, KafkaRebalance.Fields.OPTIMIZATION_RESULT, + KafkaRebalance.Fields.OPTIMIZATION_PROPOSAL, KafkaRebalance.Fields.CONDITIONS, }, payload = ErrorCategory.InvalidQueryParameter.class) @@ -214,13 +215,14 @@ public Response getRebalance( KafkaRebalance.Fields.REPLICA_MOVEMENT_STRATEGIES, KafkaRebalance.Fields.SESSION_ID, KafkaRebalance.Fields.OPTIMIZATION_RESULT, + KafkaRebalance.Fields.OPTIMIZATION_PROPOSAL, KafkaRebalance.Fields.CONDITIONS, })) List fields) { requestedFields.accept(fields); - var result = rebalanceService.getRebalance(rebalanceId); + var result = rebalanceService.getRebalance(rebalanceId, fields); var responseEntity = new KafkaRebalance.RebalanceData(result); return Response.ok(responseEntity).build(); diff --git a/api/src/main/java/com/github/streamshub/console/api/model/KafkaRebalance.java b/api/src/main/java/com/github/streamshub/console/api/model/KafkaRebalance.java index 588ee595e..fbcccf534 100644 --- a/api/src/main/java/com/github/streamshub/console/api/model/KafkaRebalance.java +++ b/api/src/main/java/com/github/streamshub/console/api/model/KafkaRebalance.java @@ -1,5 +1,6 @@ package com.github.streamshub.console.api.model; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -30,6 +31,8 @@ import com.github.streamshub.console.api.support.ListRequestContext; import com.github.streamshub.console.api.support.StringEnumeration; +import io.streamshub.console.api.model.rebalance.cc.model.ExecutorState; +import io.streamshub.console.api.model.rebalance.cc.model.OptimizationResult; import io.xlate.validation.constraints.Expression; import static java.util.Comparator.comparing; @@ -75,6 +78,8 @@ public static class Fields { public static final String REPLICA_MOVEMENT_STRATEGIES = "replicaMovementStrategies"; public static final String SESSION_ID = "sessionId"; public static final String OPTIMIZATION_RESULT = "optimizationResult"; + public static final String OPTIMIZATION_PROPOSAL = "optimizationProposal"; + public static final String PROGRESS = "progress"; public static final String CONDITIONS = "conditions"; static final Comparator ID_COMPARATOR = @@ -196,6 +201,36 @@ static final class Meta extends JsonApiMeta { String action; } + public static final record BrokerLoadImpact( + @JsonProperty + BigDecimal before, + @JsonProperty + BigDecimal after, + @JsonProperty + BigDecimal diff + ) { + } + + public static final record OptimizationProposal( + @JsonProperty + Map> brokerImpact, + + @JsonProperty + OptimizationResult optimization + ) { + } + + public static final record ProgressStatus( + @JsonProperty + Integer estimatedTimeToCompletionInMinutes, + @JsonProperty + @Schema(minimum = "0", maximum = "100") + Integer completedByteMovementPercentage, + @JsonProperty + ExecutorState executorState + ) { + } + @JsonFilter("fieldFilter") @Schema(name = "KafkaRebalanceAttributes") static class Attributes extends KubeAttributes { @@ -253,7 +288,15 @@ static class Attributes extends KubeAttributes { @JsonProperty @Schema(readOnly = true) - Map optimizationResult = new HashMap<>(0); + Map optimizationResult = HashMap.newHashMap(0); + + @JsonProperty + @Schema(readOnly = true) + OptimizationProposal optimizationProposal; + + @JsonProperty + @Schema(readOnly = true) + ProgressStatus progress; @JsonProperty @Schema(readOnly = true) @@ -361,6 +404,14 @@ public Map optimizationResult() { return attributes.optimizationResult; } + public void optimizationProposal(OptimizationProposal optimizationProposal) { + attributes.optimizationProposal = optimizationProposal; + } + + public void progress(ProgressStatus progress) { + attributes.progress = progress; + } + public void conditions(List conditions) { attributes.conditions = conditions; } diff --git a/api/src/main/java/com/github/streamshub/console/api/model/rebalance/BrokerLoadImpact.java b/api/src/main/java/com/github/streamshub/console/api/model/rebalance/BrokerLoadImpact.java new file mode 100644 index 000000000..95cd850da --- /dev/null +++ b/api/src/main/java/com/github/streamshub/console/api/model/rebalance/BrokerLoadImpact.java @@ -0,0 +1,12 @@ +package com.github.streamshub.console.api.model.rebalance; + +import java.math.BigDecimal; + +public record BrokerLoadImpact( + BigDecimal before, + BigDecimal after, + BigDecimal diff +) { + +} + diff --git a/api/src/main/java/com/github/streamshub/console/api/service/KafkaRebalanceService.java b/api/src/main/java/com/github/streamshub/console/api/service/KafkaRebalanceService.java index c274579bd..6419b8608 100644 --- a/api/src/main/java/com/github/streamshub/console/api/service/KafkaRebalanceService.java +++ b/api/src/main/java/com/github/streamshub/console/api/service/KafkaRebalanceService.java @@ -3,6 +3,7 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Base64; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -17,8 +18,11 @@ import org.jboss.logging.Logger; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import com.github.streamshub.console.api.model.Condition; import com.github.streamshub.console.api.model.KafkaRebalance; +import com.github.streamshub.console.api.model.KafkaRebalance.BrokerLoadImpact; import com.github.streamshub.console.api.security.PermissionService; import com.github.streamshub.console.api.support.KafkaContext; import com.github.streamshub.console.api.support.ListRequestContext; @@ -28,10 +32,12 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.informers.cache.Cache; +import io.streamshub.console.api.model.rebalance.cc.model.ExecutorState; import io.strimzi.api.ResourceAnnotations; import io.strimzi.api.ResourceLabels; import io.strimzi.api.kafka.model.kafka.Kafka; import io.strimzi.api.kafka.model.rebalance.KafkaRebalanceMode; +import io.strimzi.api.kafka.model.rebalance.KafkaRebalanceProgress; import io.strimzi.api.kafka.model.rebalance.KafkaRebalanceSpec; import io.strimzi.api.kafka.model.rebalance.KafkaRebalanceState; import io.strimzi.api.kafka.model.rebalance.KafkaRebalanceStatus; @@ -45,6 +51,9 @@ public class KafkaRebalanceService { @Inject KubernetesClient client; + @Inject + ObjectMapper mapper; + @Inject ConsoleConfig consoleConfig; @@ -54,7 +63,7 @@ public class KafkaRebalanceService { @Inject PermissionService permissionService; - public List listRebalances(ListRequestContext listSupport) { + public List listRebalances(List fields, ListRequestContext listSupport) { final Map statuses = new HashMap<>(); listSupport.meta().put("summary", Map.of("statuses", statuses)); @@ -65,7 +74,7 @@ public List listRebalances(ListRequestContext li ResourceTypes.Kafka.REBALANCES, Privilege.LIST, r -> r.getMetadata().getName())) - .map(this::toKafkaRebalance) + .map(r -> toKafkaRebalance(r, fields)) .map(rebalance -> tallyStatus(statuses, rebalance)) .filter(listSupport.filter(KafkaRebalance.class)) .map(listSupport::tally) @@ -77,9 +86,9 @@ public List listRebalances(ListRequestContext li .toList(); } - public KafkaRebalance getRebalance(String id) { + public KafkaRebalance getRebalance(String id, List fields) { return findRebalance(id) - .map(this::toKafkaRebalance) + .map(r -> toKafkaRebalance(r, fields)) .map(permissionService.addPrivileges(ResourceTypes.Kafka.REBALANCES, KafkaRebalance::name)) .orElseThrow(() -> new NotFoundException("No such Kafka rebalance resource")); } @@ -98,11 +107,11 @@ public KafkaRebalance patchRebalance(String id, KafkaRebalance rebalance) { return client.resource(resource).patch(); }) - .map(this::toKafkaRebalance) + .map(r -> toKafkaRebalance(r, Collections.emptyList())) .orElseThrow(() -> new NotFoundException("No such Kafka rebalance resource")); } - KafkaRebalance toKafkaRebalance(io.strimzi.api.kafka.model.rebalance.KafkaRebalance resource) { + KafkaRebalance toKafkaRebalance(io.strimzi.api.kafka.model.rebalance.KafkaRebalance resource, List fields) { KafkaRebalanceSpec rebalanceSpec = resource.getSpec(); Optional rebalanceStatus = Optional.ofNullable(resource.getStatus()); Optional state = rebalanceStatus @@ -115,9 +124,10 @@ KafkaRebalance toKafkaRebalance(io.strimzi.api.kafka.model.rebalance.KafkaRebala .findFirst(); String id = Base64.getUrlEncoder().encodeToString(Cache.metaNamespaceKeyFunc(resource).getBytes(StandardCharsets.UTF_8)); + String namespace = resource.getMetadata().getNamespace(); KafkaRebalance rebalance = new KafkaRebalance(id); rebalance.name(resource.getMetadata().getName()); - rebalance.namespace(resource.getMetadata().getNamespace()); + rebalance.namespace(namespace); rebalance.creationTimestamp(resource.getMetadata().getCreationTimestamp()); rebalance.status(state.map(Enum::name).orElse(null)); rebalance.mode(Optional.ofNullable(rebalanceSpec.getMode()).map(KafkaRebalanceMode::toValue).orElse(null)); @@ -148,9 +158,88 @@ KafkaRebalance toKafkaRebalance(io.strimzi.api.kafka.model.rebalance.KafkaRebala .map(allowed -> allowed.stream().map(Enum::name).toList()) .ifPresent(rebalance.allowedActions()::addAll); + if (fields.contains(KafkaRebalance.Fields.OPTIMIZATION_PROPOSAL)) { + rebalance.optimizationProposal(getOptimizationProposal(namespace, rebalanceStatus)); + } + + if (fields.contains(KafkaRebalance.Fields.PROGRESS)) { + rebalance.progress(getProgressStatus(namespace, rebalanceStatus)); + } + return rebalance; } + private KafkaRebalance.OptimizationProposal getOptimizationProposal(String namespace, Optional rebalanceStatus) { + return rebalanceStatus + .map(KafkaRebalanceStatus::getOptimizationResult) + .map(result -> result.get("afterBeforeLoadConfigMap")) + .filter(String.class::isInstance) + .map(String.class::cast) + .map(configMapName -> client.configMaps().inNamespace(namespace).withName(configMapName).get()) + .map(configMap -> { + var data = configMap.getData(); + var qname = "%s/%s".formatted(configMap.getMetadata().getNamespace(), configMap.getMetadata().getName()); + var brokerLoadImpact = Optional.ofNullable(data.get("brokerLoad.json")) + .map(json -> { + try { + return mapper.readValue(json, new TypeReference>>() { + // No implementation + }); + } catch (Exception e) { + logger.warnf(""" + Error reading 'brokerLoad.json' from rebalance \ + afterBeforeLoadConfigMap ConfigMap[%s]: %s""", qname, e.getMessage()); + throw new RuntimeException(e); + } + }) + .orElse(null); + + return new KafkaRebalance.OptimizationProposal(brokerLoadImpact, null); + }) + .orElse(null); + } + + private KafkaRebalance.ProgressStatus getProgressStatus(String namespace, Optional rebalanceStatus) { + return rebalanceStatus + .map(KafkaRebalanceStatus::getProgress) + .map(KafkaRebalanceProgress::getRebalanceProgressConfigMap) + .map(configMapName -> client.configMaps().inNamespace(namespace).withName(configMapName).get()) + .map(configMap -> { + var data = configMap.getData(); + var qname = "%s/%s".formatted(configMap.getMetadata().getNamespace(), configMap.getMetadata().getName()); + var executorState = Optional.ofNullable(data.get("executorState.json")) + .map(json -> { + try { + return mapper.readValue(json, ExecutorState.class); + } catch (Exception e) { + logger.warnf("Error reading 'executorState.json' from rebalance progress ConfigMap[%s]: %s", qname, e.getMessage()); + throw new RuntimeException(e); + } + }) + .orElse(null); + + return new KafkaRebalance.ProgressStatus( + getInteger(data, "estimatedTimeToCompletionInMinutes", qname), + getInteger(data, "completedByteMovementPercentage", qname), + executorState + ); + }) + .orElse(null); + } + + private Integer getInteger(Map data, String key, String mapQname) { + return Optional.ofNullable(data.get(key)) + .map(value -> { + try { + return Integer.valueOf(value); + } catch (Exception e) { + logger.warnf("Error parsing '%s' from rebalance progress ConfigMap[%s]: %s", key, mapQname, e.getMessage()); + return null; + } + }) + .orElse(null); + } + KafkaRebalance tallyStatus(Map statuses, KafkaRebalance rebalance) { String status = rebalance.status(); if (status != null) { @@ -214,3 +303,4 @@ private boolean isTemplate(io.strimzi.api.kafka.model.rebalance.KafkaRebalance r .orElse(false); } } + diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index 52d60befc..e77d8948a 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -62,6 +62,13 @@ quarkus.arc.unremovable-types=com.github.streamshub.console.api.** quarkus.arc.exclude-types=io.apicurio.registry.rest.JacksonDateTimeCustomizer quarkus.arc.ignored-split-packages=io.apicurio.registry.content.*,io.apicurio.registry.rules.*, +# These properties are used to generate Java classes for the Cruise Control +# rebalance status information published by Strimzi for KafkaRebalance resources. +quarkus.openapi-generator.codegen.input-base-dir=target/schema/cruise-control/yaml +quarkus.openapi-generator.codegen.include=base.yaml +quarkus.openapi-generator.codegen.spec.base_yaml.base-package=io.streamshub.console.api.model.rebalance.cc +quarkus.openapi-generator.codegen.spec.base_yaml.generate-apis=false + quarkus.index-dependency.kafka-clients.group-id=org.apache.kafka quarkus.index-dependency.kafka-clients.artifact-id=kafka-clients quarkus.index-dependency.strimzi-api.group-id=io.strimzi diff --git a/api/src/main/webui/src/api/hooks/useRebalances.ts b/api/src/main/webui/src/api/hooks/useRebalances.ts index 70cb1a085..725376784 100644 --- a/api/src/main/webui/src/api/hooks/useRebalances.ts +++ b/api/src/main/webui/src/api/hooks/useRebalances.ts @@ -11,6 +11,7 @@ import { import { ResourceListParams, useResourceList } from './useResourceList'; const REBALANCE_FIELDS = 'name,namespace,creationTimestamp,status,mode,brokers,optimizationResult,conditions'; +const REBALANCE_DETAIL_FIELDS = `${REBALANCE_FIELDS},goals,optimizationProposal,sessionId`; /** * Fetch all rebalances for a Kafka cluster. @@ -49,7 +50,7 @@ export function useRebalance( throw new Error('Kafka ID and Rebalance ID are required'); } - const path = `/api/kafkas/${kafkaId}/rebalances/${rebalanceId}`; + const path = `/api/kafkas/${kafkaId}/rebalances/${rebalanceId}?fields[kafkaRebalances]=${REBALANCE_DETAIL_FIELDS}`; return apiClient.get(path); }, diff --git a/api/src/main/webui/src/api/types.ts b/api/src/main/webui/src/api/types.ts index 1141b06ca..101724728 100644 --- a/api/src/main/webui/src/api/types.ts +++ b/api/src/main/webui/src/api/types.ts @@ -474,6 +474,16 @@ export interface TopicMetricsResponse { // Time duration options for metrics (in seconds) export type MetricsDuration = 300 | 900 | 3600 | 21600 | 43200 | 86400; // 5min, 15min, 1hr, 6hr, 12hr, 1d +export interface BrokerLoadImpact { + before?: number | null; + after?: number | null; + diff?: number | null; +} + +export interface OptimizationProposal { + brokerImpact?: Record> | null; +} + export interface OptimizationResult { numIntraBrokerReplicaMovements?: number; numReplicaMovements?: number; @@ -520,6 +530,8 @@ export interface Rebalance { brokers: number[] | null; sessionId?: string | null; optimizationResult?: OptimizationResult; + goals?: string[] | null; + optimizationProposal?: OptimizationProposal | null; conditions?: RebalanceCondition[] | null; }; } From b407ad00a43e6bebe34af818a8da05957be1b502 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Tue, 11 Aug 2026 16:33:49 -0400 Subject: [PATCH 05/25] Add rebalance detail page, various cleanup/fixes Signed-off-by: Michael Edgar --- .../configs/rebalanceStatusConfig.ts | 8 + .../kafka/nodes/BrokerImpactTable.tsx | 423 ++++++++++++++++++ .../kafka/nodes/ProposalDetailCard.tsx | 266 +++++++++++ .../components/kafka/nodes/RebalanceModal.tsx | 226 ---------- .../kafka/nodes/RebalancesDataView.tsx | 13 +- .../kafka/nodes/RebalancesTable.tsx | 268 ----------- api/src/main/webui/src/i18n/messages/en.json | 29 ++ .../webui/src/pages/kafka/KafkaLayout.tsx | 38 +- .../pages/kafka/nodes/NodesRebalancesTab.tsx | 24 +- .../nodes/detail/RebalanceDetailPage.tsx | 272 +++++++++++ api/src/main/webui/src/routes/index.tsx | 6 + 11 files changed, 1045 insertions(+), 528 deletions(-) create mode 100644 api/src/main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx create mode 100644 api/src/main/webui/src/components/kafka/nodes/ProposalDetailCard.tsx delete mode 100644 api/src/main/webui/src/components/kafka/nodes/RebalanceModal.tsx delete mode 100644 api/src/main/webui/src/components/kafka/nodes/RebalancesTable.tsx create mode 100644 api/src/main/webui/src/pages/kafka/nodes/detail/RebalanceDetailPage.tsx diff --git a/api/src/main/webui/src/components/StatusLabel/configs/rebalanceStatusConfig.ts b/api/src/main/webui/src/components/StatusLabel/configs/rebalanceStatusConfig.ts index d53048db8..b7111ee90 100644 --- a/api/src/main/webui/src/components/StatusLabel/configs/rebalanceStatusConfig.ts +++ b/api/src/main/webui/src/components/StatusLabel/configs/rebalanceStatusConfig.ts @@ -30,41 +30,49 @@ export function createRebalanceStatusConfig( return { New: { icon: ExclamationCircleIcon, + iconStatus: 'info', label: t('rebalancing.statuses.new.label'), tooltip: t('rebalancing.statuses.new.tooltip'), }, PendingProposal: { icon: PendingIcon, + iconStatus: 'info', label: t('rebalancing.statuses.pendingProposal.label'), tooltip: t('rebalancing.statuses.pendingProposal.tooltip'), }, ProposalReady: { icon: CheckIcon, + iconStatus: 'info', label: t('rebalancing.statuses.proposalReady.label'), tooltip: t('rebalancing.statuses.proposalReady.tooltip'), }, Stopped: { icon: PauseCircleIcon, // Note: Component may replace with custom stop icon + iconStatus: 'warning', label: t('rebalancing.statuses.stopped.label'), tooltip: t('rebalancing.statuses.stopped.tooltip'), }, Rebalancing: { icon: PendingIcon, + iconStatus: 'info', label: t('rebalancing.statuses.rebalancing.label'), tooltip: t('rebalancing.statuses.rebalancing.tooltip'), }, NotReady: { icon: OutlinedClockIcon, + iconStatus: 'danger', label: t('rebalancing.statuses.notReady.label'), tooltip: t('rebalancing.statuses.notReady.tooltip'), }, Ready: { icon: CheckIcon, + iconStatus: 'success', label: t('rebalancing.statuses.ready.label'), tooltip: t('rebalancing.statuses.ready.tooltip'), }, ReconciliationPaused: { icon: PauseCircleIcon, + iconStatus: 'warning', label: t('rebalancing.statuses.reconciliationPaused.label'), tooltip: t('rebalancing.statuses.reconciliationPaused.tooltip'), }, diff --git a/api/src/main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx b/api/src/main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx new file mode 100644 index 000000000..8fdcc5933 --- /dev/null +++ b/api/src/main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx @@ -0,0 +1,423 @@ +import { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Button, + EmptyState, + EmptyStateBody, + Pagination, + SearchInput, + Select, + SelectList, + SelectOption, + MenuToggle, + Switch, + Toolbar, + ToolbarContent, + ToolbarFilter, + ToolbarGroup, + ToolbarItem, +} from '@patternfly/react-core'; +import { Table, Tbody, Td, Th, Thead, Tr, ThProps } from '@patternfly/react-table'; +import { BrokerLoadImpact } from '@/api/types'; + +interface BrokerImpactTableProps { + brokerImpact: Record> | null | undefined; +} + +interface BrokerRow { + brokerId: string; + metrics: Record; +} + +/** + * Column groups shown in the table, in display order. + * pctKey drives the bar fill and delta colouring. + * absKey (optional) is shown alongside the percentage inside the bar label. + */ +const COLUMN_GROUPS: Array<{ + label: string; + pctKey: string; + absKey?: string; +}> = [ + { label: 'Storage', pctKey: 'diskUsedPercentage', absKey: 'diskUsedMB' }, + { label: 'CPU', pctKey: 'cpuPercentage' }, +]; + +// Sortable column identifiers +type SortKey = + | 'brokerId' + | `${string}-before` + | `${string}-after` + | `${string}-delta`; + +/** Bar with the value label centred inside it. */ +function BarCell({ + pct, + label, + diff, +}: { + pct: number | null | undefined; + label: string; + diff?: number | null; +}) { + if (pct == null) return ; + + let barColor = 'var(--pf-t--global--color--brand--default)'; + if (diff != null && diff < 0) barColor = 'var(--pf-t--color--green--60)'; + if (diff != null && diff > 0) barColor = 'var(--pf-t--color--red--60)'; + + const fill = Math.min(100, Math.max(0, pct)); + + return ( + + + + {label} + + + ); +} + +function formatDiff(diff: number): string { + const sign = diff > 0 ? '+' : ''; + return `${sign}${diff % 1 === 0 ? String(diff) : diff.toFixed(2)}`; +} + +function DeltaCell({ + diff, + absDiff, +}: { + diff: number | null | undefined; + absDiff?: number | null; +}) { + if (diff == null || diff === 0) return ; + const color = diff < 0 ? 'var(--pf-t--color--green--60)' : 'var(--pf-t--color--red--60)'; + return ( + + {absDiff != null && absDiff !== 0 && ( + <>{formatDiff(absDiff)} MB /  + )} + {formatDiff(diff)}% + + ); +} + +/** Label shown inside the bar: "123 MB – 45%" or just "45%". */ +function buildBarLabel(pct: number, abs: number | null | undefined): string { + const pctStr = `${pct.toFixed(1)}%`; + if (abs == null) return pctStr; + const absMB = abs % 1 === 0 ? String(abs) : abs.toFixed(1); + return `${absMB} MB – ${pctStr}`; +} + +const DEFAULT_PAGE_SIZE = 20; + +export function BrokerImpactTable({ brokerImpact }: BrokerImpactTableProps) { + const { t } = useTranslation(); + + const [nameFilter, setNameFilter] = useState(''); + const [selectedBrokers, setSelectedBrokers] = useState([]); + const [isBrokerSelectOpen, setIsBrokerSelectOpen] = useState(false); + const [onlyDeltas, setOnlyDeltas] = useState(false); + + // Sort state + const [sortKey, setSortKey] = useState('brokerId'); + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc'); + + // Pagination state + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(DEFAULT_PAGE_SIZE); + + // Only include groups whose pctKey is actually present in the data + const activeGroups = useMemo(() => { + if (!brokerImpact) return []; + return COLUMN_GROUPS.filter((g) => + Object.values(brokerImpact).some((m) => g.pctKey in m), + ); + }, [brokerImpact]); + + // Flat rows sorted by current sort state + const allRows = useMemo((): BrokerRow[] => { + if (!brokerImpact) return []; + return Object.entries(brokerImpact) + .map(([brokerId, metrics]) => ({ brokerId, metrics })) + .sort((a, b) => { + let cmp = 0; + if (sortKey === 'brokerId') { + const aNum = parseInt(a.brokerId, 10); + const bNum = parseInt(b.brokerId, 10); + cmp = !isNaN(aNum) && !isNaN(bNum) ? aNum - bNum : a.brokerId.localeCompare(b.brokerId); + } else { + // sortKey is "-before", "-after", or "-delta" + const lastDash = sortKey.lastIndexOf('-'); + const metricKey = sortKey.slice(0, lastDash) as string; + const slot = sortKey.slice(lastDash + 1) as 'before' | 'after' | 'delta'; + const aVal = slot === 'delta' ? (a.metrics[metricKey]?.diff ?? 0) : (a.metrics[metricKey]?.[slot === 'before' ? 'before' : 'after'] ?? 0); + const bVal = slot === 'delta' ? (b.metrics[metricKey]?.diff ?? 0) : (b.metrics[metricKey]?.[slot === 'before' ? 'before' : 'after'] ?? 0); + cmp = (aVal as number) - (bVal as number); + } + return sortDirection === 'asc' ? cmp : -cmp; + }); + }, [brokerImpact, sortKey, sortDirection]); + + const brokerIds = useMemo(() => allRows.map((r) => r.brokerId), [allRows]); + + const filteredRows = useMemo(() => { + return allRows.filter((row) => { + if (nameFilter && !row.brokerId.toLowerCase().includes(nameFilter.toLowerCase())) { + return false; + } + if (selectedBrokers.length > 0 && !selectedBrokers.includes(row.brokerId)) { + return false; + } + if (onlyDeltas) { + const hasAnyDelta = activeGroups.some((g) => { + const impact = row.metrics[g.pctKey]; + return impact?.diff != null && impact.diff !== 0; + }); + if (!hasAnyDelta) return false; + } + return true; + }); + }, [allRows, nameFilter, selectedBrokers, onlyDeltas, activeGroups]); + + const pagedRows = useMemo(() => { + const start = (page - 1) * perPage; + return filteredRows.slice(start, start + perPage); + }, [filteredRows, page, perPage]); + + const toggleBroker = (brokerId: string) => { + setSelectedBrokers((prev) => + prev.includes(brokerId) ? prev.filter((b) => b !== brokerId) : [...prev, brokerId], + ); + }; + + const handleSort = (key: SortKey) => { + if (sortKey === key) { + setSortDirection((d) => (d === 'asc' ? 'desc' : 'asc')); + } else { + setSortKey(key); + setSortDirection('asc'); + } + setPage(1); + }; + + const getSortParams = (key: SortKey): ThProps['sort'] => ({ + sortBy: { + index: 0, + direction: sortKey === key ? sortDirection : undefined, + }, + onSort: () => handleSort(key), + columnIndex: 0, + }); + + if (!brokerImpact) { + return ( + + {t('rebalancing.brokerImpact.noData')} + + ); + } + + const brokerFilterLabels = selectedBrokers.map((b) => t('rebalancing.broker', { b })); + + const colCount = 1 + activeGroups.length * (onlyDeltas ? 1 : 3); + + return ( + <> + { setNameFilter(''); setSelectedBrokers([]); setPage(1); }}> + + + + { setNameFilter(val); setPage(1); }} + onClear={() => { setNameFilter(''); setPage(1); }} + /> + + { + const brokerId = brokerIds.find( + (b) => t('rebalancing.broker', { b }) === chip, + ); + if (brokerId) toggleBroker(brokerId); + }} + deleteLabelGroup={() => { setSelectedBrokers([]); setPage(1); }} + categoryName={t('rebalancing.brokerImpact.brokers')} + > + + + + + { setOnlyDeltas(checked); setPage(1); }} + /> + + {(nameFilter || selectedBrokers.length > 0) && ( + + + + )} + + setPage(newPage)} + onPerPageSelect={(_, newPerPage) => { setPerPage(newPerPage); setPage(1); }} + variant="top" + /> + + + + + + + + + {activeGroups.map((g) => ( + <> + {!onlyDeltas && ( + + )} + {!onlyDeltas && ( + + )} + + + ))} + + + + {pagedRows.length === 0 ? ( + + + + ) : ( + pagedRows.map((row) => ( + + + {activeGroups.map((g) => { + const pctImpact = row.metrics[g.pctKey]; + const absImpact = g.absKey ? row.metrics[g.absKey] : undefined; + return ( + <> + {!onlyDeltas && ( + + )} + {!onlyDeltas && ( + + )} + + + ); + })} + + )) + )} + +
{t('rebalancing.brokerImpact.broker')} + {g.label} {t('rebalancing.brokerImpact.before')} + + {g.label} {t('rebalancing.brokerImpact.after')} + + {g.label} Δ +
+ + {t('rebalancing.brokerImpact.noResults')} + +
+ {t('rebalancing.broker', { b: row.brokerId })} + + + + + + +
+ + setPage(newPage)} + onPerPageSelect={(_, newPerPage) => { setPerPage(newPerPage); setPage(1); }} + variant="bottom" + /> + + ); +} diff --git a/api/src/main/webui/src/components/kafka/nodes/ProposalDetailCard.tsx b/api/src/main/webui/src/components/kafka/nodes/ProposalDetailCard.tsx new file mode 100644 index 000000000..e2e277353 --- /dev/null +++ b/api/src/main/webui/src/components/kafka/nodes/ProposalDetailCard.tsx @@ -0,0 +1,266 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Card, + CardBody, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + ExpandableSection, + Flex, + FlexItem, + Tooltip, +} from '@patternfly/react-core'; +import { HelpIcon } from '@patternfly/react-icons'; +import { Rebalance } from '@/api/types'; + +interface ProposalDetailCardProps { + rebalance: Rebalance; +} + +interface StatTileProps { + value: string | number; + label: string; +} + +function StatTile({ value, label }: StatTileProps) { + return ( + + +

+ {value} +

+ + {label} + +
+
+ ); +} + +export function ProposalDetailCard({ rebalance }: ProposalDetailCardProps) { + const { t } = useTranslation(); + const [isExpanded, setIsExpanded] = useState(true); + + const opt = rebalance.attributes.optimizationResult; + const sessionId = rebalance.attributes.sessionId; + + return ( + + + setIsExpanded(expanded)} + > + {/* Summary stat tiles */} + + + + + + + + + + + + + {/* Detailed description list */} + {opt ? ( + + + + {t('rebalancing.optimizationProposal.dataToMove')}{' '} + + + + + + {opt.dataToMoveMB ?? 0} MB + + + + + + {t('rebalancing.optimizationProposal.excludedBrokersForLeadership')}{' '} + + + + + + {opt.excludedBrokersForLeadership?.length + ? opt.excludedBrokersForLeadership.join(', ') + : '–'} + + + + + + {t('rebalancing.optimizationProposal.excludedBrokersForReplicaMove')}{' '} + + + + + + {opt.excludedBrokersForReplicaMove?.length + ? opt.excludedBrokersForReplicaMove.join(', ') + : '–'} + + + + + + {t('rebalancing.optimizationProposal.excludedTopics')}{' '} + + + + + + {opt.excludedTopics?.length ? opt.excludedTopics.join(', ') : '–'} + + + + + + {t('rebalancing.optimizationProposal.intraBrokerDataToMove')}{' '} + + + + + + {opt.intraBrokerDataToMoveMB ?? 0} MB + + + + + + {t('rebalancing.optimizationProposal.monitoredPartitionsPercentage')}{' '} + + + + + + {opt.monitoredPartitionsPercentage ?? 0} + + + + + + {t('rebalancing.optimizationProposal.numIntraBrokerReplicaMovements')}{' '} + + + + + + {opt.numIntraBrokerReplicaMovements ?? 0} + + + + + + {t('rebalancing.optimizationProposal.numLeaderMovements')}{' '} + + + + + + {opt.numLeaderMovements ?? 0} + + + + + + {t('rebalancing.optimizationProposal.numReplicaMovements')}{' '} + + + + + + {opt.numReplicaMovements ?? 0} + + + + + + {t('rebalancing.optimizationProposal.onDemandBalancednessScoreAfter')}{' '} + + + + + + {opt.onDemandBalancednessScoreAfter ?? 0} + + + + + + {t('rebalancing.optimizationProposal.onDemandBalancednessScoreBefore')}{' '} + + + + + + {opt.onDemandBalancednessScoreBefore ?? 0} + + + + + + {t('rebalancing.optimizationProposal.recentWindows')}{' '} + + + + + + {opt.recentWindows ?? 0} + + + + + + {t('rebalancing.optimizationProposal.sessionId')}{' '} + + + + + + {sessionId ?? '–'} + + + + ) : ( +

+ {t('rebalancing.proposalDetail.noProposalData')} +

+ )} +
+
+
+ ); +} diff --git a/api/src/main/webui/src/components/kafka/nodes/RebalanceModal.tsx b/api/src/main/webui/src/components/kafka/nodes/RebalanceModal.tsx deleted file mode 100644 index f04150364..000000000 --- a/api/src/main/webui/src/components/kafka/nodes/RebalanceModal.tsx +++ /dev/null @@ -1,226 +0,0 @@ -/** - * Rebalance Modal Component - * Displays optimization proposal details for a Kafka rebalance - */ - -import { useTranslation } from 'react-i18next'; -import { - Modal, - ModalVariant, - Button, - DescriptionList, - DescriptionListGroup, - DescriptionListTerm, - DescriptionListDescription, - Tooltip, -} from '@patternfly/react-core'; -import { HelpIcon } from '@patternfly/react-icons'; -import { Rebalance } from '@/api/types'; - -interface RebalanceModalProps { - rebalance: Rebalance | null; - isOpen: boolean; - onClose: () => void; -} - -export function RebalanceModal({ rebalance, isOpen, onClose }: RebalanceModalProps) { - const { t } = useTranslation(); - - if (!rebalance) { - return null; - } - - const optimizationResult = rebalance.attributes.optimizationResult; - const sessionId = rebalance.attributes.sessionId; - - return ( - -
-

{t('rebalancing.optimizationProposal.description')}

- - - - {t('rebalancing.optimizationProposal.dataToMove')}{' '} - - - - - - {optimizationResult?.dataToMoveMB || 0} MB - - - - - - {t('rebalancing.optimizationProposal.excludedBrokersForLeadership')}{' '} - - - - - - {optimizationResult?.excludedBrokersForLeadership && - optimizationResult.excludedBrokersForLeadership.length > 0 - ? optimizationResult.excludedBrokersForLeadership.join(', ') - : '-'} - - - - - - {t('rebalancing.optimizationProposal.excludedBrokersForReplicaMove')}{' '} - - - - - - {optimizationResult?.excludedBrokersForReplicaMove && - optimizationResult.excludedBrokersForReplicaMove.length > 0 - ? optimizationResult.excludedBrokersForReplicaMove.join(', ') - : '-'} - - - - - - {t('rebalancing.optimizationProposal.excludedTopics')}{' '} - - - - - - {optimizationResult?.excludedTopics && optimizationResult.excludedTopics.length > 0 - ? optimizationResult.excludedTopics.join(', ') - : '-'} - - - - - - {t('rebalancing.optimizationProposal.intraBrokerDataToMove')}{' '} - - - - - - {optimizationResult?.intraBrokerDataToMoveMB || 0} - - - - - - {t('rebalancing.optimizationProposal.monitoredPartitionsPercentage')}{' '} - - - - - - {optimizationResult?.monitoredPartitionsPercentage || 0} - - - - - - {t('rebalancing.optimizationProposal.numIntraBrokerReplicaMovements')}{' '} - - - - - - {optimizationResult?.numIntraBrokerReplicaMovements || 0} - - - - - - {t('rebalancing.optimizationProposal.numLeaderMovements')}{' '} - - - - - - {optimizationResult?.numLeaderMovements || 0} - - - - - - {t('rebalancing.optimizationProposal.numReplicaMovements')}{' '} - - - - - - {optimizationResult?.numReplicaMovements || 0} - - - - - - {t('rebalancing.optimizationProposal.onDemandBalancednessScoreAfter')}{' '} - - - - - - {optimizationResult?.onDemandBalancednessScoreAfter || 0} - - - - - - {t('rebalancing.optimizationProposal.onDemandBalancednessScoreBefore')}{' '} - - - - - - {optimizationResult?.onDemandBalancednessScoreBefore || 0} - - - - - - {t('rebalancing.optimizationProposal.recentWindows')}{' '} - - - - - - {optimizationResult?.recentWindows || 0} - - - - - - {t('rebalancing.optimizationProposal.sessionId')}{' '} - - - - - {sessionId ?? '-'} - - -
-
- -
-
- ); -} \ No newline at end of file diff --git a/api/src/main/webui/src/components/kafka/nodes/RebalancesDataView.tsx b/api/src/main/webui/src/components/kafka/nodes/RebalancesDataView.tsx index 4e13debde..5f00fb847 100644 --- a/api/src/main/webui/src/components/kafka/nodes/RebalancesDataView.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/RebalancesDataView.tsx @@ -5,7 +5,6 @@ import { DataViewTd } from '@patternfly/react-data-view'; import { ThProps, ActionsColumn } from '@patternfly/react-table'; import { UseQueryResult } from '@tanstack/react-query'; import { - Button, DescriptionList, DescriptionListDescription, DescriptionListGroup, @@ -46,7 +45,6 @@ interface RebalancesDataViewProps { onApprove: (rebalance: Rebalance) => void; onStop: (rebalance: Rebalance) => void; onRefresh: (rebalance: Rebalance) => void; - onViewDetails: (rebalance: Rebalance) => void; } export function RebalancesDataView({ @@ -56,7 +54,6 @@ export function RebalancesDataView({ onApprove, onStop, onRefresh, - onViewDetails, }: RebalancesDataViewProps) { const { t } = useTranslation(); const statusConfig = useMemo(() => createRebalanceStatusConfig(t), [t]); @@ -152,9 +149,9 @@ export function RebalancesDataView({ } as DataViewTd, { cell: ( - + ), props: { dataLabel: t('rebalancing.rebalanceName') }, }, @@ -288,13 +285,13 @@ export function RebalancesDataView({ }], }; }, - [kafkaId, t, statusConfig, onApprove, onStop, onRefresh, onViewDetails], + [kafkaId, t, statusConfig, onApprove, onStop, onRefresh], ); const rowProvider = useMemo(() => ({ - dependencies: [kafkaId, t, statusConfig, onApprove, onStop, onRefresh, onViewDetails], + dependencies: [kafkaId, t, statusConfig, onApprove, onStop, onRefresh], callback: rowMapper, - }), [rowMapper, kafkaId, t, statusConfig, onApprove, onStop, onRefresh, onViewDetails]); + }), [rowMapper, kafkaId, t, statusConfig, onApprove, onStop, onRefresh]); return ( void; - onApprove: (rebalance: Rebalance) => void; - onStop: (rebalance: Rebalance) => void; - onRefresh: (rebalance: Rebalance) => void; - kafkaId: string; -} - -export function RebalancesTable({ - rebalances, - sortBy, - sortDirection, - onSort, - onApprove, - onStop, - onRefresh, - kafkaId, -}: RebalancesTableProps) { - const { t } = useTranslation(); - - // Create status config with i18n translations - const statusConfig = useMemo(() => createRebalanceStatusConfig(t), [t]); - - const [expandedRows, setExpandedRows] = useState>(new Set()); - const [selectedRebalance, setSelectedRebalance] = useState(null); - const [isModalOpen, setIsModalOpen] = useState(false); - - const handleRebalanceClick = (rebalance: Rebalance) => { - setSelectedRebalance(rebalance); - setIsModalOpen(true); - }; - - const handleModalClose = () => { - setIsModalOpen(false); - setSelectedRebalance(null); - }; - - const toggleRowExpanded = (id: string) => { - setExpandedRows((prev) => { - const newSet = new Set(prev); - if (newSet.has(id)) { - newSet.delete(id); - } else { - newSet.add(id); - } - return newSet; - }); - }; - - const getSortParams = (columnName: string): ThProps['sort'] => ({ - sortBy: { - index: sortBy === columnName ? 0 : undefined, - direction: sortDirection, - }, - onSort: () => onSort(columnName), - columnIndex: 0, - }); - - // Get last updated timestamp - const getLastUpdated = (rebalance: Rebalance): string => { - const statusCondition = rebalance.attributes.conditions?.find( - (c) => c.type === rebalance.attributes.status - ); - return statusCondition?.lastTransitionTime || rebalance.attributes.creationTimestamp || ''; - }; - - return ( - <> - - - - - - - - - - {rebalances?.map((rebalance) => { - const isExpanded = expandedRows.has(rebalance.id); - const lastUpdated = getLastUpdated(rebalance); - - const canUpdate = hasPrivilege('UPDATE', rebalance); - - return ( - <> - - - - - - - {isExpanded && ( - - - - )} - - ); - })} - -
- - {t('rebalancing.rebalanceName')} - {t('rebalancing.status')}{t('rebalancing.lastUpdated')} -
toggleRowExpanded(rebalance.id), - }} - /> - - - - - - {formatDateTime({ value: lastUpdated })} - - onApprove(rebalance), - isDisabled: !canUpdate || !rebalance.meta?.allowedActions?.includes('approve'), - }, - { - title: t('rebalancing.refresh'), - onClick: () => onRefresh(rebalance), - isDisabled: !canUpdate || !rebalance.meta?.allowedActions?.includes('refresh'), - }, - { - title: t('rebalancing.stop'), - onClick: () => onStop(rebalance), - isDisabled: !canUpdate || !rebalance.meta?.allowedActions?.includes('stop'), - }, - ]} - /> -
- - - - - - {t('rebalancing.autoApprovalEnabled')} - - {rebalance.meta?.autoApproval === true ? 'true' : 'false'} - - - - - - - {t('rebalancing.mode')}{' '} - {t('rebalancing.rebalanceMode')}} - bodyContent={ -
- - - {t('rebalancing.fullMode')}{' '} - {t('rebalancing.fullModeDescription')} - - - {t('rebalancing.addBrokersMode')}{' '} - {t('rebalancing.addBrokersModeDescription')} - - - {t('rebalancing.removeBrokersMode')}{' '} - {t('rebalancing.removeBrokersModeDescription')} - - -
- } - > - -
-
- - {rebalance.attributes.mode === 'full' ? ( - t('rebalancing.fullMode') - ) : ( - <> - {rebalance.attributes.mode === 'add-brokers' - ? t('rebalancing.addBrokersMode') - : t('rebalancing.removeBrokersMode')}{' '} - {rebalance.attributes.brokers?.length - ? rebalance.attributes.brokers.map((b, index) => ( - - - {t('rebalancing.broker', { b })} - - {index < (rebalance.attributes.brokers?.length || 0) - 1 && ', '} - - )) - : ''} - - )} - -
-
-
-
-
-
- - - - ); -} \ No newline at end of file diff --git a/api/src/main/webui/src/i18n/messages/en.json b/api/src/main/webui/src/i18n/messages/en.json index 7a267c6eb..4a50b569c 100644 --- a/api/src/main/webui/src/i18n/messages/en.json +++ b/api/src/main/webui/src/i18n/messages/en.json @@ -553,12 +553,41 @@ "rebalancing": "Rebalancing", "ready": "Ready", "stopped": "Stopped", + "namespace": "Namespace", + "created": "Created", + "rebalanceNotFound": "Rebalance not found.", "mode": "Mode", "rebalanceMode": "Rebalance mode", + "goals": "Goals", "autoApprovalEnabled": "Auto-approval enabled", "approve": "Approve", "refresh": "Refresh", + "refreshProposal": "Refresh proposal", "stop": "Stop", + "proposalReadyAlert": { + "title": "Proposal ready for review", + "description": "Cruise Control has generated a rebalance proposal. Review the before/after state and approve if the changes meet your needs." + }, + "brokerImpact": { + "title": "Broker impact", + "tableLabel": "Broker impact table", + "broker": "Broker", + "before": "Before", + "after": "After", + "findBroker": "Find broker", + "brokers": "Brokers", + "selectBrokers": "Select brokers", + "onlyShowDeltas": "Only show deltas", + "noData": "No broker impact data available. A proposal must be generated first.", + "noResults": "No brokers match the current filters." + }, + "proposalDetail": { + "title": "Proposal detail", + "partitionMoves": "Partition moves", + "leaderChanges": "Leader changes", + "dataToMove": "Data to move", + "noProposalData": "No proposal data available." + }, "confirm": "Confirm", "confirmApproveTitle": "Approve Rebalance Proposal?", "confirmApproveDescription": "This will apply the optimization changes. Are you sure you want to proceed?", diff --git a/api/src/main/webui/src/pages/kafka/KafkaLayout.tsx b/api/src/main/webui/src/pages/kafka/KafkaLayout.tsx index dad8df039..996202c17 100644 --- a/api/src/main/webui/src/pages/kafka/KafkaLayout.tsx +++ b/api/src/main/webui/src/pages/kafka/KafkaLayout.tsx @@ -20,6 +20,7 @@ import { useKafkaCluster, useKafkaClusters } from '@/api/hooks/useKafkaClusters' import { useConnector, useConnectCluster } from '@/api/hooks/useConnect'; import { useTopic } from '@/api/hooks/useTopics'; import { useUser } from '@/api/hooks/useUsers'; +import { useRebalance } from '@/api/hooks/useRebalances'; import { KafkaClusterSidebar } from '@/components/kafka/KafkaClusterSidebar'; import { AppMasthead } from '@/components/app/AppMasthead'; import { ReconciliationControls } from '@/components/kafka/overview/ReconciliationControls'; @@ -35,7 +36,8 @@ export function KafkaLayout() { connectorId, connectClusterId, userId, - nodeId + nodeId, + rebalanceId, } = useParams<{ kafkaId: string; topicId?: string; @@ -44,6 +46,7 @@ export function KafkaLayout() { connectClusterId?: string; userId?: string; nodeId?: string; + rebalanceId?: string; }>(); // Must be called unconditionally before any early returns. @@ -90,6 +93,12 @@ export function KafkaLayout() { { fields: ['username'] } ); + // Fetch rebalance name if we're on a rebalance detail page + const { data: rebalanceData } = useRebalance( + rebalanceId ? kafkaId : undefined, + rebalanceId, + ); + if (isLoading) { return ( }> @@ -131,8 +140,12 @@ export function KafkaLayout() { const isTopicDetailPage = !!topicId; const topicName = topicData?.data?.attributes?.name || topicId || ''; + // Check if we're on a rebalance detail page + const isRebalanceDetailPage = !!rebalanceId; + const rebalanceName = rebalanceData?.data?.attributes?.name || rebalanceId || ''; + // Check if we're on a nodes page (overview or rebalances tab) - const isNodesPage = pathSegments.includes('nodes') && !nodeId; + const isNodesPage = pathSegments.includes('nodes') && !nodeId && !isRebalanceDetailPage; const nodesTab = isNodesPage ? currentPage : null; // Check if we're on a node detail page @@ -238,6 +251,25 @@ export function KafkaLayout() { {getNodesTabTitle(nodesTab)} )} + {isRebalanceDetailPage && ( + + + {t('kafka.nodes')} + + + )} + {isRebalanceDetailPage && ( + + + {t('nodes.tabs.rebalances')} + + + )} + {isRebalanceDetailPage && ( + + {rebalanceName} + + )} {isNodeDetailPage && ( @@ -320,7 +352,7 @@ export function KafkaLayout() { {username} )} - {!isTopicDetailPage && !isNodesPage && !isNodeDetailPage && !isConnectPage && !isConnectorDetailPage && !isConnectClusterDetailPage && !isGroupDetailPage && !isUserDetailPage && currentPage !== kafkaId && ( + {!isTopicDetailPage && !isNodesPage && !isNodeDetailPage && !isRebalanceDetailPage && !isConnectPage && !isConnectorDetailPage && !isConnectClusterDetailPage && !isGroupDetailPage && !isUserDetailPage && currentPage !== kafkaId && ( {getPageTitle(currentPage)} diff --git a/api/src/main/webui/src/pages/kafka/nodes/NodesRebalancesTab.tsx b/api/src/main/webui/src/pages/kafka/nodes/NodesRebalancesTab.tsx index a40fe3805..fa3706288 100644 --- a/api/src/main/webui/src/pages/kafka/nodes/NodesRebalancesTab.tsx +++ b/api/src/main/webui/src/pages/kafka/nodes/NodesRebalancesTab.tsx @@ -1,7 +1,3 @@ -/** - * Nodes Rebalances Tab - Shows Kafka rebalances - */ - import { useState, useCallback } from 'react'; import { useParams } from 'react-router'; import { useTranslation } from 'react-i18next'; @@ -19,16 +15,14 @@ import { useKafkaCluster } from '@/api/hooks/useKafkaClusters'; import { ResourceListParams } from '@/api/hooks/useResourceList'; import { RebalancesDataView } from '@/components/kafka/nodes/RebalancesDataView'; import { RebalanceConfirmationModal } from '@/components/kafka/nodes/RebalanceConfirmationModal'; -import { RebalanceModal } from '@/components/kafka/nodes/RebalanceModal'; import { Rebalance } from '@/api/types'; export function NodesRebalancesTab() { const { t } = useTranslation(); const { kafkaId } = useParams<{ kafkaId: string }>(); const { data: clusterData } = useKafkaCluster(kafkaId, { fields: 'cruiseControlEnabled' }); - const cruiseControlEnabled = clusterData?.data?.attributes?.cruiseControlEnabled ?? false; + const cruiseControlEnabled = clusterData?.data?.attributes?.cruiseControlEnabled ?? true; - // Table params driven by RebalancesDataView const [dataParams, setDataParams] = useState({}); const rebalanceResult = useRebalances(kafkaId, dataParams); @@ -41,10 +35,6 @@ export function NodesRebalancesTab() { const [pendingAction, setPendingAction] = useState<'approve' | 'stop' | 'refresh'>('approve'); const [pendingRebalance, setPendingRebalance] = useState(null); - // Detail modal state - const [isDetailModalOpen, setIsDetailModalOpen] = useState(false); - const [selectedRebalance, setSelectedRebalance] = useState(null); - const { mutate: patchRebalance } = usePatchRebalance(kafkaId!); const handleApprove = useCallback((rebalance: Rebalance) => { @@ -65,11 +55,6 @@ export function NodesRebalancesTab() { setIsConfirmModalOpen(true); }, []); - const handleViewDetails = useCallback((rebalance: Rebalance) => { - setSelectedRebalance(rebalance); - setIsDetailModalOpen(true); - }, []); - const handleConfirmAction = () => { if (pendingRebalance) { patchRebalance({ rebalanceId: pendingRebalance.id, action: pendingAction }); @@ -115,7 +100,6 @@ export function NodesRebalancesTab() { onApprove={handleApprove} onStop={handleStop} onRefresh={handleRefresh} - onViewDetails={handleViewDetails} /> - - { setIsDetailModalOpen(false); setSelectedRebalance(null); }} - /> ); } diff --git a/api/src/main/webui/src/pages/kafka/nodes/detail/RebalanceDetailPage.tsx b/api/src/main/webui/src/pages/kafka/nodes/detail/RebalanceDetailPage.tsx new file mode 100644 index 000000000..7642f14f1 --- /dev/null +++ b/api/src/main/webui/src/pages/kafka/nodes/detail/RebalanceDetailPage.tsx @@ -0,0 +1,272 @@ +import { useCallback, useMemo, useState } from 'react'; +import { useParams } from 'react-router'; +import { useTranslation } from 'react-i18next'; +import { + Alert, + AlertActionCloseButton, + Button, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Divider, + EmptyState, + EmptyStateBody, + Flex, + FlexItem, + Label, + LabelGroup, + PageSection, + Spinner, + Title, +} from '@patternfly/react-core'; +import { SyncAltIcon } from '@patternfly/react-icons'; +import { useRebalance, usePatchRebalance } from '@/api/hooks/useRebalances'; +import { usePageTitle } from '@/hooks'; +import { StatusLabel } from '@/components/StatusLabel'; +import { createRebalanceStatusConfig } from '@/components/StatusLabel/configs'; +import { RebalanceConfirmationModal } from '@/components/kafka/nodes/RebalanceConfirmationModal'; +import { BrokerImpactTable } from '@/components/kafka/nodes/BrokerImpactTable'; +import { ProposalDetailCard } from '@/components/kafka/nodes/ProposalDetailCard'; +import { hasPrivilege } from '@/utils/privileges'; +import { formatDateTime } from '@/utils/dateTime'; +import { Rebalance } from '@/api/types'; + +function getLastUpdated(rebalance: Rebalance): string { + const statusCondition = rebalance.attributes.conditions?.find( + (c) => c.type === rebalance.attributes.status, + ); + return statusCondition?.lastTransitionTime || rebalance.attributes.creationTimestamp || ''; +} + +export function RebalanceDetailPage() { + const { t } = useTranslation(); + const { kafkaId, rebalanceId } = useParams<{ kafkaId: string; rebalanceId: string }>(); + + const { data, isLoading, error, refetch } = useRebalance(kafkaId, rebalanceId); + const rebalance = data?.data; + + const statusConfig = useMemo(() => createRebalanceStatusConfig(t), [t]); + + usePageTitle(rebalance?.attributes.name); + + // Action confirmation state + const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); + const [pendingAction, setPendingAction] = useState<'approve' | 'stop' | 'refresh'>('approve'); + + // Alert dismiss state + const [isAlertDismissed, setIsAlertDismissed] = useState(false); + + const { mutate: patchRebalance } = usePatchRebalance(kafkaId!); + + const handleAction = useCallback((action: 'approve' | 'stop' | 'refresh') => { + setPendingAction(action); + setIsConfirmModalOpen(true); + }, []); + + const handleConfirmAction = useCallback(() => { + if (rebalance) { + patchRebalance( + { rebalanceId: rebalance.id, action: pendingAction }, + { onSuccess: () => { void refetch(); } }, + ); + } + setIsConfirmModalOpen(false); + }, [rebalance, patchRebalance, pendingAction, refetch]); + + const handleCancelAction = useCallback(() => { + setIsConfirmModalOpen(false); + }, []); + + if (isLoading) { + return ( + + + + + {t('common.loading')} + + + + ); + } + + if (error || !rebalance) { + return ( + + + + {t('common.error')} + + {error?.message ?? t('rebalancing.rebalanceNotFound')} + + + ); + } + + const canUpdate = hasPrivilege('UPDATE', rebalance); + const allowedActions = rebalance.meta?.allowedActions ?? []; + const status = rebalance.attributes.status; + const lastUpdated = getLastUpdated(rebalance); + + const modeLabel = + rebalance.attributes.mode === 'full' + ? t('rebalancing.fullMode') + : rebalance.attributes.mode === 'add-brokers' + ? t('rebalancing.addBrokersMode') + : t('rebalancing.removeBrokersMode'); + + return ( + <> + {/* Header */} + + + + + {rebalance.attributes.name} + + + + + + {/* Proposal-ready alert */} + {status === 'ProposalReady' && !isAlertDismissed && ( + + setIsAlertDismissed(true)} />} + > + {t('rebalancing.proposalReadyAlert.description')} + + + )} + + {/* Action buttons */} + + + + + + + + + + + + + + + {/* Metadata */} + + + + {t('rebalancing.rebalanceName')} + {rebalance.attributes.name} + + + + {t('rebalancing.namespace')} + {rebalance.attributes.namespace ?? '–'} + + + + {t('rebalancing.created')} + + {formatDateTime({ value: rebalance.attributes.creationTimestamp })} + + + + + {t('rebalancing.lastUpdated')} + + {formatDateTime({ value: lastUpdated })} + + + + + {t('rebalancing.mode')} + {modeLabel} + + + + {t('rebalancing.autoApprovalEnabled')} + + {String(rebalance.meta?.autoApproval === true)} + + + + {rebalance.attributes.goals && rebalance.attributes.goals.length > 0 && ( + + {t('rebalancing.goals')} + + + {rebalance.attributes.goals.map((goal) => ( + + ))} + + + + )} + + + {t('rebalancing.status')} + + {status + ? + : '–'} + + + + + + + + + + {/* Broker impact table */} + + + {t('rebalancing.brokerImpact.title')} + + + + + {/* Proposal detail expandable card */} + + + + + + + ); +} diff --git a/api/src/main/webui/src/routes/index.tsx b/api/src/main/webui/src/routes/index.tsx index f83f559c9..e5fd05012 100644 --- a/api/src/main/webui/src/routes/index.tsx +++ b/api/src/main/webui/src/routes/index.tsx @@ -28,6 +28,7 @@ import { NodesOverviewTab } from '@/pages/kafka/nodes/NodesOverviewTab'; import { NodesRebalancesTab } from '@/pages/kafka/nodes/NodesRebalancesTab'; import { NodeDetailPage } from '@/pages/kafka/nodes/detail/NodeDetailPage'; import { NodeConfigurationTab } from '@/pages/kafka/nodes/detail/NodeConfigurationTab'; +import { RebalanceDetailPage } from '@/pages/kafka/nodes/detail/RebalanceDetailPage'; // Groups pages import { GroupsPage } from '@/pages/kafka/groups/GroupsPage'; @@ -127,6 +128,11 @@ export const router = createBrowserRouter([ }, ], }, + { + path: 'nodes/rebalances/:rebalanceId', + // Title is dynamic (rebalance name) — set by RebalanceDetailPage via usePageTitle + element: , + }, { path: 'nodes/:nodeId', // Title is dynamic (broker ID) — set by NodeDetailPage via usePageTitle From 054d13b1251eb7af7b9af0fdffb33ee3a26fb5f4 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Wed, 12 Aug 2026 14:24:15 -0400 Subject: [PATCH 06/25] Add missing broker impact columns, make table scroll horizontally Signed-off-by: Michael Edgar --- .../console/api/KafkaRebalancesResource.java | 2 + .../console/api/model/KafkaRebalance.java | 32 +++ .../api/service/KafkaRebalanceService.java | 20 ++ .../main/webui/src/api/hooks/useRebalances.ts | 2 +- api/src/main/webui/src/api/types.ts | 19 +- .../kafka/nodes/BrokerImpactTable.tsx | 250 +++++++++++------- .../kafka/nodes/ProposalDetailCard.tsx | 132 +++++---- api/src/main/webui/src/i18n/messages/en.json | 16 +- .../nodes/detail/RebalanceDetailPage.tsx | 21 +- .../dependents/console.clusterrole.yaml | 7 + 10 files changed, 316 insertions(+), 185 deletions(-) diff --git a/api/src/main/java/com/github/streamshub/console/api/KafkaRebalancesResource.java b/api/src/main/java/com/github/streamshub/console/api/KafkaRebalancesResource.java index b7b11724d..e2365a4ee 100644 --- a/api/src/main/java/com/github/streamshub/console/api/KafkaRebalancesResource.java +++ b/api/src/main/java/com/github/streamshub/console/api/KafkaRebalancesResource.java @@ -176,6 +176,7 @@ public Response getRebalance( KafkaRebalance.Fields.STATUS, KafkaRebalance.Fields.MODE, KafkaRebalance.Fields.BROKERS, + KafkaRebalance.Fields.BROKER_CAPACITY, KafkaRebalance.Fields.GOALS, KafkaRebalance.Fields.SKIP_HARD_GOAL_CHECK, KafkaRebalance.Fields.REBALANCE_DISK, @@ -204,6 +205,7 @@ public Response getRebalance( KafkaRebalance.Fields.STATUS, KafkaRebalance.Fields.MODE, KafkaRebalance.Fields.BROKERS, + KafkaRebalance.Fields.BROKER_CAPACITY, KafkaRebalance.Fields.GOALS, KafkaRebalance.Fields.SKIP_HARD_GOAL_CHECK, KafkaRebalance.Fields.REBALANCE_DISK, diff --git a/api/src/main/java/com/github/streamshub/console/api/model/KafkaRebalance.java b/api/src/main/java/com/github/streamshub/console/api/model/KafkaRebalance.java index fbcccf534..a9dd0c0fe 100644 --- a/api/src/main/java/com/github/streamshub/console/api/model/KafkaRebalance.java +++ b/api/src/main/java/com/github/streamshub/console/api/model/KafkaRebalance.java @@ -67,6 +67,7 @@ public static class Fields { public static final String STATUS = "status"; public static final String MODE = "mode"; public static final String BROKERS = "brokers"; + public static final String BROKER_CAPACITY = "brokerCapacity"; public static final String GOALS = "goals"; public static final String SKIP_HARD_GOAL_CHECK = "skipHardGoalCheck"; public static final String REBALANCE_DISK = "rebalanceDisk"; @@ -201,6 +202,30 @@ static final class Meta extends JsonApiMeta { String action; } + public static final record BrokerCapacityOverride( + @JsonProperty + List brokers, + @JsonProperty + String cpu, + @JsonProperty + String inboundNetwork, + @JsonProperty + String outboundNetwork + ) { + } + + public static final record BrokerCapacity( + @JsonProperty + String cpu, + @JsonProperty + String inboundNetwork, + @JsonProperty + String outboundNetwork, + @JsonProperty + List overrides + ) { + } + public static final record BrokerLoadImpact( @JsonProperty BigDecimal before, @@ -246,6 +271,9 @@ static class Attributes extends KubeAttributes { @Schema(readOnly = true, nullable = true) List brokers; + @JsonProperty + BrokerCapacity brokerCapacity; + @JsonProperty @Schema(readOnly = true, nullable = true) List goals; @@ -360,6 +388,10 @@ public void brokers(List brokers) { attributes.brokers = brokers; } + public void brokerCapacity(BrokerCapacity brokerCapacity) { + attributes.brokerCapacity = brokerCapacity; + } + public void goals(List goals) { attributes.goals = goals; } diff --git a/api/src/main/java/com/github/streamshub/console/api/service/KafkaRebalanceService.java b/api/src/main/java/com/github/streamshub/console/api/service/KafkaRebalanceService.java index 6419b8608..93fec50b9 100644 --- a/api/src/main/java/com/github/streamshub/console/api/service/KafkaRebalanceService.java +++ b/api/src/main/java/com/github/streamshub/console/api/service/KafkaRebalanceService.java @@ -36,6 +36,8 @@ import io.strimzi.api.ResourceAnnotations; import io.strimzi.api.ResourceLabels; import io.strimzi.api.kafka.model.kafka.Kafka; +import io.strimzi.api.kafka.model.kafka.KafkaSpec; +import io.strimzi.api.kafka.model.kafka.cruisecontrol.CruiseControlSpec; import io.strimzi.api.kafka.model.rebalance.KafkaRebalanceMode; import io.strimzi.api.kafka.model.rebalance.KafkaRebalanceProgress; import io.strimzi.api.kafka.model.rebalance.KafkaRebalanceSpec; @@ -132,6 +134,24 @@ KafkaRebalance toKafkaRebalance(io.strimzi.api.kafka.model.rebalance.KafkaRebala rebalance.status(state.map(Enum::name).orElse(null)); rebalance.mode(Optional.ofNullable(rebalanceSpec.getMode()).map(KafkaRebalanceMode::toValue).orElse(null)); rebalance.brokers(rebalanceSpec.getBrokers()); + rebalance.brokerCapacity(Optional.ofNullable(kafkaContext.resource()) + .map(Kafka::getSpec) + .map(KafkaSpec::getCruiseControl) + .map(CruiseControlSpec::getBrokerCapacity) + .map(capacity -> new KafkaRebalance.BrokerCapacity( + capacity.getCpu(), + capacity.getInboundNetwork(), + capacity.getOutboundNetwork(), + Optional.ofNullable(capacity.getOverrides()) + .orElseGet(Collections::emptyList) + .stream() + .map(override -> new KafkaRebalance.BrokerCapacityOverride( + override.getBrokers(), + override.getCpu(), + override.getInboundNetwork(), + override.getOutboundNetwork())) + .toList())) + .orElse(null)); rebalance.goals(rebalanceSpec.getGoals()); rebalance.skipHardGoalCheck(rebalanceSpec.isSkipHardGoalCheck()); rebalance.rebalanceDisk(rebalanceSpec.isRebalanceDisk()); diff --git a/api/src/main/webui/src/api/hooks/useRebalances.ts b/api/src/main/webui/src/api/hooks/useRebalances.ts index 725376784..0148918b0 100644 --- a/api/src/main/webui/src/api/hooks/useRebalances.ts +++ b/api/src/main/webui/src/api/hooks/useRebalances.ts @@ -11,7 +11,7 @@ import { import { ResourceListParams, useResourceList } from './useResourceList'; const REBALANCE_FIELDS = 'name,namespace,creationTimestamp,status,mode,brokers,optimizationResult,conditions'; -const REBALANCE_DETAIL_FIELDS = `${REBALANCE_FIELDS},goals,optimizationProposal,sessionId`; +const REBALANCE_DETAIL_FIELDS = `${REBALANCE_FIELDS},brokerCapacity,goals,optimizationProposal,sessionId`; /** * Fetch all rebalances for a Kafka cluster. diff --git a/api/src/main/webui/src/api/types.ts b/api/src/main/webui/src/api/types.ts index 101724728..36f497e42 100644 --- a/api/src/main/webui/src/api/types.ts +++ b/api/src/main/webui/src/api/types.ts @@ -474,10 +474,22 @@ export interface TopicMetricsResponse { // Time duration options for metrics (in seconds) export type MetricsDuration = 300 | 900 | 3600 | 21600 | 43200 | 86400; // 5min, 15min, 1hr, 6hr, 12hr, 1d +export interface BrokerCapacity { + cpu: string | null; + inboundNetwork: string | null; + outboundNetwork: string | null; + overrides: [{ + brokers: number[] | null; + cpu: string | null; + inboundNetwork: string | null; + outboundNetwork: string | null; + }]; +} + export interface BrokerLoadImpact { - before?: number | null; - after?: number | null; - diff?: number | null; + before?: number; + after?: number; + diff?: number; } export interface OptimizationProposal { @@ -528,6 +540,7 @@ export interface Rebalance { status: RebalanceStatus | null; mode: RebalanceMode; brokers: number[] | null; + brokerCapacity?: BrokerCapacity; sessionId?: string | null; optimizationResult?: OptimizationResult; goals?: string[] | null; diff --git a/api/src/main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx b/api/src/main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx index 8fdcc5933..6af9438f8 100644 --- a/api/src/main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx @@ -17,10 +17,11 @@ import { ToolbarGroup, ToolbarItem, } from '@patternfly/react-core'; -import { Table, Tbody, Td, Th, Thead, Tr, ThProps } from '@patternfly/react-table'; -import { BrokerLoadImpact } from '@/api/types'; +import { Table, Tbody, Td, Th, Thead, Tr, ThProps, InnerScrollContainer } from '@patternfly/react-table'; +import { BrokerCapacity, BrokerLoadImpact } from '@/api/types'; interface BrokerImpactTableProps { + brokerCapacity?: BrokerCapacity; brokerImpact: Record> | null | undefined; } @@ -36,11 +37,16 @@ interface BrokerRow { */ const COLUMN_GROUPS: Array<{ label: string; - pctKey: string; + pctKey?: string; absKey?: string; + absUnit?: string; }> = [ - { label: 'Storage', pctKey: 'diskUsedPercentage', absKey: 'diskUsedMB' }, + { label: 'Storage', pctKey: 'diskUsedPercentage', absKey: 'diskUsedMB', absUnit: 'MB' }, { label: 'CPU', pctKey: 'cpuPercentage' }, + { label: 'Leaders', absKey: 'leaders' }, + { label: 'Followers', absKey: 'replicas' }, + { label: 'Network In', absKey: 'leaderNetworkInRateKB', absUnit: "KB/s" }, + { label: 'Network Out', absKey: 'networkOutRateKB', absUnit: "KB/s" }, ]; // Sortable column identifiers @@ -54,18 +60,13 @@ type SortKey = function BarCell({ pct, label, - diff, }: { pct: number | null | undefined; label: string; - diff?: number | null; }) { if (pct == null) return ; - let barColor = 'var(--pf-t--global--color--brand--default)'; - if (diff != null && diff < 0) barColor = 'var(--pf-t--color--green--60)'; - if (diff != null && diff > 0) barColor = 'var(--pf-t--color--red--60)'; - + const barColor = 'var(--pf-t--global--color--brand--default)'; const fill = Math.min(100, Math.max(0, pct)); return ( @@ -110,21 +111,35 @@ function formatDiff(diff: number): string { return `${sign}${diff % 1 === 0 ? String(diff) : diff.toFixed(2)}`; } +function formatFixed(positions: number, val?: number): string { + if (val) { + if (Number.isInteger(val)) { + return val.toString(); + } else { + return val.toFixed(positions); + } + } + return ''; +} + function DeltaCell({ - diff, + pctDiff, absDiff, + absUnit, }: { - diff: number | null | undefined; - absDiff?: number | null; + pctDiff?: number; + absDiff?: number; + absUnit?: string; }) { - if (diff == null || diff === 0) return ; - const color = diff < 0 ? 'var(--pf-t--color--green--60)' : 'var(--pf-t--color--red--60)'; + if (absDiff === 0 && pctDiff === 0) { + return -; + } + return ( - - {absDiff != null && absDiff !== 0 && ( - <>{formatDiff(absDiff)} MB /  - )} - {formatDiff(diff)}% + + {absDiff !== undefined ? <>{formatDiff(absDiff)} {absUnit} : <>} + {absDiff !== undefined && pctDiff !== undefined ? <> /  : <>} + {pctDiff !== undefined ? <>{formatDiff(pctDiff ?? '-')}% : <>} ); } @@ -137,9 +152,35 @@ function buildBarLabel(pct: number, abs: number | null | undefined): string { return `${absMB} MB – ${pctStr}`; } +function buildBrokerCapacity(id: string, brokerCapacity?: BrokerCapacity): string { + if (brokerCapacity) { + const brokerId = parseInt(id); + + const capacity: { + cpu: string | null; + inboundNetwork: string | null; + outboundNetwork: string | null; + } = brokerCapacity.overrides?.find(o => o.brokers?.includes(brokerId)) + ?? brokerCapacity; + + const elements = [ + (capacity.cpu ? 'CPU: ' + capacity.cpu : null), + (capacity.inboundNetwork ? capacity.inboundNetwork + ' in' : null), + (capacity.outboundNetwork ? capacity.outboundNetwork + ' out' : null) + ]; + + return elements.filter(s => s != null).join(", "); + } + + return '-'; +} + const DEFAULT_PAGE_SIZE = 20; -export function BrokerImpactTable({ brokerImpact }: BrokerImpactTableProps) { +export function BrokerImpactTable({ + brokerCapacity, + brokerImpact +}: BrokerImpactTableProps) { const { t } = useTranslation(); const [nameFilter, setNameFilter] = useState(''); @@ -159,7 +200,7 @@ export function BrokerImpactTable({ brokerImpact }: BrokerImpactTableProps) { const activeGroups = useMemo(() => { if (!brokerImpact) return []; return COLUMN_GROUPS.filter((g) => - Object.values(brokerImpact).some((m) => g.pctKey in m), + Object.values(brokerImpact).some((m) => (g.absKey ?? '' in m) || (g.pctKey ?? '' in m)), ); }, [brokerImpact]); @@ -169,11 +210,13 @@ export function BrokerImpactTable({ brokerImpact }: BrokerImpactTableProps) { return Object.entries(brokerImpact) .map(([brokerId, metrics]) => ({ brokerId, metrics })) .sort((a, b) => { - let cmp = 0; + // eslint-disable-next-line no-useless-assignment + let result = 0; + if (sortKey === 'brokerId') { const aNum = parseInt(a.brokerId, 10); const bNum = parseInt(b.brokerId, 10); - cmp = !isNaN(aNum) && !isNaN(bNum) ? aNum - bNum : a.brokerId.localeCompare(b.brokerId); + result = !isNaN(aNum) && !isNaN(bNum) ? aNum - bNum : a.brokerId.localeCompare(b.brokerId); } else { // sortKey is "-before", "-after", or "-delta" const lastDash = sortKey.lastIndexOf('-'); @@ -181,9 +224,10 @@ export function BrokerImpactTable({ brokerImpact }: BrokerImpactTableProps) { const slot = sortKey.slice(lastDash + 1) as 'before' | 'after' | 'delta'; const aVal = slot === 'delta' ? (a.metrics[metricKey]?.diff ?? 0) : (a.metrics[metricKey]?.[slot === 'before' ? 'before' : 'after'] ?? 0); const bVal = slot === 'delta' ? (b.metrics[metricKey]?.diff ?? 0) : (b.metrics[metricKey]?.[slot === 'before' ? 'before' : 'after'] ?? 0); - cmp = (aVal as number) - (bVal as number); + result = (aVal as number) - (bVal as number); } - return sortDirection === 'asc' ? cmp : -cmp; + + return sortDirection === 'asc' ? result : -result; }); }, [brokerImpact, sortKey, sortDirection]); @@ -199,7 +243,7 @@ export function BrokerImpactTable({ brokerImpact }: BrokerImpactTableProps) { } if (onlyDeltas) { const hasAnyDelta = activeGroups.some((g) => { - const impact = row.metrics[g.pctKey]; + const impact = row.metrics[g.absKey ?? ''] ?? row.metrics[g.pctKey ?? '']; return impact?.diff != null && impact.diff !== 0; }); if (!hasAnyDelta) return false; @@ -335,80 +379,94 @@ export function BrokerImpactTable({ brokerImpact }: BrokerImpactTableProps) { - - - - - {activeGroups.map((g) => ( - <> - {!onlyDeltas && ( - - )} - {!onlyDeltas && ( - - )} - - - ))} - - - - {pagedRows.length === 0 ? ( + +
{t('rebalancing.brokerImpact.broker')} - {g.label} {t('rebalancing.brokerImpact.before')} - - {g.label} {t('rebalancing.brokerImpact.after')} - - {g.label} Δ -
+ - + + {activeGroups.map((g) => ( + <> + {!onlyDeltas && ( + + )} + {!onlyDeltas && ( + + )} + + + ))} + - ) : ( - pagedRows.map((row) => ( - - + + {pagedRows.length === 0 ? ( + + - {activeGroups.map((g) => { - const pctImpact = row.metrics[g.pctKey]; - const absImpact = g.absKey ? row.metrics[g.absKey] : undefined; - return ( - <> - {!onlyDeltas && ( - - )} - {!onlyDeltas && ( - + + {activeGroups.map((g) => { + const pctImpact = g.pctKey ? row.metrics[g.pctKey] : undefined; + const absImpact = g.absKey ? row.metrics[g.absKey] : undefined; + return ( + <> + {!onlyDeltas && ( + + )} + {!onlyDeltas && ( + + )} + - )} - - - ); - })} - - )) - )} - -
- - {t('rebalancing.brokerImpact.noResults')} - - + {t('rebalancing.brokerImpact.broker')} + + {g.label} {t('rebalancing.brokerImpact.before')} + + {g.label} {t('rebalancing.brokerImpact.after')} + + {g.label} Δ + + {t('rebalancing.brokerImpact.brokerCapacity')} +
- {t('rebalancing.broker', { b: row.brokerId })} +
+ + {t('rebalancing.brokerImpact.noResults')} + - - - + ) : ( + pagedRows.map((row) => ( +
+ {t('rebalancing.broker', { b: row.brokerId })} + + {pctImpact + ? + : <>{formatFixed(2, absImpact?.before)} {g?.absUnit}} + + {pctImpact + ? + : <>{formatFixed(2, absImpact?.after)} {g?.absUnit}} + + - -
+ + ); + })} + + {buildBrokerCapacity(row.brokerId, brokerCapacity)} + + + )) + )} + + + + {t('rebalancing.optimizationProposal.numReplicaMovements')}{' '} + + + + + } /> + {t('rebalancing.optimizationProposal.numLeaderMovements')}{' '} + + + + + } /> + {t('rebalancing.optimizationProposal.dataToMove')}{' '} + + + + + } /> @@ -96,77 +117,49 @@ export function ProposalDetailCard({ rebalance }: ProposalDetailCardProps) { > - {t('rebalancing.optimizationProposal.dataToMove')}{' '} - - - - - - {opt.dataToMoveMB ?? 0} MB - - - - - - {t('rebalancing.optimizationProposal.excludedBrokersForLeadership')}{' '} - - - - - - {opt.excludedBrokersForLeadership?.length - ? opt.excludedBrokersForLeadership.join(', ') - : '–'} - - - - - - {t('rebalancing.optimizationProposal.excludedBrokersForReplicaMove')}{' '} - + {t('rebalancing.optimizationProposal.sessionId')}{' '} + - {opt.excludedBrokersForReplicaMove?.length - ? opt.excludedBrokersForReplicaMove.join(', ') - : '–'} + {sessionId ?? '–'} - {t('rebalancing.optimizationProposal.excludedTopics')}{' '} - + {t('rebalancing.optimizationProposal.recentWindows')}{' '} + - {opt.excludedTopics?.length ? opt.excludedTopics.join(', ') : '–'} + {opt.recentWindows ?? '-'} - {t('rebalancing.optimizationProposal.intraBrokerDataToMove')}{' '} - + {t('rebalancing.optimizationProposal.onDemandBalancednessScoreBefore')}{' '} + - {opt.intraBrokerDataToMoveMB ?? 0} MB + {opt.onDemandBalancednessScoreBefore ?? '-'} - {t('rebalancing.optimizationProposal.monitoredPartitionsPercentage')}{' '} - + {t('rebalancing.optimizationProposal.onDemandBalancednessScoreAfter')}{' '} + - {opt.monitoredPartitionsPercentage ?? 0} + {opt.onDemandBalancednessScoreAfter ?? '-'} @@ -178,81 +171,74 @@ export function ProposalDetailCard({ rebalance }: ProposalDetailCardProps) { - {opt.numIntraBrokerReplicaMovements ?? 0} + {opt.numIntraBrokerReplicaMovements ?? '-'} - {t('rebalancing.optimizationProposal.numLeaderMovements')}{' '} - + {t('rebalancing.optimizationProposal.intraBrokerDataToMove')}{' '} + - {opt.numLeaderMovements ?? 0} + {opt.intraBrokerDataToMoveMB ? opt.intraBrokerDataToMoveMB + ' MB' : '-'} - {t('rebalancing.optimizationProposal.numReplicaMovements')}{' '} - + {t('rebalancing.optimizationProposal.excludedBrokersForReplicaMove')}{' '} + - {opt.numReplicaMovements ?? 0} + {opt.excludedBrokersForReplicaMove?.length + ? opt.excludedBrokersForReplicaMove.join(', ') + : '–'} - {t('rebalancing.optimizationProposal.onDemandBalancednessScoreAfter')}{' '} - + {t('rebalancing.optimizationProposal.excludedBrokersForLeadership')}{' '} + - {opt.onDemandBalancednessScoreAfter ?? 0} + {opt.excludedBrokersForLeadership?.length + ? opt.excludedBrokersForLeadership.join(', ') + : '–'} - {t('rebalancing.optimizationProposal.onDemandBalancednessScoreBefore')}{' '} - + {t('rebalancing.optimizationProposal.excludedTopics')}{' '} + - {opt.onDemandBalancednessScoreBefore ?? 0} + {opt.excludedTopics?.length ? opt.excludedTopics.join(', ') : '–'} - {t('rebalancing.optimizationProposal.recentWindows')}{' '} - + {t('rebalancing.optimizationProposal.monitoredPartitionsPercentage')}{' '} + - {opt.recentWindows ?? 0} + {opt.monitoredPartitionsPercentage ?? '-'} - - - {t('rebalancing.optimizationProposal.sessionId')}{' '} - - - - - - {sessionId ?? '–'} - - ) : (

diff --git a/api/src/main/webui/src/i18n/messages/en.json b/api/src/main/webui/src/i18n/messages/en.json index 4a50b569c..4785cda0b 100644 --- a/api/src/main/webui/src/i18n/messages/en.json +++ b/api/src/main/webui/src/i18n/messages/en.json @@ -572,6 +572,7 @@ "title": "Broker impact", "tableLabel": "Broker impact table", "broker": "Broker", + "brokerCapacity": "Cruise Control Broker Capacity", "before": "Before", "after": "After", "findBroker": "Find broker", @@ -583,9 +584,6 @@ }, "proposalDetail": { "title": "Proposal detail", - "partitionMoves": "Partition moves", - "leaderChanges": "Leader changes", - "dataToMove": "Data to move", "noProposalData": "No proposal data available." }, "confirm": "Confirm", @@ -652,7 +650,7 @@ "optimizationProposal": { "title": "Optimization proposal for KafkaRebalance", "description": "A summary of proposed changes based on defined optimization goals, assessed in a specific order of priority.", - "dataToMove": "Data To Move MB", + "dataToMove": "Data To Move", "dataToMoveTooltip": "Total amount of data (in MB) moved across brokers as part of the optimization.", "excludedBrokersForLeadership": "Excluded Brokers For Leadership", "excludedBrokersForLeadershipTooltip": "Brokers excluded from becoming leaders for any partition.", @@ -660,15 +658,15 @@ "excludedBrokersForReplicaMoveTooltip": "Brokers excluded from losing existing replicas or receiving new replicas", "excludedTopics": "Excluded Topics", "excludedTopicsTooltip": "Topics excluded from partition movements.", - "intraBrokerDataToMove": "Intra Broker Data to Move MB", + "intraBrokerDataToMove": "Intrabroker Data to Move", "intraBrokerDataToMoveTooltip": "Total amount of data (in MB) moved within individual brokers. For example, log directory changes.", "monitoredPartitionsPercentage": "Monitored Partitions Percentage", "monitoredPartitionsPercentageTooltip": "Percentage of partitions being monitored for optimization. If this percentage is low, it may be necessary to investigate why certain partitions are not being monitored.", - "numIntraBrokerReplicaMovements": "Num Intra Broker Replica Movements", + "numIntraBrokerReplicaMovements": "Intrabroker Replica Movements", "numIntraBrokerReplicaMovementsTooltip": "Number of replica movements within the same broker.", - "numLeaderMovements": "Num Leader Movements", + "numLeaderMovements": "Leader Movements", "numLeaderMovementsTooltip": "Number of leadership changes for partitions.", - "numReplicaMovements": "Number Replica Movements", + "numReplicaMovements": "Replica Movements", "numReplicaMovementsTooltip": "Total number of replica movements across brokers.", "onDemandBalancednessScoreAfter": "On Demand Balancedness Score After", "onDemandBalancednessScoreAfterTooltip": "Balancedness score of the Kafka cluster after optimization. If all goals are satisfied, the score is 100.", @@ -676,7 +674,7 @@ "onDemandBalancednessScoreBeforeTooltip": "Balancedness score of the Kafka cluster before optimization. Scores range from 0 to 100, where lower values indicate greater imbalance.", "recentWindows": "Recent Windows", "recentWindowsTooltip": "Number of recent monitoring windows used to assess the Kafka cluster's performance for this optimization.", - "sessionId": "Session Id", + "sessionId": "Session ID", "sessionIdTooltip": "Unique identifier for the optimization." } }, diff --git a/api/src/main/webui/src/pages/kafka/nodes/detail/RebalanceDetailPage.tsx b/api/src/main/webui/src/pages/kafka/nodes/detail/RebalanceDetailPage.tsx index 7642f14f1..6f346c8e0 100644 --- a/api/src/main/webui/src/pages/kafka/nodes/detail/RebalanceDetailPage.tsx +++ b/api/src/main/webui/src/pages/kafka/nodes/detail/RebalanceDetailPage.tsx @@ -5,6 +5,7 @@ import { Alert, AlertActionCloseButton, Button, + CodeBlock, DescriptionList, DescriptionListDescription, DescriptionListGroup, @@ -234,10 +235,22 @@ export function RebalanceDetailPage() { {t('rebalancing.status')} - {status - ? @@ -253,7 +266,9 @@ export function RebalanceDetailPage() { {t('rebalancing.brokerImpact.title')} - + {/* Proposal detail expandable card */} diff --git a/operator/src/main/resources/com/github/streamshub/console/dependents/console.clusterrole.yaml b/operator/src/main/resources/com/github/streamshub/console/dependents/console.clusterrole.yaml index 7a4647d0c..6a07370c0 100644 --- a/operator/src/main/resources/com/github/streamshub/console/dependents/console.clusterrole.yaml +++ b/operator/src/main/resources/com/github/streamshub/console/dependents/console.clusterrole.yaml @@ -33,6 +33,13 @@ rules: - "" resources: - pods + # API may read the ConfigMap associated with a KafkaRebalance + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get # API may read the ClusterVersion to fetch the OpenShift version for display - verbs: - get From 429654299cd2fba8e067d246afad1bbd9a00796c Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Wed, 12 Aug 2026 15:54:26 -0400 Subject: [PATCH 07/25] Render node storage and partition distribution charts horizontally Signed-off-by: Michael Edgar --- .../nodes/charts/ChartNodeStorageUsage.tsx | 23 +++++++++--------- .../charts/ChartPartitionDistribution.tsx | 24 +++++++++---------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx index f80beadc2..1494c7609 100644 --- a/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx @@ -13,7 +13,6 @@ import { Node } from '@/api/types'; import { formatBytes } from '@/utils/format'; import { useChartWidth } from '@/components/kafka/overview/utils/useChartWidth'; import { getPadding } from '@/components/kafka/overview/utils/chartConsts'; -import { VictoryZoomContainer } from 'victory-zoom-container'; interface ChartNodeStorageUsageProps { nodes: Node[]; @@ -65,26 +64,25 @@ export function ChartNodeStorageUsage({ nodes }: ChartNodeStorageUsageProps) { // so we derive explicit tickValues instead. const maxCapacity = Math.max(...storageNodes.map((n) => n.attributes.storageCapacity as number)); const tickStep = maxCapacity / 4; + // Round step up to a power-of-1024 boundary so labels stay in one unit. const unitBoundary = Math.pow(1024, Math.floor(Math.log(tickStep) / Math.log(1024))); const roundedStep = Math.ceil(tickStep / unitBoundary) * unitBoundary; const tickValues = [0, 1, 2, 3, 4].map((i) => i * roundedStep); - const barWidth = 20; + // Configure custom spacing dimensions + const barWidth = 20; // Thickness of each individual bar + const innerPadding = 16; // Distance between bars in pixels + + // Dynamically calculate the SVG canvas size based on data density + const calculatedChartHeight = usedData.length * (barWidth + innerPadding) + 100; const legendRows = 1; - const padding = { ...getPadding(legendRows), left: 90 }; + const padding = { ...getPadding(legendRows), left: 70 }; return ( -

+
- } legendPosition="bottom-left" legendComponent={ @@ -93,6 +91,7 @@ export function ChartNodeStorageUsage({ nodes }: ChartNodeStorageUsageProps) { domainPadding={{ x: [30, 25] }} themeColor={ChartThemeColor.multiOrdered} width={width} + height={calculatedChartHeight} legendAllowWrap={true} > formatBytes(d)} - style={{ axisLabel: { padding: 75 } }} + horizontal /> diff --git a/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx b/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx index 00f507140..5e62a9cdd 100644 --- a/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx @@ -8,7 +8,6 @@ import { ChartThemeColor, ChartTooltip, } from '@patternfly/react-charts/victory'; -import { VictoryZoomContainer } from 'victory-zoom-container'; import { Alert } from '@patternfly/react-core'; import { Node } from '@/api/types'; import { formatNumber } from '@/utils/format'; @@ -64,21 +63,19 @@ export function ChartPartitionDistribution({ nodes }: ChartPartitionDistribution { name: t('nodes.charts.partitionDistributionSeriesLeaders') }, ]; - const barWidth = 20; + // Configure custom spacing dimensions + const barWidth = 20; // Thickness of each individual bar + const innerPadding = 16; // Distance between bars in pixels + + // Dynamically calculate the SVG canvas size based on data density + const calculatedChartHeight = leadersData.length * (barWidth + innerPadding) + 100; const legendRows = 1; - const padding = { ...getPadding(legendRows), left: 90 }; + const padding = { ...getPadding(legendRows), left: 70 }; return ( -
+
- } legendPosition="bottom-left" legendComponent={ @@ -87,13 +84,14 @@ export function ChartPartitionDistribution({ nodes }: ChartPartitionDistribution domainPadding={{ x: [30, 25] }} themeColor={ChartThemeColor.multiOrdered} width={width} + height={calculatedChartHeight} legendAllowWrap={true} > From cc714a664c39acb1cc9ea5787818895f6ccd39b5 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Thu, 13 Aug 2026 07:33:08 -0400 Subject: [PATCH 08/25] Add reference to LinkedIn repository for Cruise Control artifact Signed-off-by: Michael Edgar --- pom.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pom.xml b/pom.xml index 807867ae2..a0c4312e9 100644 --- a/pom.xml +++ b/pom.xml @@ -87,6 +87,17 @@ systemtests + + + linkedin-artifactory + LinkedIn JFrog Artifactory + https://linkedin.jfrog.io/artifactory/release + + false + + + + From 921110b67b1aca8c970bb55ffb82c07965aca01d Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Thu, 13 Aug 2026 08:43:36 -0400 Subject: [PATCH 09/25] Remove unused class Signed-off-by: Michael Edgar --- .../api/model/rebalance/BrokerLoadImpact.java | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 api/src/main/java/com/github/streamshub/console/api/model/rebalance/BrokerLoadImpact.java diff --git a/api/src/main/java/com/github/streamshub/console/api/model/rebalance/BrokerLoadImpact.java b/api/src/main/java/com/github/streamshub/console/api/model/rebalance/BrokerLoadImpact.java deleted file mode 100644 index 95cd850da..000000000 --- a/api/src/main/java/com/github/streamshub/console/api/model/rebalance/BrokerLoadImpact.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.streamshub.console.api.model.rebalance; - -import java.math.BigDecimal; - -public record BrokerLoadImpact( - BigDecimal before, - BigDecimal after, - BigDecimal diff -) { - -} - From c95f38d78dbd21014c9583da3fda25cc7c6802c6 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Thu, 13 Aug 2026 10:21:26 -0400 Subject: [PATCH 10/25] Disable Quarkus `write-transformed-bytecode-to-build-output` Signed-off-by: Michael Edgar --- .github/workflows/integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index d62d5e92d..645a8d3ae 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -166,7 +166,7 @@ jobs: # See: https://quarkus.io/guides/tests-with-coverage#coverage-for-integration-tests # mvn verify -B --no-transfer-progress -DskipSTs \ - -Dquarkus.package.write-transformed-bytecode-to-build-output=true + -Dquarkus.package.write-transformed-bytecode-to-build-output=false - name: Archive Failed Tests Results uses: actions/upload-artifact@v7 From 15e413e2d7db8149f7326a79879a1055b5b9cc33 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Fri, 14 Aug 2026 14:04:06 -0400 Subject: [PATCH 11/25] Refine node storage and partition distribution charts Signed-off-by: Michael Edgar --- .../nodes/charts/ChartNodeStorageUsage.tsx | 56 +++++++++++-------- .../charts/ChartPartitionDistribution.tsx | 37 ++++++------ 2 files changed, 53 insertions(+), 40 deletions(-) diff --git a/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx index 1494c7609..ce2bddf27 100644 --- a/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx @@ -13,6 +13,7 @@ import { Node } from '@/api/types'; import { formatBytes } from '@/utils/format'; import { useChartWidth } from '@/components/kafka/overview/utils/useChartWidth'; import { getPadding } from '@/components/kafka/overview/utils/chartConsts'; +import { useMemo } from 'react'; interface ChartNodeStorageUsageProps { nodes: Node[]; @@ -22,37 +23,34 @@ export function ChartNodeStorageUsage({ nodes }: ChartNodeStorageUsageProps) { const { t } = useTranslation(); const [containerRef, width] = useChartWidth(); - const storageNodes = nodes.filter( - (n) => n.attributes.storageUsed != null && n.attributes.storageCapacity != null, - ).sort((n1, n2) => parseInt(n2.id) - parseInt(n1.id)); + const storageNodes = useMemo(() => nodes + .filter((n) => n.attributes.storageUsed != null && n.attributes.storageCapacity != null) + .sort((n1, n2) => parseInt(n2.id) - parseInt(n1.id)), + [nodes]); - if (storageNodes.length === 0) { - return ( - - ); - } - - const usedData = storageNodes.map((n) => ({ + const usedData = useMemo(() => storageNodes.map((n) => ({ name: t('nodes.charts.storageUsageSeriesUsed'), x: `Node ${n.id}`, y: n.attributes.storageUsed as number, - label: `Node ${n.id}\n${t('nodes.charts.storageUsageSeriesUsed')}: ${formatBytes(n.attributes.storageUsed as number)}`, - })); + //label: `${t('nodes.charts.storageUsageSeriesUsed')}: ${formatBytes(n.attributes.storageUsed as number)}`, + })), [t, storageNodes]); - const availableData = storageNodes.map((n) => { + const availableData = useMemo(() => storageNodes.map((n) => { const available = (n.attributes.storageCapacity as number) - (n.attributes.storageUsed as number); return { name: t('nodes.charts.storageUsageSeriesAvailable'), x: `Node ${n.id}`, y: available, - label: `Node ${n.id}\n${t('nodes.charts.storageUsageSeriesAvailable')}: ${formatBytes(available)}`, + //label: `${t('nodes.charts.storageUsageSeriesAvailable')}: ${formatBytes(available)}`, }; - }); + }), [t, storageNodes]); + + const labels = useMemo(() => storageNodes.map((n) => { + const capacity = n.attributes.storageCapacity as number; + const used = n.attributes.storageUsed as number; + const available = capacity - used; + return `Used ${formatBytes(used)} (${((used / capacity) * 100).toFixed(2)}%) of ${formatBytes(capacity)}\nAvailable ${formatBytes(available)} (${((available / capacity) * 100).toFixed(2)}%)`; + }), [storageNodes]); const legendData = [ { name: t('nodes.charts.storageUsageSeriesUsed') }, @@ -79,6 +77,17 @@ export function ChartNodeStorageUsage({ nodes }: ChartNodeStorageUsageProps) { const legendRows = 1; const padding = { ...getPadding(legendRows), left: 70 }; + if (storageNodes.length === 0) { + return ( + + ); + } + return (
- + } + > } /> } /> diff --git a/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx b/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx index 5e62a9cdd..dd27cfbe6 100644 --- a/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx @@ -13,6 +13,7 @@ import { Node } from '@/api/types'; import { formatNumber } from '@/utils/format'; import { useChartWidth } from '@/components/kafka/overview/utils/useChartWidth'; import { getPadding } from '@/components/kafka/overview/utils/chartConsts'; +import { useMemo } from 'react'; interface ChartPartitionDistributionProps { nodes: Node[]; @@ -22,22 +23,13 @@ export function ChartPartitionDistribution({ nodes }: ChartPartitionDistribution const { t } = useTranslation(); const [containerRef, width] = useChartWidth(); - const brokerNodes = nodes.filter((n) => n.attributes.broker != null) - .sort((n1, n2) => parseInt(n2.id) - parseInt(n1.id)); - - if (brokerNodes.length === 0) { - return ( - - ); - } + const brokerNodes = useMemo(() => nodes + .filter((n) => n.attributes.broker != null) + .sort((n1, n2) => parseInt(n2.id) - parseInt(n1.id)), + [nodes]); // Bottom segment: leader partitions - const leadersData = brokerNodes.map((n) => { + const leadersData = useMemo(() => brokerNodes.map((n) => { const broker = n.attributes.broker!; return { name: t('nodes.charts.partitionDistributionSeriesLeaders'), @@ -45,10 +37,10 @@ export function ChartPartitionDistribution({ nodes }: ChartPartitionDistribution y: broker.leaderCount, label: `${t('nodes.charts.partitionDistributionSeriesLeaders')}: ${formatNumber(broker.leaderCount)}`, }; - }); + }), [t, brokerNodes]); // Top segment: follower replicas only (excludes leaders) - const replicasData = brokerNodes.map((n) => { + const replicasData = useMemo(() => brokerNodes.map((n) => { const broker = n.attributes.broker!; return { name: t('nodes.charts.partitionDistributionSeriesReplicas'), @@ -56,7 +48,7 @@ export function ChartPartitionDistribution({ nodes }: ChartPartitionDistribution y: broker.replicaCount, label: `${t('nodes.charts.partitionDistributionSeriesReplicas')}: ${formatNumber(broker.replicaCount)}`, }; - }); + }), [t, brokerNodes]); const legendData = [ { name: t('nodes.charts.partitionDistributionSeriesReplicas') }, @@ -72,6 +64,17 @@ export function ChartPartitionDistribution({ nodes }: ChartPartitionDistribution const legendRows = 1; const padding = { ...getPadding(legendRows), left: 70 }; + if (brokerNodes.length === 0) { + return ( + + ); + } + return (
Date: Fri, 14 Aug 2026 14:05:05 -0400 Subject: [PATCH 12/25] Fix TypeScript Playwright tests for data view changes Signed-off-by: Michael Edgar --- ui/tests/playwright/NodePropertyPage.test.tsx | 2 +- ui/tests/playwright/NodesPage.test.tsx | 24 +++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/ui/tests/playwright/NodePropertyPage.test.tsx b/ui/tests/playwright/NodePropertyPage.test.tsx index a6a48576a..522c653e5 100644 --- a/ui/tests/playwright/NodePropertyPage.test.tsx +++ b/ui/tests/playwright/NodePropertyPage.test.tsx @@ -8,7 +8,7 @@ test("Node property page", async ({ page, authenticatedPage }) => { await test.step("Navigate to node property page", async () => { await page.click('text="Kafka Nodes"'); await expect(page.getByRole('columnheader', { name: 'Node ID' })).toBeVisible(); - await authenticatedPage.clickFirstLinkInTheTable("nodes-listing"); + await authenticatedPage.clickFirstLinkInTheTable("nodes-table"); await expect(page.getByRole('columnheader', { name: 'Property' })).toBeVisible(); }); await test.step("Node page should display properties", async () => { diff --git a/ui/tests/playwright/NodesPage.test.tsx b/ui/tests/playwright/NodesPage.test.tsx index d3549abe7..76df797aa 100644 --- a/ui/tests/playwright/NodesPage.test.tsx +++ b/ui/tests/playwright/NodesPage.test.tsx @@ -11,27 +11,31 @@ test("Nodes page", async ({ page, authenticatedPage }) => { }); await test.step("Nodes page should display table", async () => { await expect(page.locator('h1').getByText('Nodes')).toBeVisible(); - await expect(page.getByText('Node Partition Distribution')).toBeVisible(); const headerRows = await page - .locator('table[data-ouia-component-id="nodes-listing"] thead tr') + .locator('table[data-ouia-component-id="nodes-table"] thead tr') .all(); const headerRow = headerRows[0]; - expect(await headerRow.locator("th").nth(1).innerText()).toBe("Node ID"); - expect(await headerRow.locator("th").nth(2).innerText()).toBe("Roles"); - expect(await headerRow.locator("th").nth(3).innerText()).toBe("Status"); - expect(await headerRow.locator("th").nth(4).innerText()).toContain( + let col = 0; + expect(await headerRow.locator("th").nth(col++).innerText()).toBe("Node ID"); + expect(await headerRow.locator("th").nth(col++).innerText()).toBe("Roles"); + expect(await headerRow.locator("th").nth(col++).innerText()).toBe("Status"); + expect(await headerRow.locator("th").nth(col++).innerText()).toBe("Kafka version"); + expect(await headerRow.locator("th").nth(col++).innerText()).toContain( "Total Replicas ", ); - expect(await headerRow.locator("th").nth(5).innerText()).toContain("Rack "); - expect(await headerRow.locator("th").nth(6).innerText()).toBe("Node Pool"); + expect(await headerRow.locator("th").nth(col++).innerText()).toContain( + "Leader partitions ", + ); + expect(await headerRow.locator("th").nth(col++).innerText()).toContain("Rack "); + expect(await headerRow.locator("th").nth(col++).innerText()).toBe("Node Pool"); const dataRows = await page - .locator('table[data-ouia-component-id="nodes-listing"] tbody tr') + .locator('table[data-ouia-component-id="nodes-table"] tbody tr') .count(); expect(dataRows).toBeGreaterThan(0); const dataCells = await page - .locator('table[data-ouia-component-id="nodes-listing"] tbody tr td') + .locator('table[data-ouia-component-id="nodes-table"] tbody tr td') .evaluateAll((tds) => tds.map((td) => td.innerHTML?.trim() ?? "")); expect(dataCells.length).toBeGreaterThan(0); From bd7755f48f0b3683f13930ffa8948d4f23bc46dd Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Fri, 14 Aug 2026 14:17:01 -0400 Subject: [PATCH 13/25] Remove commented Cruise Control progress code Signed-off-by: Michael Edgar --- api/pom.xml | 5 -- .../console/api/model/KafkaRebalance.java | 27 +--------- .../api/service/KafkaRebalanceService.java | 49 ++----------------- 3 files changed, 5 insertions(+), 76 deletions(-) diff --git a/api/pom.xml b/api/pom.xml index ceed752fb..fa5673f20 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -109,11 +109,6 @@ quarkus-quinoa ${quarkus-quinoa.version} - - io.quarkiverse.openapi.generator - quarkus-openapi-generator - 2.23.0 - org.apache.kafka diff --git a/api/src/main/java/com/github/streamshub/console/api/model/KafkaRebalance.java b/api/src/main/java/com/github/streamshub/console/api/model/KafkaRebalance.java index a9dd0c0fe..a9edaedcd 100644 --- a/api/src/main/java/com/github/streamshub/console/api/model/KafkaRebalance.java +++ b/api/src/main/java/com/github/streamshub/console/api/model/KafkaRebalance.java @@ -31,8 +31,6 @@ import com.github.streamshub.console.api.support.ListRequestContext; import com.github.streamshub.console.api.support.StringEnumeration; -import io.streamshub.console.api.model.rebalance.cc.model.ExecutorState; -import io.streamshub.console.api.model.rebalance.cc.model.OptimizationResult; import io.xlate.validation.constraints.Expression; import static java.util.Comparator.comparing; @@ -80,7 +78,6 @@ public static class Fields { public static final String SESSION_ID = "sessionId"; public static final String OPTIMIZATION_RESULT = "optimizationResult"; public static final String OPTIMIZATION_PROPOSAL = "optimizationProposal"; - public static final String PROGRESS = "progress"; public static final String CONDITIONS = "conditions"; static final Comparator ID_COMPARATOR = @@ -238,21 +235,7 @@ public static final record BrokerLoadImpact( public static final record OptimizationProposal( @JsonProperty - Map> brokerImpact, - - @JsonProperty - OptimizationResult optimization - ) { - } - - public static final record ProgressStatus( - @JsonProperty - Integer estimatedTimeToCompletionInMinutes, - @JsonProperty - @Schema(minimum = "0", maximum = "100") - Integer completedByteMovementPercentage, - @JsonProperty - ExecutorState executorState + Map> brokerImpact ) { } @@ -322,10 +305,6 @@ static class Attributes extends KubeAttributes { @Schema(readOnly = true) OptimizationProposal optimizationProposal; - @JsonProperty - @Schema(readOnly = true) - ProgressStatus progress; - @JsonProperty @Schema(readOnly = true) List conditions; @@ -440,10 +419,6 @@ public void optimizationProposal(OptimizationProposal optimizationProposal) { attributes.optimizationProposal = optimizationProposal; } - public void progress(ProgressStatus progress) { - attributes.progress = progress; - } - public void conditions(List conditions) { attributes.conditions = conditions; } diff --git a/api/src/main/java/com/github/streamshub/console/api/service/KafkaRebalanceService.java b/api/src/main/java/com/github/streamshub/console/api/service/KafkaRebalanceService.java index 93fec50b9..c7c2a95c9 100644 --- a/api/src/main/java/com/github/streamshub/console/api/service/KafkaRebalanceService.java +++ b/api/src/main/java/com/github/streamshub/console/api/service/KafkaRebalanceService.java @@ -32,14 +32,12 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.informers.cache.Cache; -import io.streamshub.console.api.model.rebalance.cc.model.ExecutorState; import io.strimzi.api.ResourceAnnotations; import io.strimzi.api.ResourceLabels; import io.strimzi.api.kafka.model.kafka.Kafka; import io.strimzi.api.kafka.model.kafka.KafkaSpec; import io.strimzi.api.kafka.model.kafka.cruisecontrol.CruiseControlSpec; import io.strimzi.api.kafka.model.rebalance.KafkaRebalanceMode; -import io.strimzi.api.kafka.model.rebalance.KafkaRebalanceProgress; import io.strimzi.api.kafka.model.rebalance.KafkaRebalanceSpec; import io.strimzi.api.kafka.model.rebalance.KafkaRebalanceState; import io.strimzi.api.kafka.model.rebalance.KafkaRebalanceStatus; @@ -182,9 +180,11 @@ KafkaRebalance toKafkaRebalance(io.strimzi.api.kafka.model.rebalance.KafkaRebala rebalance.optimizationProposal(getOptimizationProposal(namespace, rebalanceStatus)); } - if (fields.contains(KafkaRebalance.Fields.PROGRESS)) { + /* FUTURE + if (fields.contains(KafkaRebalance.Fields.PROGRESS)) { NOSONAR rebalance.progress(getProgressStatus(namespace, rebalanceStatus)); } + */ return rebalance; } @@ -214,48 +214,7 @@ private KafkaRebalance.OptimizationProposal getOptimizationProposal(String names }) .orElse(null); - return new KafkaRebalance.OptimizationProposal(brokerLoadImpact, null); - }) - .orElse(null); - } - - private KafkaRebalance.ProgressStatus getProgressStatus(String namespace, Optional rebalanceStatus) { - return rebalanceStatus - .map(KafkaRebalanceStatus::getProgress) - .map(KafkaRebalanceProgress::getRebalanceProgressConfigMap) - .map(configMapName -> client.configMaps().inNamespace(namespace).withName(configMapName).get()) - .map(configMap -> { - var data = configMap.getData(); - var qname = "%s/%s".formatted(configMap.getMetadata().getNamespace(), configMap.getMetadata().getName()); - var executorState = Optional.ofNullable(data.get("executorState.json")) - .map(json -> { - try { - return mapper.readValue(json, ExecutorState.class); - } catch (Exception e) { - logger.warnf("Error reading 'executorState.json' from rebalance progress ConfigMap[%s]: %s", qname, e.getMessage()); - throw new RuntimeException(e); - } - }) - .orElse(null); - - return new KafkaRebalance.ProgressStatus( - getInteger(data, "estimatedTimeToCompletionInMinutes", qname), - getInteger(data, "completedByteMovementPercentage", qname), - executorState - ); - }) - .orElse(null); - } - - private Integer getInteger(Map data, String key, String mapQname) { - return Optional.ofNullable(data.get(key)) - .map(value -> { - try { - return Integer.valueOf(value); - } catch (Exception e) { - logger.warnf("Error parsing '%s' from rebalance progress ConfigMap[%s]: %s", key, mapQname, e.getMessage()); - return null; - } + return new KafkaRebalance.OptimizationProposal(brokerLoadImpact); }) .orElse(null); } From 14de36201d96abf605719585b1aaf0c4db295950 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Fri, 14 Aug 2026 15:13:44 -0400 Subject: [PATCH 14/25] Add test cases for broker impact retrieval from ConfigMap Signed-off-by: Michael Edgar --- .../console/api/KafkaRebalancesResource.java | 2 +- .../api/service/KafkaRebalanceService.java | 8 +- .../api/KafkaRebalancesResourceIT.java | 116 +++++++++++++++++- 3 files changed, 117 insertions(+), 9 deletions(-) diff --git a/api/src/main/java/com/github/streamshub/console/api/KafkaRebalancesResource.java b/api/src/main/java/com/github/streamshub/console/api/KafkaRebalancesResource.java index e2365a4ee..ed8798889 100644 --- a/api/src/main/java/com/github/streamshub/console/api/KafkaRebalancesResource.java +++ b/api/src/main/java/com/github/streamshub/console/api/KafkaRebalancesResource.java @@ -156,7 +156,7 @@ public Response listRebalances( @APIResponse(responseCode = "504", ref = "ServerTimeout") @Authorized @ResourcePrivilege(Privilege.GET) - public Response getRebalance( + public Response describeRebalance( @Parameter(description = "Cluster identifier") @PathParam("clusterId") String clusterId, diff --git a/api/src/main/java/com/github/streamshub/console/api/service/KafkaRebalanceService.java b/api/src/main/java/com/github/streamshub/console/api/service/KafkaRebalanceService.java index c7c2a95c9..a0cc6797e 100644 --- a/api/src/main/java/com/github/streamshub/console/api/service/KafkaRebalanceService.java +++ b/api/src/main/java/com/github/streamshub/console/api/service/KafkaRebalanceService.java @@ -180,12 +180,6 @@ KafkaRebalance toKafkaRebalance(io.strimzi.api.kafka.model.rebalance.KafkaRebala rebalance.optimizationProposal(getOptimizationProposal(namespace, rebalanceStatus)); } - /* FUTURE - if (fields.contains(KafkaRebalance.Fields.PROGRESS)) { NOSONAR - rebalance.progress(getProgressStatus(namespace, rebalanceStatus)); - } - */ - return rebalance; } @@ -209,7 +203,7 @@ private KafkaRebalance.OptimizationProposal getOptimizationProposal(String names logger.warnf(""" Error reading 'brokerLoad.json' from rebalance \ afterBeforeLoadConfigMap ConfigMap[%s]: %s""", qname, e.getMessage()); - throw new RuntimeException(e); + return null; } }) .orElse(null); diff --git a/api/src/test/java/com/github/streamshub/console/api/KafkaRebalancesResourceIT.java b/api/src/test/java/com/github/streamshub/console/api/KafkaRebalancesResourceIT.java index 6206f39fa..2b82f1f97 100644 --- a/api/src/test/java/com/github/streamshub/console/api/KafkaRebalancesResourceIT.java +++ b/api/src/test/java/com/github/streamshub/console/api/KafkaRebalancesResourceIT.java @@ -24,6 +24,8 @@ import com.github.streamshub.console.kafka.systemtest.TestPlainProfile; import com.github.streamshub.console.test.TestHelper; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.client.KubernetesClient; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; @@ -47,6 +49,7 @@ import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -79,6 +82,7 @@ static KafkaRebalance buildRebalance(int sequence, String clusterName, KafkaReba .endMetadata() .withNewSpec() .withMode(mode) + .withGoals("Goal1", "Goal2", "Goal3") .endSpec(); if (clusterName != null) { @@ -102,6 +106,8 @@ static KafkaRebalance buildRebalance(int sequence, String clusterName, KafkaReba .withLastTransitionTime(Instant.now().toString()) .endCondition() .addToOptimizationResult("intraBrokerDataToMoveMB", "0") + // May not be created always - only for some tests + .addToOptimizationResult("afterBeforeLoadConfigMap", "rebalance-" + sequence) .endStatus(); } @@ -119,13 +125,30 @@ void setup() { utils = new TestHelper(bootstrapServers, config); utils.resetSecurity(consoleConfig, false); + client.resources(ConfigMap.class).inAnyNamespace().delete(); client.resources(Kafka.class).inAnyNamespace().delete(); client.resources(KafkaRebalance.class).inAnyNamespace().delete(); utils.apply(client, new KafkaBuilder(utils.buildKafkaResource("test-kafka1", utils.getClusterId(), bootstrapServers)) .editSpec() .withNewCruiseControl() - // empty + .withNewBrokerCapacity() + .withCpu("100m") + .withInboundNetwork("100KiB/s") + .withOutboundNetwork("100KiB/s") + .addNewOverride() + .withBrokers(2) + .withCpu("200m") + .withInboundNetwork("200KiB/s") + .withOutboundNetwork("200KiB/s") + .endOverride() + .addNewOverride() + .withBrokers(3) + .withCpu("300m") + .withInboundNetwork("300KiB/s") + .withOutboundNetwork("300KiB/s") + .endOverride() + .endBrokerCapacity() .endCruiseControl() .endSpec() .build()); @@ -215,6 +238,97 @@ void testListRebalancesFullySorted(String sortField) { assertEquals(sortedValues, values); } + @Test + void testDescribeRebalanceWithBrokerImpact() { + var response = whenRequesting(req -> req + .param("filter[mode]", KafkaRebalanceMode.FULL.toValue()) + .param("filter[status]", KafkaRebalanceState.ProposalReady.name()) + .param("filter[name]", "like,rebalance-*") + .get("", clusterId1)) + .assertThat() + .statusCode(is(Status.OK.getStatusCode())) + .body("data.size()", equalTo(1)) + .extract(); + + String rebalanceId = response.jsonPath().getString("data[0].id"); + String rebalanceName = response.jsonPath().getString("data[0].attributes.name"); + + client.resource(new ConfigMapBuilder() + .withNewMetadata() + .withNamespace("default") + .withName(rebalanceName) + .endMetadata() + .addToData( + "brokerLoad.json", + """ + { + "0": { + "leaders": { "before": 1, "after": 2, "diff": 1 }, + "replicas": { "before": 2, "after": 1, "diff": -1 } + }, + "1": { + "leaders": { "before": 1, "after": 2, "diff": 1 }, + "replicas": { "before": 2, "after": 1, "diff": -1 } + }, + "2": { + "leaders": { "before": 1, "after": 2, "diff": 1 }, + "replicas": { "before": 2, "after": 1, "diff": -1 } + } + } + """ + ) + .build()) + .create(); + + whenRequesting(req -> req + .param( + "fields[" + com.github.streamshub.console.api.model.KafkaRebalance.API_TYPE + "]", + "brokerCapacity,optimizationProposal" + ) + .get("{rebalanceId}", clusterId1, rebalanceId)) + .assertThat() + .statusCode(is(Status.OK.getStatusCode())) + .body("data.attributes.optimizationProposal.brokerImpact", allOf(hasKey("0"), hasKey("1"), hasKey("2"))); + } + + @Test + void testDescribeRebalanceWithInvalidBrokerImpact() { + var response = whenRequesting(req -> req + .param("filter[mode]", KafkaRebalanceMode.FULL.toValue()) + .param("filter[status]", KafkaRebalanceState.ProposalReady.name()) + .param("filter[name]", "like,rebalance-*") + .get("", clusterId1)) + .assertThat() + .statusCode(is(Status.OK.getStatusCode())) + .body("data.size()", equalTo(1)) + .extract(); + + String rebalanceId = response.jsonPath().getString("data[0].id"); + String rebalanceName = response.jsonPath().getString("data[0].attributes.name"); + + client.resource(new ConfigMapBuilder() + .withNewMetadata() + .withNamespace("default") + .withName(rebalanceName) + .endMetadata() + .addToData( + "brokerLoad.json", + "{ INVALID JSON }" + ) + .build()) + .create(); + + whenRequesting(req -> req + .param( + "fields[" + com.github.streamshub.console.api.model.KafkaRebalance.API_TYPE + "]", + "brokerCapacity,optimizationProposal" + ) + .get("{rebalanceId}", clusterId1, rebalanceId)) + .assertThat() + .statusCode(is(Status.OK.getStatusCode())) + .body("data.attributes.optimizationProposal.brokerImpact", nullValue(String.class)); + } + @Test void testPatchRebalanceWithStatusProposalReady() { String rebalanceId = whenRequesting(req -> req From 340a0ac67b9f565dbadb3d337f924279a11cf902 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Mon, 17 Aug 2026 10:18:34 -0400 Subject: [PATCH 15/25] Fix node storage labels, hide controller links, fetch all chart nodes Signed-off-by: Michael Edgar --- .../components/kafka/nodes/NodesDataView.tsx | 3 +- .../nodes/charts/ChartNodeStorageUsage.tsx | 41 +++++++++++-------- api/src/main/webui/src/i18n/messages/en.json | 4 +- .../pages/kafka/nodes/NodesOverviewTab.tsx | 9 +++- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/api/src/main/webui/src/components/kafka/nodes/NodesDataView.tsx b/api/src/main/webui/src/components/kafka/nodes/NodesDataView.tsx index f90e3b108..35e3d49fa 100644 --- a/api/src/main/webui/src/components/kafka/nodes/NodesDataView.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/NodesDataView.tsx @@ -143,7 +143,8 @@ export function NodesDataView({ { cell: ( <> - {node.meta?.privileges?.includes('GET') === true ? ( + {node.attributes.roles?.includes('broker') + && node.meta?.privileges?.includes('GET') === true ? ( {node.id} diff --git a/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx index ce2bddf27..c3f637821 100644 --- a/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx @@ -28,30 +28,40 @@ export function ChartNodeStorageUsage({ nodes }: ChartNodeStorageUsageProps) { .sort((n1, n2) => parseInt(n2.id) - parseInt(n1.id)), [nodes]); - const usedData = useMemo(() => storageNodes.map((n) => ({ - name: t('nodes.charts.storageUsageSeriesUsed'), - x: `Node ${n.id}`, - y: n.attributes.storageUsed as number, - //label: `${t('nodes.charts.storageUsageSeriesUsed')}: ${formatBytes(n.attributes.storageUsed as number)}`, - })), [t, storageNodes]); + const usedData = useMemo(() => storageNodes.map((n) => { + const capacity = n.attributes.storageCapacity as number; + const used = n.attributes.storageUsed ?? 0; + const usedPct = (used / capacity) * 100; + + return { + name: t('nodes.charts.storageUsageSeriesUsed'), + x: `Node ${n.id}`, + y: n.attributes.storageUsed as number, + label: t('nodes.charts.storageUsageSeriesUsedLabel', { + "storageUsed": formatBytes(used), + "storageUsedPct": usedPct.toFixed(2), + "storageTotal": formatBytes(capacity), + }), + }; + }), [t, storageNodes]); const availableData = useMemo(() => storageNodes.map((n) => { - const available = (n.attributes.storageCapacity as number) - (n.attributes.storageUsed as number); + const capacity = n.attributes.storageCapacity as number; + const available = capacity - (n.attributes.storageUsed ?? 0); + const availablePct = (available / capacity) * 100; + return { name: t('nodes.charts.storageUsageSeriesAvailable'), x: `Node ${n.id}`, y: available, - //label: `${t('nodes.charts.storageUsageSeriesAvailable')}: ${formatBytes(available)}`, + label: t('nodes.charts.storageUsageSeriesAvailableLabel', { + "storageAvailable": formatBytes(available), + "storageAvailablePct": availablePct.toFixed(2), + "storageTotal": formatBytes(capacity), + }), }; }), [t, storageNodes]); - const labels = useMemo(() => storageNodes.map((n) => { - const capacity = n.attributes.storageCapacity as number; - const used = n.attributes.storageUsed as number; - const available = capacity - used; - return `Used ${formatBytes(used)} (${((used / capacity) * 100).toFixed(2)}%) of ${formatBytes(capacity)}\nAvailable ${formatBytes(available)} (${((available / capacity) * 100).toFixed(2)}%)`; - }), [storageNodes]); - const legendData = [ { name: t('nodes.charts.storageUsageSeriesUsed') }, { name: t('nodes.charts.storageUsageSeriesAvailable') }, @@ -113,7 +123,6 @@ export function ChartNodeStorageUsage({ nodes }: ChartNodeStorageUsageProps) { /> } > ({}); const nodeResult = useNodes(kafkaId, tableParams); + const nodeChartsResult = useNodes(kafkaId, { + fields: 'storageCapacity,storageUsed,broker', + page: { + sort: 'id', + size: 1000, + }, + }); const handleDataViewChange = useCallback((params: ResourceListParams) => { setTableParams(params); @@ -138,7 +145,7 @@ export function NodesOverviewTab() { - + From 4a94bf866c1ef0307be84eda82c2e04a8491c4af Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Mon, 17 Aug 2026 11:05:40 -0400 Subject: [PATCH 16/25] Add node storage chart sort options Signed-off-by: Michael Edgar --- .../nodes/charts/ChartNodeStorageUsage.tsx | 83 +++++++++++++++++-- api/src/main/webui/src/i18n/messages/en.json | 8 ++ 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx index c3f637821..8cae872c0 100644 --- a/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx @@ -8,12 +8,20 @@ import { ChartThemeColor, ChartTooltip, } from '@patternfly/react-charts/victory'; -import { Alert } from '@patternfly/react-core'; +import { + Alert, + MenuToggle, + Select, + SelectList, + SelectOption, +} from '@patternfly/react-core'; import { Node } from '@/api/types'; import { formatBytes } from '@/utils/format'; import { useChartWidth } from '@/components/kafka/overview/utils/useChartWidth'; import { getPadding } from '@/components/kafka/overview/utils/chartConsts'; -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; + +type StorageSortKey = 'nodeId' | 'usedAsc' | 'usedDesc' | 'availableAsc' | 'availableDesc'; interface ChartNodeStorageUsageProps { nodes: Node[]; @@ -22,11 +30,29 @@ interface ChartNodeStorageUsageProps { export function ChartNodeStorageUsage({ nodes }: ChartNodeStorageUsageProps) { const { t } = useTranslation(); const [containerRef, width] = useChartWidth(); + const [sortKey, setSortKey] = useState('nodeId'); + const [isSortOpen, setIsSortOpen] = useState(false); - const storageNodes = useMemo(() => nodes - .filter((n) => n.attributes.storageUsed != null && n.attributes.storageCapacity != null) - .sort((n1, n2) => parseInt(n2.id) - parseInt(n1.id)), - [nodes]); + const storageNodes = useMemo(() => { + const filtered = nodes.filter( + (n) => n.attributes.storageUsed != null && n.attributes.storageCapacity != null, + ); + // Victory renders horizontal bar charts bottom-to-top, so the array must be + // in the opposite order to what the user sees visually (top-to-bottom). + return filtered.sort((n1, n2) => { + const used1 = n1.attributes.storageUsed as number; + const used2 = n2.attributes.storageUsed as number; + const avail1 = (n1.attributes.storageCapacity as number) - used1; + const avail2 = (n2.attributes.storageCapacity as number) - used2; + switch (sortKey) { + case 'usedAsc': return used2 - used1; + case 'usedDesc': return used1 - used2; + case 'availableAsc': return avail2 - avail1; + case 'availableDesc':return avail1 - avail2; + default: return parseInt(n2.id) - parseInt(n1.id); + } + }); + }, [nodes, sortKey]); const usedData = useMemo(() => storageNodes.map((n) => { const capacity = n.attributes.storageCapacity as number; @@ -99,7 +125,47 @@ export function ChartNodeStorageUsage({ nodes }: ChartNodeStorageUsageProps) { } return ( -
+ <> +
+ {t('nodes.charts.sortBy.label')} + +
+
-
+
+ ); } diff --git a/api/src/main/webui/src/i18n/messages/en.json b/api/src/main/webui/src/i18n/messages/en.json index d3f43ab9a..c15d02d13 100644 --- a/api/src/main/webui/src/i18n/messages/en.json +++ b/api/src/main/webui/src/i18n/messages/en.json @@ -526,6 +526,14 @@ "storageUsageSeriesUsedLabel": "Used {{storageUsed}} ({{storageUsedPct}})% of {{storageTotal}}", "storageUsageSeriesAvailable": "Available", "storageUsageSeriesAvailableLabel": "Available {{storageAvailable}} ({{storageAvailablePct}})% of {{storageTotal}}", + "sortBy": { + "label": "Order", + "nodeId": "Node ID", + "usedAsc": "Storage used: low to high", + "usedDesc": "Storage used: high to low", + "availableAsc": "Storage available: low to high", + "availableDesc": "Storage available: high to low" + }, "partitionDistribution": "Partition distribution", "partitionDistributionSubtitle": "Balance of partition leaders and replicas across brokers", "partitionDistributionTooltip": "Total replicas and leader partitions per broker node.", From 1c716db48b2f4ebbb93df9da9a492b84a6371b08 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Mon, 17 Aug 2026 14:05:10 -0400 Subject: [PATCH 17/25] Remove remnants from CC model generation Signed-off-by: Michael Edgar --- .gitignore | 1 - api/pom.xml | 25 ------------------- api/src/main/resources/application.properties | 7 ------ pom.xml | 11 -------- 4 files changed, 44 deletions(-) diff --git a/.gitignore b/.gitignore index cf63cfcdb..bd8ee05f0 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,3 @@ release.properties systemtests/screenshots/ systemtests/config.yaml systemtests/tracing/ -/.playwright-mcp/ diff --git a/api/pom.xml b/api/pom.xml index fa5673f20..2779561b2 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -306,7 +306,6 @@ build - generate-code @@ -320,30 +319,6 @@ org.apache.maven.plugins maven-dependency-plugin - - unpack - initialize - - unpack - - - - - - com.linkedin.cruisecontrol - cruise-control - 2.5.146 - jar - true - ${project.build.directory}/schema/cruise-control - yaml/** - - - - analyze diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index e77d8948a..52d60befc 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -62,13 +62,6 @@ quarkus.arc.unremovable-types=com.github.streamshub.console.api.** quarkus.arc.exclude-types=io.apicurio.registry.rest.JacksonDateTimeCustomizer quarkus.arc.ignored-split-packages=io.apicurio.registry.content.*,io.apicurio.registry.rules.*, -# These properties are used to generate Java classes for the Cruise Control -# rebalance status information published by Strimzi for KafkaRebalance resources. -quarkus.openapi-generator.codegen.input-base-dir=target/schema/cruise-control/yaml -quarkus.openapi-generator.codegen.include=base.yaml -quarkus.openapi-generator.codegen.spec.base_yaml.base-package=io.streamshub.console.api.model.rebalance.cc -quarkus.openapi-generator.codegen.spec.base_yaml.generate-apis=false - quarkus.index-dependency.kafka-clients.group-id=org.apache.kafka quarkus.index-dependency.kafka-clients.artifact-id=kafka-clients quarkus.index-dependency.strimzi-api.group-id=io.strimzi diff --git a/pom.xml b/pom.xml index a0c4312e9..807867ae2 100644 --- a/pom.xml +++ b/pom.xml @@ -87,17 +87,6 @@ systemtests - - - linkedin-artifactory - LinkedIn JFrog Artifactory - https://linkedin.jfrog.io/artifactory/release - - false - - - - From d5b54253bc8d0d849db3209d17beea1d07102d95 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Mon, 17 Aug 2026 14:42:22 -0400 Subject: [PATCH 18/25] Add secondary x-axis at top of node storage and partition dist. graphs Assisted-by: IBM Bob Signed-off-by: Michael Edgar --- .../kafka/nodes/charts/ChartNodeStorageUsage.tsx | 9 ++++++++- .../kafka/nodes/charts/ChartPartitionDistribution.tsx | 7 ++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx index 8cae872c0..014906082 100644 --- a/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx @@ -111,7 +111,7 @@ export function ChartNodeStorageUsage({ nodes }: ChartNodeStorageUsageProps) { // Dynamically calculate the SVG canvas size based on data density const calculatedChartHeight = usedData.length * (barWidth + innerPadding) + 100; const legendRows = 1; - const padding = { ...getPadding(legendRows), left: 70 }; + const padding = { ...getPadding(legendRows), left: 70, top: 40 }; if (storageNodes.length === 0) { return ( @@ -187,6 +187,13 @@ export function ChartNodeStorageUsage({ nodes }: ChartNodeStorageUsageProps) { tickFormat={(d: number) => formatBytes(d)} horizontal /> + formatBytes(d)} + horizontal + /> } diff --git a/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx b/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx index dd27cfbe6..c0c0d1881 100644 --- a/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx @@ -62,7 +62,7 @@ export function ChartPartitionDistribution({ nodes }: ChartPartitionDistribution // Dynamically calculate the SVG canvas size based on data density const calculatedChartHeight = leadersData.length * (barWidth + innerPadding) + 100; const legendRows = 1; - const padding = { ...getPadding(legendRows), left: 70 }; + const padding = { ...getPadding(legendRows), left: 70, top: 40 }; if (brokerNodes.length === 0) { return ( @@ -96,6 +96,11 @@ export function ChartPartitionDistribution({ nodes }: ChartPartitionDistribution showGrid horizontal /> + Date: Mon, 17 Aug 2026 14:56:07 -0400 Subject: [PATCH 19/25] Remove obsolete node/rebalance components Assisted-by: IBM Bob Signed-off-by: Michael Edgar --- .../src/components/kafka/nodes/NodesTable.tsx | 289 ------------------ .../kafka/nodes/RebalancesCountCard.tsx | 101 ------ 2 files changed, 390 deletions(-) delete mode 100644 api/src/main/webui/src/components/kafka/nodes/NodesTable.tsx delete mode 100644 api/src/main/webui/src/components/kafka/nodes/RebalancesCountCard.tsx diff --git a/api/src/main/webui/src/components/kafka/nodes/NodesTable.tsx b/api/src/main/webui/src/components/kafka/nodes/NodesTable.tsx deleted file mode 100644 index 6a57b6b99..000000000 --- a/api/src/main/webui/src/components/kafka/nodes/NodesTable.tsx +++ /dev/null @@ -1,289 +0,0 @@ -/** - * Nodes Table Component - * Displays Kafka cluster nodes with sorting and expandable rows - */ - -import { Fragment, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - Table, - Thead, - Tr, - Th, - Tbody, - Td, - ExpandableRowContent, - ThProps, -} from '@patternfly/react-table'; -import { - EmptyState, - EmptyStateBody, - Title, - Flex, - FlexItem, - Content, - ClipboardCopy, - Tooltip, - Button, - Label, -} from '@patternfly/react-core'; -import { HelpIcon } from '@patternfly/react-icons'; -import { ChartDonutUtilization } from '@patternfly/react-charts/victory'; -import type { Node } from '@/api/types'; -import type { ChartDatum } from '@/components/kafka/overview/utils/types'; -import { - useRoleLabels, - useBrokerStatusLabels, - useControllerStatusLabels, -} from './NodeStatusLabel'; -import { formatNumber } from '@/utils/format'; -import { Link } from 'react-router'; - -interface NodesTableProps { - kafkaId: string; - nodes: Node[]; - isLoading?: boolean; - hasActiveFilters?: boolean; - onClearAllFilters?: () => void; - onSort?: (column: string) => void; - sortBy?: string; - sortDirection?: 'asc' | 'desc'; -} - -export function NodesTable({ - kafkaId, - nodes, - isLoading, - hasActiveFilters = false, - onClearAllFilters, - onSort, - sortBy, - sortDirection = 'asc', -}: NodesTableProps) { - const { t } = useTranslation(); - const roleLabels = useRoleLabels(); - const brokerStatusLabels = useBrokerStatusLabels(); - const controllerStatusLabels = useControllerStatusLabels(); - - // Expandable row state - const [expandedRows, setExpandedRows] = useState>(new Set()); - - // Toggle row expansion - const toggleRowExpansion = (nodeId: string) => { - setExpandedRows((prev) => { - const newSet = new Set(prev); - if (newSet.has(nodeId)) { - newSet.delete(nodeId); - } else { - newSet.add(nodeId); - } - return newSet; - }); - }; - - // Get sort params for column - const getSortParams = (columnKey: string): ThProps['sort'] | undefined => { - if (!onSort) return undefined; - - return { - sortBy: { - index: sortBy === columnKey ? 0 : undefined, - direction: sortDirection, - }, - onSort: () => onSort(columnKey), - columnIndex: 0, - }; - }; - - // Format bytes - const formatBytes = (bytes?: number): string => { - if (bytes === undefined || bytes === null) return 'N/A'; - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; - }; - - // Empty state - if (nodes.length === 0 && !isLoading) { - return ( - - - {hasActiveFilters ? t('topics.filter.clearAllFilters') : t('nodes.noNodes')} - - - {hasActiveFilters - ? 'No nodes match the current filter criteria.' - : t('nodes.noNodesDescription')} - - {hasActiveFilters && onClearAllFilters && ( - - )} - - ); - } - - return ( - - - - - - - - - - - - - {nodes.map((node, rowIndex) => { - const isExpanded = expandedRows.has(node.id); - const diskCapacity = node.attributes.storageCapacity; - const diskUsage = node.attributes.storageUsed; - const usedCapacity = - diskUsage !== undefined && diskUsage !== null && - diskCapacity !== undefined && diskCapacity !== null - ? diskUsage / diskCapacity - : undefined; - - return ( - - - - - - - - - - {isExpanded && ( - - - - )} - - ); - })} - -
- {t('nodes.nodeId')}{t('nodes.roles')}{t('nodes.status')} - {t('nodes.replicas')}{' '} - - - - - {t('nodes.rack')}{' '} - - - - {t('nodes.nodePool')}
toggleRowExpansion(node.id), - }} - /> - - {node.meta?.privileges?.includes('GET') === true ? ( - - {node.id} - - ) : ( - node.id - )} - {node.attributes.metadataState?.status === 'leader' && ( - - )} - - {node.attributes.roles?.map((role) => ( -
{roleLabels[role].label}
- ))} -
-
- {node.attributes.broker && brokerStatusLabels[node.attributes.broker.status]} -
-
- {node.attributes.controller && - controllerStatusLabels[node.attributes.controller.status]} -
-
- {typeof node.attributes.broker?.leaderCount === 'number' && - typeof node.attributes.broker?.replicaCount === 'number' - ? formatNumber( - node.attributes.broker.leaderCount + node.attributes.broker.replicaCount - ) - : '-'} - {node.attributes.rack || 'n/a'}{node.attributes.nodePool || 'n/a'}
- - - - - - {t('nodes.hostName')} - - - - {node.attributes.host || 'n/a'} - - - - - - - - {t('nodes.diskUsage')} - - -
- {usedCapacity !== undefined && ( -
- - datum.x ? `${datum.x}: ${datum.y.toFixed(1)}%` : null - } - legendData={[ - { - name: `Used capacity: ${formatBytes(diskUsage!)}`, - }, - { - name: `Available: ${formatBytes(diskCapacity! - diskUsage!)}`, - }, - ]} - legendOrientation="vertical" - legendPosition="bottom" - padding={{ - bottom: 75, - left: 20, - right: 20, - top: 20, - }} - title={`${(usedCapacity * 100).toFixed(1)}%`} - subTitle={`of ${formatBytes(diskCapacity!)}`} - thresholds={[{ value: 60 }, { value: 90 }]} - height={300} - width={230} - /> -
- )} -
-
- - - - {t('nodes.kafkaVersion')} - - -
{node.attributes.kafkaVersion ?? 'Unknown'}
-
-
-
-
- ); -} diff --git a/api/src/main/webui/src/components/kafka/nodes/RebalancesCountCard.tsx b/api/src/main/webui/src/components/kafka/nodes/RebalancesCountCard.tsx deleted file mode 100644 index 3c7a3415c..000000000 --- a/api/src/main/webui/src/components/kafka/nodes/RebalancesCountCard.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Rebalances Count Card Component - * Displays counts for different rebalance statuses - */ - -import { useTranslation } from 'react-i18next'; -import { - Card, - CardBody, - DescriptionList, - DescriptionListGroup, - DescriptionListTerm, - DescriptionListDescription, - Flex, - FlexItem, - Divider, - Bullseye, -} from '@patternfly/react-core'; - -interface RebalancesCountCardProps { - totalRebalances: number; - proposalReady: number; - rebalancing: number; - ready: number; - stopped: number; -} - -export function RebalancesCountCard({ - totalRebalances, - proposalReady, - rebalancing, - ready, - stopped, -}: RebalancesCountCardProps) { - const { t } = useTranslation(); - - return ( - - - - - - - {t('rebalancing.totalRebalances')} - - {totalRebalances} - - - - - - - - {t('rebalancing.statuses.proposalReady.label')} - - {proposalReady} - - - - - - - - - {t('rebalancing.statuses.rebalancing.label')} - - {rebalancing} - - - - - - - - - {t('rebalancing.statuses.ready.label')} - - {ready} - - - - - - - - - - {t('rebalancing.statuses.stopped.label')} - - - {stopped} - - - - - - - - - ); -} \ No newline at end of file From 596837a03c44734b0868b171f55e1053ff9b09d7 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Tue, 18 Aug 2026 08:45:10 -0400 Subject: [PATCH 20/25] Add Storybook and stories for node charts Signed-off-by: Michael Edgar --- api/src/main/webui/.gitignore | 5 +- api/src/main/webui/.storybook/main.ts | 19 + api/src/main/webui/.storybook/preview.tsx | 46 + api/src/main/webui/package-lock.json | 3352 +++++++++++++++-- api/src/main/webui/package.json | 12 +- .../charts/ChartNodeStorageUsage.stories.tsx | 78 + .../nodes/charts/ChartNodeStorageUsage.tsx | 12 +- .../ChartPartitionDistribution.stories.tsx | 77 + .../charts/ChartPartitionDistribution.tsx | 12 +- 9 files changed, 3224 insertions(+), 389 deletions(-) create mode 100644 api/src/main/webui/.storybook/main.ts create mode 100644 api/src/main/webui/.storybook/preview.tsx create mode 100644 api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.stories.tsx create mode 100644 api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.stories.tsx diff --git a/api/src/main/webui/.gitignore b/api/src/main/webui/.gitignore index 54f07af58..03baf322f 100644 --- a/api/src/main/webui/.gitignore +++ b/api/src/main/webui/.gitignore @@ -21,4 +21,7 @@ dist-ssr *.ntvs* *.njsproj *.sln -*.sw? \ No newline at end of file +*.sw? + +# Storybook output +storybook-static/ diff --git a/api/src/main/webui/.storybook/main.ts b/api/src/main/webui/.storybook/main.ts new file mode 100644 index 000000000..22be829a4 --- /dev/null +++ b/api/src/main/webui/.storybook/main.ts @@ -0,0 +1,19 @@ +import type { StorybookConfig } from '@storybook/react-vite'; + +const config: StorybookConfig = { + stories: [ + { directory: '../src', files: '**/*.stories.@(ts|tsx)' }, + ], + + addons: [ + '@storybook/addon-links', + '@storybook/addon-docs', + ], + + framework: { + name: '@storybook/react-vite', + options: {}, + }, +}; + +export default config; diff --git a/api/src/main/webui/.storybook/preview.tsx b/api/src/main/webui/.storybook/preview.tsx new file mode 100644 index 000000000..4ab580637 --- /dev/null +++ b/api/src/main/webui/.storybook/preview.tsx @@ -0,0 +1,46 @@ +import type { Preview } from '@storybook/react'; +import '../src/i18n/config'; +import '@patternfly/patternfly/patternfly.css'; +import '@patternfly/patternfly/patternfly-charts.css'; +import '@patternfly/patternfly/patternfly-addons.css'; + +const preview: Preview = { + globalTypes: { + theme: { + name: 'Theme', + defaultValue: 'light', + toolbar: { + icon: 'paintbrush', + items: [ + { value: 'light', title: 'Light' }, + { value: 'dark', title: 'Dark' }, + ], + showName: true, + dynamicTitle: true, + }, + }, + }, + + parameters: { + layout: 'fullscreen', + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, + + decorators: [ + (Story, context) => { + const theme = context.globals.theme; + document.documentElement.classList.remove('pf-v6-theme-dark', 'pf-v6-theme-light'); + document.documentElement.classList.add( + theme === 'dark' ? 'pf-v6-theme-dark' : 'pf-v6-theme-light', + ); + return ; + }, + ], +}; + +export default preview; diff --git a/api/src/main/webui/package-lock.json b/api/src/main/webui/package-lock.json index b8ba38646..921bd5262 100644 --- a/api/src/main/webui/package-lock.json +++ b/api/src/main/webui/package-lock.json @@ -33,6 +33,10 @@ }, "devDependencies": { "@eslint/js": "10.0.1", + "@storybook/addon-docs": "^10.5.8", + "@storybook/addon-links": "^10.5.8", + "@storybook/react": "^10.5.8", + "@storybook/react-vite": "^10.5.8", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", "@typescript-eslint/eslint-plugin": "^8.67.0", @@ -41,11 +45,20 @@ "eslint": "^10.8.1", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.4", + "eslint-plugin-storybook": "^10.5.8", + "storybook": "^10.5.8", "typescript": "^6.0.3", "typescript-eslint": "^8.67.0", "vite": "^8.2.2" } }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -382,6 +395,40 @@ "react": ">=16.8.0" } }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emotion/is-prop-valid": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.7.3.tgz", @@ -397,233 +444,1382 @@ "integrity": "sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==", "license": "MIT" }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=18" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18" } }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", - "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", - "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@jridgewell/remapping": { + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.7.0.tgz", + "integrity": "sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^13.0.1", + "react-docgen-typescript": "^2.2.2" + }, + "peerDependencies": { + "typescript": ">= 4.3.x", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.127.0.tgz", + "integrity": "sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.127.0.tgz", + "integrity": "sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.127.0.tgz", + "integrity": "sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.127.0.tgz", + "integrity": "sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.127.0.tgz", + "integrity": "sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.127.0.tgz", + "integrity": "sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.127.0.tgz", + "integrity": "sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.127.0.tgz", + "integrity": "sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.127.0.tgz", + "integrity": "sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.127.0.tgz", + "integrity": "sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.127.0.tgz", + "integrity": "sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.127.0.tgz", + "integrity": "sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.127.0.tgz", + "integrity": "sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.127.0.tgz", + "integrity": "sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.127.0.tgz", + "integrity": "sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.127.0.tgz", + "integrity": "sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.127.0.tgz", + "integrity": "sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.127.0.tgz", + "integrity": "sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.127.0.tgz", + "integrity": "sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.127.0.tgz", + "integrity": "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.24.2.tgz", + "integrity": "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.24.2.tgz", + "integrity": "sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.24.2.tgz", + "integrity": "sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.24.2.tgz", + "integrity": "sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.24.2.tgz", + "integrity": "sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.24.2.tgz", + "integrity": "sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.24.2.tgz", + "integrity": "sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.24.2.tgz", + "integrity": "sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.24.2.tgz", + "integrity": "sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.24.2.tgz", + "integrity": "sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.24.2.tgz", + "integrity": "sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.24.2.tgz", + "integrity": "sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.24.2.tgz", + "integrity": "sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.24.2.tgz", + "integrity": "sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.24.2.tgz", + "integrity": "sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.24.2.tgz", + "integrity": "sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.24.2.tgz", + "integrity": "sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==", + "cpu": [ + "wasm32" + ], "dev": true, "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.2", + "@emnapi/runtime": "1.11.2", + "@napi-rs/wasm-runtime": "^1.1.6" + }, "engines": { - "node": ">=6.0.0" + "node": ">=14.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "tslib": "^2.4.0" } }, - "node_modules/@oxc-project/types": { - "version": "0.146.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", - "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.24.2.tgz", + "integrity": "sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.24.2.tgz", + "integrity": "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@patternfly/patternfly": { "version": "6.6.1", "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-6.6.1.tgz", @@ -837,324 +2033,687 @@ "integrity": "sha512-t7dDEJMnO3QkdIKyjbyKJznVYki9ONZG27Cn/9RXwaM0lDGCrgQEJXy2XoVUCdYhIdXZu+TGSEqXeNFYUyZECg==", "license": "MIT" }, - "node_modules/@patternfly/react-user-feedback": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@patternfly/react-user-feedback/-/react-user-feedback-6.4.0.tgz", - "integrity": "sha512-D8QIwOezspzLwhKygUURy0Gy4Ip2f4/GaMAn8BgwGwOipO7jgJUzyf8nfFmVy79kXjCHJdbFPu+37hzY6yYb6A==", + "node_modules/@patternfly/react-user-feedback": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-user-feedback/-/react-user-feedback-6.4.0.tgz", + "integrity": "sha512-D8QIwOezspzLwhKygUURy0Gy4Ip2f4/GaMAn8BgwGwOipO7jgJUzyf8nfFmVy79kXjCHJdbFPu+37hzY6yYb6A==", + "license": "MIT", + "dependencies": { + "@patternfly/react-core": "^6.5.1", + "@patternfly/react-icons": "^6.5.1" + }, + "peerDependencies": { + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", + "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", + "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@patternfly/react-core": "^6.5.1", - "@patternfly/react-icons": "^6.5.1" - }, - "peerDependencies": { - "react": "^17 || ^18 || ^19", - "react-dom": "^17 || ^18 || ^19" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-android-arm-eabi": { + "node_modules/@rolldown/binding-linux-s390x-gnu": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", - "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", "cpu": [ - "arm" + "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" + "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-android-arm64": { + "node_modules/@rolldown/binding-linux-x64-gnu": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", - "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" + "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-darwin-arm64": { + "node_modules/@rolldown/binding-linux-x64-musl": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", - "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-darwin-x64": { + "node_modules/@rolldown/binding-openharmony-arm64": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", - "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" + "openharmony" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-freebsd-x64": { + "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", - "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" + "win32" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "node_modules/@rolldown/binding-win32-x64-msvc": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", - "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", "cpu": [ - "arm" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", - "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", - "cpu": [ - "arm64" - ], + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@storybook/addon-docs": { + "version": "10.5.8", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.8.tgz", + "integrity": "sha512-NlHiMKW/UvW/uL8HXFDCEVwoH3qZeGYZ/qlWax4d7H471b/T54MBq2KcB4ZrdA785FfIH3numAJdBb5jwn00Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdx-js/react": "^3.0.0", + "@storybook/csf-plugin": "10.5.8", + "@storybook/icons": "^2.0.2", + "@storybook/react-dom-shim": "10.5.8", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "storybook": "^10.5.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@storybook/addon-links": { + "version": "10.5.8", + "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.5.8.tgz", + "integrity": "sha512-mpWw4alBJVGqgVh897LZ2keN/xnMHcH93wKJG+oGg4+cdEUA+06hCs5T4k+AS5Aa+EZ6LvdOoi2VPHssyQlCCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "storybook": "^10.5.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@storybook/builder-vite": { + "version": "10.5.8", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.5.8.tgz", + "integrity": "sha512-UeRnn7yT55WmBlHNOQzLrvN7vsHEvVgIukhKDO+4cMbGXN87wZkbxhx6NstpuXRH8OxGqwKS0SZNVp+SC1ftLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/csf-plugin": "10.5.8", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^10.5.8", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@storybook/csf-plugin": { + "version": "10.5.8", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.8.tgz", + "integrity": "sha512-/FHiMyOWWEXfwK/lM0WxmkP9GLzbSJJuzGtfeuNWSOVDnvAMbjavitxfHb5wSbWKIQo0XYC1EJ2Y7x91XNYP4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "unplugin": "^2.3.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "esbuild": "*", + "rollup": "*", + "storybook": "^10.5.8", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "esbuild": { + "optional": true + }, + "rollup": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@storybook/global": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", + "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@storybook/icons": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-2.1.0.tgz", + "integrity": "sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@storybook/react": { + "version": "10.5.8", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.8.tgz", + "integrity": "sha512-6qqkmqX6imtL+0Z9Uan2tIfYivOI0FiVmWr0zpqqQR15AkJ18JfNcNTQoyjeAlCO0Kei56SWqnu2qLq52TYplg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/react-dom-shim": "10.5.8", + "react-docgen": "^8.0.2", + "react-docgen-typescript": "^2.2.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "storybook": "^10.5.8", + "typescript": ">= 4.9.x" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/react-dom-shim": { + "version": "10.5.8", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.8.tgz", + "integrity": "sha512-N8D13/Xny+V3kfe1KBgsAHS0nKWXLLdgOOXS9poKdYzVwVCN+CGEGBxWX0zMMtdCptqa6/57em9coPlZMoO+bg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "storybook": "^10.5.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@storybook/react-vite": { + "version": "10.5.8", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.5.8.tgz", + "integrity": "sha512-ioMJGi4YzueGsJBlYio+2+UhfCFB9QV5Bs1lOilkek+a4BZgKJl0D1mVSJl6k96stQBPZmLgI9/l0hLVcUL6Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0", + "@rollup/pluginutils": "^5.0.2", + "@storybook/builder-vite": "10.5.8", + "@storybook/react": "10.5.8", + "empathic": "^2.0.0", + "magic-string": "^0.30.0", + "react-docgen": "^8.0.2", + "resolve": "^1.22.8", + "tsconfig-paths": "^4.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "storybook": "^10.5.8", + "typescript": ">= 4.9.x", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", + "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz", + "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", - "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", - "cpu": [ - "arm64" - ], + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", "dev": true, - "libc": [ - "musl" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" } }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", - "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", - "cpu": [ - "ppc64" - ], + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "license": "MIT" }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", - "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", - "cpu": [ - "s390x" - ], + "node_modules/@testing-library/user-event": { + "version": "14.6.4", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.4.tgz", + "integrity": "sha512-QCGwP6QrjypBLwyj5cuyfVamkaIEy/XGY+1VDehbtbQqOggYmTFpFOdWR5mPz14vX8vXLMVjDHlRNBcClyO9ew==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", - "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", - "cpu": [ - "x64" - ], + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", - "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", - "cpu": [ - "x64" - ], + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "license": "MIT" }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", - "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", - "cpu": [ - "arm64" - ], + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", - "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", - "cpu": [ - "arm64" - ], + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "@babel/types": "^7.0.0" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", - "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", - "cpu": [ - "x64" - ], + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, - "license": "MIT" - }, - "node_modules/@tanstack/query-core": { - "version": "5.101.4", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", - "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" + "dependencies": { + "@babel/types": "^7.28.2" } }, - "node_modules/@tanstack/react-query": { - "version": "5.101.4", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz", - "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.101.4" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, "node_modules/@types/d3-array": { @@ -1220,6 +2779,20 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/doctrine": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", + "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -1241,6 +2814,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdx": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz", + "integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", @@ -1261,6 +2841,13 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/resolve": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", + "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.67.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", @@ -1524,6 +3111,71 @@ } } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@webcontainer/env": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@webcontainer/env/-/env-1.1.1.tgz", + "integrity": "sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==", + "dev": true, + "license": "MIT" + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -1537,31 +3189,87 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "engines": { + "node": ">=12" } }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "tslib": "^2.0.1" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=4" } }, "node_modules/attr-accept": { @@ -1643,6 +3351,22 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001799", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", @@ -1664,6 +3388,33 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -1728,6 +3479,13 @@ "is-in-browser": "^1.0.2" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1898,6 +3656,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1905,6 +3673,49 @@ "dev": true, "license": "MIT" }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delaunator": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-4.0.1.tgz", @@ -1920,6 +3731,16 @@ "delaunator": "^4.0.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1930,6 +3751,26 @@ "node": ">=8" } }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.378", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.378.tgz", @@ -1937,6 +3778,68 @@ "dev": true, "license": "ISC" }, + "node_modules/empathic": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", + "integrity": "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2049,6 +3952,21 @@ "eslint": "^9 || ^10" } }, + "node_modules/eslint-plugin-storybook": { + "version": "10.5.8", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.8.tgz", + "integrity": "sha512-bf9W5nZyWdIaCUZf4aEZnEeD1mn+csNYX8dYUQjAo6L7/DkSLtr65R4zFZ1xeS4m6dOXO6UtUySesCSw4e8w1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "^8.60.0", + "@typescript-eslint/utils": "^8.60.0" + }, + "peerDependencies": { + "eslint": ">=8", + "storybook": "^10.5.8" + } + }, "node_modules/eslint-scope": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", @@ -2135,6 +4053,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -2171,6 +4103,13 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2307,6 +4246,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2317,6 +4266,24 @@ "node": ">=6.9.0" } }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2330,6 +4297,19 @@ "node": ">=10.13.0" } }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -2419,6 +4399,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -2428,6 +4418,38 @@ "node": ">=12" } }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2457,6 +4479,41 @@ "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==", "license": "MIT" }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2523,6 +4580,13 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, "node_modules/jss": { "version": "10.10.0", "resolved": "https://registry.npmjs.org/jss/-/jss-10.10.0.tgz", @@ -2856,9 +4920,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2880,9 +4941,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2904,9 +4962,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -2928,9 +4983,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3020,6 +5072,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3030,6 +5089,36 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -3046,6 +5135,26 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3098,6 +5207,25 @@ "node": ">=0.10.0" } }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3116,6 +5244,85 @@ "node": ">= 0.8.0" } }, + "node_modules/oxc-parser": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.127.0.tgz", + "integrity": "sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.127.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.127.0", + "@oxc-parser/binding-android-arm64": "0.127.0", + "@oxc-parser/binding-darwin-arm64": "0.127.0", + "@oxc-parser/binding-darwin-x64": "0.127.0", + "@oxc-parser/binding-freebsd-x64": "0.127.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.127.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.127.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.127.0", + "@oxc-parser/binding-linux-arm64-musl": "0.127.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.127.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.127.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.127.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.127.0", + "@oxc-parser/binding-linux-x64-gnu": "0.127.0", + "@oxc-parser/binding-linux-x64-musl": "0.127.0", + "@oxc-parser/binding-openharmony-arm64": "0.127.0", + "@oxc-parser/binding-wasm32-wasi": "0.127.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.127.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.127.0", + "@oxc-parser/binding-win32-x64-msvc": "0.127.0" + } + }, + "node_modules/oxc-parser/node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/oxc-resolver": { + "version": "11.24.2", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.24.2.tgz", + "integrity": "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.24.2", + "@oxc-resolver/binding-android-arm64": "11.24.2", + "@oxc-resolver/binding-darwin-arm64": "11.24.2", + "@oxc-resolver/binding-darwin-x64": "11.24.2", + "@oxc-resolver/binding-freebsd-x64": "11.24.2", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", + "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", + "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", + "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", + "@oxc-resolver/binding-linux-x64-musl": "11.24.2", + "@oxc-resolver/binding-openharmony-arm64": "11.24.2", + "@oxc-resolver/binding-wasm32-wasi": "11.24.2", + "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", + "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3168,6 +5375,50 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3227,6 +5478,28 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -3263,6 +5536,38 @@ "integrity": "sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg==", "license": "MIT" }, + "node_modules/react-docgen": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.3.tgz", + "integrity": "sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@types/babel__core": "^7.20.5", + "@types/babel__traverse": "^7.20.7", + "@types/doctrine": "^0.0.9", + "@types/resolve": "^1.20.2", + "doctrine": "^3.0.0", + "resolve": "^1.22.1", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": "^20.9.0 || >=22" + } + }, + "node_modules/react-docgen-typescript": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", + "integrity": "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 4.3.x" + } + }, "node_modules/react-dom": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", @@ -3386,12 +5691,78 @@ } } }, + "node_modules/recast": { + "version": "0.23.21", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.21.tgz", + "integrity": "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redent/node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", "license": "MIT" }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/rolldown": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", @@ -3426,6 +5797,19 @@ "@rolldown/binding-win32-x64-msvc": "1.2.5" } }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -3474,6 +5858,16 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3484,6 +5878,91 @@ "node": ">=0.10.0" } }, + "node_modules/storybook": { + "version": "10.5.8", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.8.tgz", + "integrity": "sha512-rR4oFMSiWBSqI0lvsJPtcQUPj8+hzj3TkLu+Mw61Wo6YxPSb5FsLSHai0jZnuaIdKIlmu25KCfwlSQl4e1uvnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/icons": "^2.0.2", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/user-event": "^14.6.1", + "@vitest/expect": "3.2.4", + "@vitest/spy": "3.2.4", + "@webcontainer/env": "^1.1.1", + "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", + "jsonc-parser": "^3.3.1", + "open": "^10.2.0", + "oxc-parser": "^0.127.0", + "oxc-resolver": "^11.19.1", + "recast": "^0.23.5", + "semver": "^7.7.3", + "use-sync-external-store": "^1.5.0", + "ws": "^8.21.1" + }, + "bin": { + "storybook": "dist/bin/dispatcher.js" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "prettier": "^2 || ^3", + "vite-plus": "^0.1.15 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "prettier": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-indent": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", + "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", @@ -3517,6 +5996,13 @@ "react": ">=16.3" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, + "license": "MIT" + }, "node_modules/tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -3540,6 +6026,26 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -3553,6 +6059,31 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -3610,6 +6141,22 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -4264,6 +6811,13 @@ } } }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4290,6 +6844,44 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/api/src/main/webui/package.json b/api/src/main/webui/package.json index c3034a066..4ec319983 100644 --- a/api/src/main/webui/package.json +++ b/api/src/main/webui/package.json @@ -7,7 +7,9 @@ "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", - "lint": "eslint . --ext js,ts,tsx --report-unused-disable-directives --max-warnings 0" + "lint": "eslint . --ext js,ts,tsx --report-unused-disable-directives --max-warnings 0", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" }, "dependencies": { "@patternfly/patternfly": "^6.6.1", @@ -35,6 +37,10 @@ }, "devDependencies": { "@eslint/js": "10.0.1", + "@storybook/addon-docs": "^10.5.8", + "@storybook/addon-links": "^10.5.8", + "@storybook/react": "^10.5.8", + "@storybook/react-vite": "^10.5.8", "@types/react": "^19.2.18", "@types/react-dom": "^19.2.4", "@typescript-eslint/eslint-plugin": "^8.67.0", @@ -43,8 +49,10 @@ "eslint": "^10.8.1", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.4", + "eslint-plugin-storybook": "^10.5.8", + "storybook": "^10.5.8", "typescript": "^6.0.3", "typescript-eslint": "^8.67.0", "vite": "^8.2.2" } -} \ No newline at end of file +} diff --git a/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.stories.tsx b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.stories.tsx new file mode 100644 index 000000000..28fe45c95 --- /dev/null +++ b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.stories.tsx @@ -0,0 +1,78 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { ChartNodeStorageUsage } from './ChartNodeStorageUsage'; +import { Node } from '@/api/types'; + +const GB = 1024 ** 3; + +const storageNodes: Node[] = [ + { + id: '1', + type: 'nodes', + attributes: { storageUsed: 30 * GB, storageCapacity: 100 * GB }, + }, + { + id: '2', + type: 'nodes', + attributes: { storageUsed: 55 * GB, storageCapacity: 100 * GB }, + }, + { + id: '3', + type: 'nodes', + attributes: { storageUsed: 80 * GB, storageCapacity: 100 * GB }, + }, +]; + +/** 500 nodes with storage + 9 controller-only nodes (no storage attributes, filtered out). */ +const largeClusterNodes: Node[] = [ + ...Array.from({ length: 500 }, (_, i) => ({ + id: String(i + 1), + type: 'nodes' as const, + attributes: { + // Vary used storage across the range 10–90 GB to create a realistic spread + storageUsed: (10 + ((i * 41) % 80)) * GB, + storageCapacity: 100 * GB, + }, + })), + ...Array.from({ length: 9 }, (_, i) => ({ + id: String(501 + i), + type: 'nodes' as const, + // No storage attributes — these are filtered out by the component + attributes: {}, + })), +]; + +const meta: Meta = { + component: ChartNodeStorageUsage, + title: 'Kafka/Nodes/Charts/ChartNodeStorageUsage', +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { nodes: storageNodes }, +}; + +/** Renders the warning alert when no nodes have storage attributes. */ +export const Empty: Story = { + args: { nodes: [] }, +}; + +/** Nodes that lack storageUsed / storageCapacity are filtered out — same result as Empty. */ +export const NoStorageAttributes: Story = { + args: { + nodes: storageNodes.map((n) => ({ ...n, attributes: {} })), + }, +}; + +/** Single node — verifies chart dimensions with minimal data. */ +export const SingleNode: Story = { + args: { + nodes: [storageNodes[0]], + }, +}; + +/** 500 storage nodes + 9 controller-only nodes — exercises chart height scaling and sort controls. */ +export const LargeCluster: Story = { + args: { nodes: largeClusterNodes }, +}; diff --git a/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx index 014906082..eab24c069 100644 --- a/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx @@ -108,10 +108,16 @@ export function ChartNodeStorageUsage({ nodes }: ChartNodeStorageUsageProps) { const barWidth = 20; // Thickness of each individual bar const innerPadding = 16; // Distance between bars in pixels - // Dynamically calculate the SVG canvas size based on data density - const calculatedChartHeight = usedData.length * (barWidth + innerPadding) + 100; + // Size the canvas so Victory allocates exactly (barWidth + innerPadding) px per bar slot. + // Adding the actual top + bottom padding (rather than an arbitrary constant) ensures the + // gap between bars stays constant regardless of the number of nodes. const legendRows = 1; const padding = { ...getPadding(legendRows), left: 70, top: 40 }; + const slotHeight = barWidth + innerPadding; + const calculatedChartHeight = usedData.length * slotHeight + padding.top + padding.bottom; + // Half a slot keeps the first/last bar the same distance from the axis edge as the + // inter-bar gap, regardless of how many nodes are shown. + const edgePadding = slotHeight / 2; if (storageNodes.length === 0) { return ( @@ -173,7 +179,7 @@ export function ChartNodeStorageUsage({ nodes }: ChartNodeStorageUsageProps) { } padding={padding} - domainPadding={{ x: [30, 25] }} + domainPadding={{ x: [edgePadding, edgePadding] }} themeColor={ChartThemeColor.multiOrdered} width={width} height={calculatedChartHeight} diff --git a/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.stories.tsx b/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.stories.tsx new file mode 100644 index 000000000..4a77f7a1d --- /dev/null +++ b/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.stories.tsx @@ -0,0 +1,77 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { ChartPartitionDistribution } from './ChartPartitionDistribution'; +import { Node } from '@/api/types'; + +const brokerNodes: Node[] = [ + { + id: '1', + type: 'nodes', + attributes: { broker: { status: 'Running', replicaCount: 80, leaderCount: 20 } }, + }, + { + id: '2', + type: 'nodes', + attributes: { broker: { status: 'Running', replicaCount: 60, leaderCount: 30 } }, + }, + { + id: '3', + type: 'nodes', + attributes: { broker: { status: 'Running', replicaCount: 65, leaderCount: 45 } }, + }, +]; + +/** 500 brokers + 9 controllers. Controllers have no broker attribute and are filtered out. */ +const largeClusterNodes: Node[] = [ + ...Array.from({ length: 500 }, (_, i) => ({ + id: String(i + 1), + type: 'nodes' as const, + attributes: { + broker: { + status: 'Running' as const, + // Vary replica and leader counts to create a realistic spread + replicaCount: 50 + ((i * 37) % 150), + leaderCount: 10 + ((i * 13) % 50), + }, + }, + })), + ...Array.from({ length: 9 }, (_, i) => ({ + id: String(501 + i), + type: 'nodes' as const, + attributes: { controller: { status: 'QuorumFollower' as const } }, + })), +]; + +const meta: Meta = { + component: ChartPartitionDistribution, + title: 'Kafka/Nodes/Charts/ChartPartitionDistribution', +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { nodes: brokerNodes }, +}; + +/** Renders the warning alert when no broker nodes are present. */ +export const Empty: Story = { + args: { nodes: [] }, +}; + +/** Non-broker nodes (controllers only) are filtered out — same result as Empty. */ +export const ControllerOnlyNodes: Story = { + args: { + nodes: [ + { + id: '10', + type: 'nodes', + attributes: { controller: { status: 'QuorumLeader' } }, + }, + ], + }, +}; + +/** 500 brokers + 9 controllers — exercises chart height scaling and axis tick readability. */ +export const LargeCluster: Story = { + args: { nodes: largeClusterNodes }, +}; diff --git a/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx b/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx index c0c0d1881..bb9c0991c 100644 --- a/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx @@ -59,10 +59,16 @@ export function ChartPartitionDistribution({ nodes }: ChartPartitionDistribution const barWidth = 20; // Thickness of each individual bar const innerPadding = 16; // Distance between bars in pixels - // Dynamically calculate the SVG canvas size based on data density - const calculatedChartHeight = leadersData.length * (barWidth + innerPadding) + 100; + // Size the canvas so Victory allocates exactly (barWidth + innerPadding) px per bar slot. + // Adding the actual top + bottom padding (rather than an arbitrary constant) ensures the + // gap between bars stays constant regardless of the number of nodes. const legendRows = 1; const padding = { ...getPadding(legendRows), left: 70, top: 40 }; + const slotHeight = barWidth + innerPadding; + const calculatedChartHeight = leadersData.length * slotHeight + padding.top + padding.bottom; + // Half a slot keeps the first/last bar the same distance from the axis edge as the + // inter-bar gap, regardless of how many nodes are shown. + const edgePadding = slotHeight / 2; if (brokerNodes.length === 0) { return ( @@ -84,7 +90,7 @@ export function ChartPartitionDistribution({ nodes }: ChartPartitionDistribution } padding={padding} - domainPadding={{ x: [30, 25] }} + domainPadding={{ x: [edgePadding, edgePadding] }} themeColor={ChartThemeColor.multiOrdered} width={width} height={calculatedChartHeight} From e5b294723466af3dc4d4bad0a0777c2024716dc6 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Tue, 18 Aug 2026 09:19:28 -0400 Subject: [PATCH 21/25] Re-enable Storybook test in integration workflow Assisted-by: IBM Bob Signed-off-by: Michael Edgar --- .github/workflows/integration.yml | 36 +- api/src/main/webui/.storybook/main.ts | 1 + api/src/main/webui/package-lock.json | 1169 ++++++++++++++++--------- api/src/main/webui/package.json | 9 +- api/src/main/webui/vite.config.ts | 2 +- api/src/main/webui/vitest.config.ts | 31 + 6 files changed, 807 insertions(+), 441 deletions(-) create mode 100644 api/src/main/webui/vitest.config.ts diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 645a8d3ae..278e20887 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -208,25 +208,23 @@ jobs: npm ci --ignore-scripts npm run lint -# test-storybook: -# runs-on: ubuntu-24.04 -# steps: -# - name: Checkout -# uses: actions/checkout@v7 -# -# - name: Build Storybook -# working-directory: ./ui -# run: | -# npm ci -# npx playwright install -# npm run build-storybook -# -# - name: Test Storybook -# working-directory: ./ui -# run: | -# npx --yes concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \ -# "npx http-server storybook-static --port 6006 --silent" \ -# "npx wait-on tcp:127.0.0.1:6006 && npm run test-storybook" + test-storybook: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Install Dependencies + working-directory: api/src/main/webui + run: npm ci --ignore-scripts + + - name: Install Playwright Browsers + working-directory: api/src/main/webui + run: node_modules/.bin/playwright install chromium --with-deps + + - name: Test Storybook + working-directory: api/src/main/webui + run: npm run test-storybook Playwright: if: ${{ contains(github.event.pull_request.labels.*.name, 'safe to test') || github.repository == 'streamshub/console' }} diff --git a/api/src/main/webui/.storybook/main.ts b/api/src/main/webui/.storybook/main.ts index 22be829a4..5924130d4 100644 --- a/api/src/main/webui/.storybook/main.ts +++ b/api/src/main/webui/.storybook/main.ts @@ -8,6 +8,7 @@ const config: StorybookConfig = { addons: [ '@storybook/addon-links', '@storybook/addon-docs', + '@storybook/addon-vitest', ], framework: { diff --git a/api/src/main/webui/package-lock.json b/api/src/main/webui/package-lock.json index 921bd5262..f03b85cda 100644 --- a/api/src/main/webui/package-lock.json +++ b/api/src/main/webui/package-lock.json @@ -35,6 +35,7 @@ "@eslint/js": "10.0.1", "@storybook/addon-docs": "^10.5.8", "@storybook/addon-links": "^10.5.8", + "@storybook/addon-vitest": "^10.5.9", "@storybook/react": "^10.5.8", "@storybook/react-vite": "^10.5.8", "@types/react": "^19.2.18", @@ -42,6 +43,8 @@ "@typescript-eslint/eslint-plugin": "^8.67.0", "@typescript-eslint/parser": "^8.67.0", "@vitejs/plugin-react": "^6.1.0", + "@vitest/browser": "^4.1.10", + "@vitest/browser-playwright": "^4.1.10", "eslint": "^10.8.1", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.4", @@ -49,7 +52,8 @@ "storybook": "^10.5.8", "typescript": "^6.0.3", "typescript-eslint": "^8.67.0", - "vite": "^8.2.2" + "vite": "^8.2.2", + "vitest": "^4.1.10" } }, "node_modules/@adobe/css-tools": { @@ -328,6 +332,13 @@ "node": ">=6.9.0" } }, + "node_modules/@blazediff/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz", + "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==", + "dev": true, + "license": "MIT" + }, "node_modules/@dnd-kit/accessibility": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", @@ -1506,9 +1517,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.146.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", - "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", "dev": true, "license": "MIT", "funding": { @@ -1516,9 +1527,9 @@ } }, "node_modules/@oxc-resolver/binding-android-arm-eabi": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.24.2.tgz", - "integrity": "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.2.tgz", + "integrity": "sha512-xQoCRv+gKax9KTdwdaQNnAFOai8neay7g3jExDIORzhbrejwGSJaZNTdOJHR5ziLg2joMxOCMOFMo4zuxza2uQ==", "cpu": [ "arm" ], @@ -1530,9 +1541,9 @@ ] }, "node_modules/@oxc-resolver/binding-android-arm64": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.24.2.tgz", - "integrity": "sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.2.tgz", + "integrity": "sha512-HF5oiE2L05yInPYCFD/4uxSrEZW4SuIfn99Y6L1xnJnzl066JR+MJs2rIdstw8A2MPlAKH+13dpFPNycjqzvGg==", "cpu": [ "arm64" ], @@ -1544,9 +1555,9 @@ ] }, "node_modules/@oxc-resolver/binding-darwin-arm64": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.24.2.tgz", - "integrity": "sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.2.tgz", + "integrity": "sha512-UX4u49CVCAD8QZNELaW8eMGgMAGwFWYEPbvNsh+3r/gs4NX3KfpiACMVwRQT0EuH3uat9hM5Zl+Ppm9pJD8tgg==", "cpu": [ "arm64" ], @@ -1558,9 +1569,9 @@ ] }, "node_modules/@oxc-resolver/binding-darwin-x64": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.24.2.tgz", - "integrity": "sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.2.tgz", + "integrity": "sha512-J9xPx7YBkrRmJ+xl561ztnMWEc1aOyjEIxBiGX1dVb3u7bGSnfObfcZk+Pd+uM0HZAPNsQ1xvD8j52A/uOSqNQ==", "cpu": [ "x64" ], @@ -1572,9 +1583,9 @@ ] }, "node_modules/@oxc-resolver/binding-freebsd-x64": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.24.2.tgz", - "integrity": "sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.2.tgz", + "integrity": "sha512-fRlt7OvSaQkWj6+EDTVxawVxOlqJB2QSnBfkeCyK4RTvsGctbw3BiH2Tb7DzMs7bikc4BRBpvWP5zF9K8b54Zg==", "cpu": [ "x64" ], @@ -1586,9 +1597,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.24.2.tgz", - "integrity": "sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.2.tgz", + "integrity": "sha512-gK+vPUcPQITkGwBKpZGrcDHSlU6eDGl7AQacxS2CEKAZIBHWkOVFeJwLZ4tYnA1acJqRM5lt7yYwPCVqGHIJ7A==", "cpu": [ "arm" ], @@ -1600,9 +1611,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.24.2.tgz", - "integrity": "sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.2.tgz", + "integrity": "sha512-L/Rgas7SrOKy/z7IH+HxSFRqVO4PuLDKLEGKvnhoCBBq3UJ0YzGBou3qMzaAGgkJwsIvX2pBF+7ojzfUhLiZxQ==", "cpu": [ "arm" ], @@ -1614,9 +1625,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.24.2.tgz", - "integrity": "sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.2.tgz", + "integrity": "sha512-CUEYvlX1Fk7E9kUMzuswru1J9HLxMwnpDeQGjQuI4ZH+iNCoa2X9T+pvyzrbsgl7WnIeTFTlNlHsRfVdf5g9/g==", "cpu": [ "arm64" ], @@ -1628,9 +1639,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-arm64-musl": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.24.2.tgz", - "integrity": "sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.2.tgz", + "integrity": "sha512-ViN1ZibQyxwC67GpoP33oo+S9UyUnkog13vzQb9+v9bCNvrVzJuk0MRdacjtv/9xfRMVF/eqHFyl8YOeykI10Q==", "cpu": [ "arm64" ], @@ -1642,9 +1653,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.24.2.tgz", - "integrity": "sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.2.tgz", + "integrity": "sha512-CU1sCqWnhGqYD5I1HedHk5pujrn7ssDkNB/AEQd/pd3D/EojVSgJUlpbafwIbyhia3PgIfkvdFpRPWSALzVumA==", "cpu": [ "ppc64" ], @@ -1656,9 +1667,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.24.2.tgz", - "integrity": "sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.2.tgz", + "integrity": "sha512-jWVyZtIHca4Gb96x7dag+y69vlei7ffjrsveLkmf2ZhqEAz6ZSBnY1GWvgZUaZlwZP62A3xD092BW97Q5VGc+g==", "cpu": [ "riscv64" ], @@ -1670,9 +1681,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.24.2.tgz", - "integrity": "sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.2.tgz", + "integrity": "sha512-LF29obFqNgBUgDX7rmUK7M4D0JQG5LxhYzn3xXmECcHU9aQAdWG7NiY052qybtesEdwHQXKNTWYQ7mTsybNvWg==", "cpu": [ "riscv64" ], @@ -1684,9 +1695,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.24.2.tgz", - "integrity": "sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.2.tgz", + "integrity": "sha512-+NYcm+cCHBbtdQQ3A4phQTSuVRYnNHz7wrl9XRAPEovcdoqi0mb1K5ZOl+jN54ZD+q1zz3V0vltbFJmzecJKmw==", "cpu": [ "s390x" ], @@ -1698,9 +1709,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-x64-gnu": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.24.2.tgz", - "integrity": "sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.2.tgz", + "integrity": "sha512-UQqZDdG2r2HhAOsZEgufkIWHPQ886IUyuJQkoZByvzhW8j51R4UNzGBJFkTiTnLhQnggwJRdJgFWK4uY6ZVIMw==", "cpu": [ "x64" ], @@ -1712,9 +1723,9 @@ ] }, "node_modules/@oxc-resolver/binding-linux-x64-musl": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.24.2.tgz", - "integrity": "sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.2.tgz", + "integrity": "sha512-3Q9PMRjalWkT6NZ4jfujuqTCFwoWErg3y3BnOgb544B8IMw4PktiwWOigMfOHNRLMghZeJ7hpfpZf4CP7rV7Og==", "cpu": [ "x64" ], @@ -1726,9 +1737,9 @@ ] }, "node_modules/@oxc-resolver/binding-openharmony-arm64": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.24.2.tgz", - "integrity": "sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.2.tgz", + "integrity": "sha512-Eljeq3ndtyKhM+Es8LITi4Zl2htzuRZcrPMF3kMCsrILztvU6AjZ3FuEhHHKobPUB9rMpzLpJP5bDqXI7r+iog==", "cpu": [ "arm64" ], @@ -1740,9 +1751,9 @@ ] }, "node_modules/@oxc-resolver/binding-wasm32-wasi": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.24.2.tgz", - "integrity": "sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.2.tgz", + "integrity": "sha512-HGDbNsIywqc4LxU38+CTJNnB/6BF7rheWOJ259b4eE0aEnelYblC8x+1tEd63bp31fYne63RQz9Jb4yVnC7Yig==", "cpu": [ "wasm32" ], @@ -1750,18 +1761,18 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "1.11.2", - "@emnapi/runtime": "1.11.2", - "@napi-rs/wasm-runtime": "^1.1.6" + "@emnapi/core": "1.11.0", + "@emnapi/runtime": "1.11.0", + "@napi-rs/wasm-runtime": "^1.1.5" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", - "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", + "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", "dev": true, "license": "MIT", "optional": true, @@ -1771,9 +1782,9 @@ } }, "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", - "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", "dev": true, "license": "MIT", "optional": true, @@ -1793,9 +1804,9 @@ } }, "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.24.2.tgz", - "integrity": "sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.2.tgz", + "integrity": "sha512-iWx25CBEgH49iE9q5coEGI/jb1jl5kkCY9z6U5Og67xCkQ/WFMDc2J5U78+AE91SUxM2NqSLhJC8/PLfWnImww==", "cpu": [ "arm64" ], @@ -1807,9 +1818,9 @@ ] }, "node_modules/@oxc-resolver/binding-win32-x64-msvc": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.24.2.tgz", - "integrity": "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.2.tgz", + "integrity": "sha512-VPoCAhKvCQTlG7vxqaBXcmuvbh77BfnGXj8g0pbvVXpm1F/R8rDVwqIWcEbMzrI1JvJlm8v7T9uMVQb6UctMRg==", "cpu": [ "x64" ], @@ -2047,6 +2058,13 @@ "react-dom": "^17 || ^18 || ^19" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", @@ -2332,17 +2350,24 @@ } } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@storybook/addon-docs": { - "version": "10.5.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.8.tgz", - "integrity": "sha512-NlHiMKW/UvW/uL8HXFDCEVwoH3qZeGYZ/qlWax4d7H471b/T54MBq2KcB4ZrdA785FfIH3numAJdBb5jwn00Mg==", + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.10.tgz", + "integrity": "sha512-06JoK3/a7FWI/6GzuidJP9iHp1/Vejboe6lzS1jW+d8ItpecriBt+oXh1VNmUM7i7PjI6pZnet+j51QnLyeOoQ==", "dev": true, "license": "MIT", "dependencies": { "@mdx-js/react": "^3.0.0", - "@storybook/csf-plugin": "10.5.8", + "@storybook/csf-plugin": "10.5.10", "@storybook/icons": "^2.0.2", - "@storybook/react-dom-shim": "10.5.8", + "@storybook/react-dom-shim": "10.5.10", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" @@ -2353,7 +2378,7 @@ }, "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.8" + "storybook": "^10.5.10" }, "peerDependenciesMeta": { "@types/react": { @@ -2362,9 +2387,9 @@ } }, "node_modules/@storybook/addon-links": { - "version": "10.5.8", - "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.5.8.tgz", - "integrity": "sha512-mpWw4alBJVGqgVh897LZ2keN/xnMHcH93wKJG+oGg4+cdEUA+06hCs5T4k+AS5Aa+EZ6LvdOoi2VPHssyQlCCA==", + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-10.5.10.tgz", + "integrity": "sha512-wZQX26vSKEBuQB+EKHuAoeDh0XiUDrN2GMX2lhhj2KpJ/di4OyXFaHXspXOXWkjNtyMTRd0QkCHQqKx4LY3E6w==", "dev": true, "license": "MIT", "dependencies": { @@ -2377,7 +2402,7 @@ "peerDependencies": { "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.8" + "storybook": "^10.5.10" }, "peerDependenciesMeta": { "@types/react": { @@ -2388,14 +2413,50 @@ } } }, + "node_modules/@storybook/addon-vitest": { + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.5.10.tgz", + "integrity": "sha512-JNQ9DSkLfxC8qqytBCej91zBExIZ7z97B410U2zgfQPki4HkI9Ffz97a15f5yhVZ79ppIrZ/ssI+WcyWn0ykXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/icons": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@vitest/browser": "^3.0.0 || ^4.0.0", + "@vitest/browser-playwright": "^4.0.0", + "@vitest/runner": "^3.0.0 || ^4.0.0", + "storybook": "^10.5.10", + "vitest": "^3.0.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/runner": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, "node_modules/@storybook/builder-vite": { - "version": "10.5.8", - "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.5.8.tgz", - "integrity": "sha512-UeRnn7yT55WmBlHNOQzLrvN7vsHEvVgIukhKDO+4cMbGXN87wZkbxhx6NstpuXRH8OxGqwKS0SZNVp+SC1ftLQ==", + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.5.10.tgz", + "integrity": "sha512-O4GgIP0tKLRueom3EmU3OaBUHKjNYj+jkOvmTIkn3PYTiWVkCuHqSKEs4ADvRyaQuLH+peHhFe4JtkNC9KbtrQ==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/csf-plugin": "10.5.8", + "@storybook/csf-plugin": "10.5.10", "ts-dedent": "^2.0.0" }, "funding": { @@ -2403,14 +2464,14 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^10.5.8", + "storybook": "^10.5.10", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@storybook/csf-plugin": { - "version": "10.5.8", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.8.tgz", - "integrity": "sha512-/FHiMyOWWEXfwK/lM0WxmkP9GLzbSJJuzGtfeuNWSOVDnvAMbjavitxfHb5wSbWKIQo0XYC1EJ2Y7x91XNYP4w==", + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.10.tgz", + "integrity": "sha512-TaCLBrqVEr767+w58QDotDUiCTuE5cyRJuRcDlsKQyUIyGBv+lYD3lu8wBiVCYxgIjB/gu9HmqiC+0yx1rHzaw==", "dev": true, "license": "MIT", "dependencies": { @@ -2423,7 +2484,7 @@ "peerDependencies": { "esbuild": "*", "rollup": "*", - "storybook": "^10.5.8", + "storybook": "^10.5.10", "vite": "*", "webpack": "*" }, @@ -2460,14 +2521,14 @@ } }, "node_modules/@storybook/react": { - "version": "10.5.8", - "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.8.tgz", - "integrity": "sha512-6qqkmqX6imtL+0Z9Uan2tIfYivOI0FiVmWr0zpqqQR15AkJ18JfNcNTQoyjeAlCO0Kei56SWqnu2qLq52TYplg==", + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.10.tgz", + "integrity": "sha512-4MBV5e1SXIMfPynLHzr+Mp0dwGv/FW1bklWAsS4ynBOAbC98W9p/I9vqBnUctsvE3BJkhzHQQyPwMHL5tTcHVA==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/react-dom-shim": "10.5.8", + "@storybook/react-dom-shim": "10.5.10", "react-docgen": "^8.0.2", "react-docgen-typescript": "^2.2.2" }, @@ -2480,7 +2541,7 @@ "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.8", + "storybook": "^10.5.10", "typescript": ">= 4.9.x" }, "peerDependenciesMeta": { @@ -2496,9 +2557,9 @@ } }, "node_modules/@storybook/react-dom-shim": { - "version": "10.5.8", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.8.tgz", - "integrity": "sha512-N8D13/Xny+V3kfe1KBgsAHS0nKWXLLdgOOXS9poKdYzVwVCN+CGEGBxWX0zMMtdCptqa6/57em9coPlZMoO+bg==", + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.10.tgz", + "integrity": "sha512-rbu62ILo/VE3iXKmu+kWXFpD1H1Lwi0f19q/x7JnDsD2dxKS9w5znLEqPIq2qxpzi/wjjIb2iUP1cRG1d/9W5A==", "dev": true, "license": "MIT", "funding": { @@ -2510,7 +2571,7 @@ "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.8" + "storybook": "^10.5.10" }, "peerDependenciesMeta": { "@types/react": { @@ -2522,16 +2583,16 @@ } }, "node_modules/@storybook/react-vite": { - "version": "10.5.8", - "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.5.8.tgz", - "integrity": "sha512-ioMJGi4YzueGsJBlYio+2+UhfCFB9QV5Bs1lOilkek+a4BZgKJl0D1mVSJl6k96stQBPZmLgI9/l0hLVcUL6Kg==", + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.5.10.tgz", + "integrity": "sha512-xOztxefUnqKeuyvcnjspqmlDnER4cExL+liltrpdXLPJVqfFNr9lgM49FyEPajzsUVG9W/vHJWjbaQGGu1UsYQ==", "dev": true, "license": "MIT", "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0", "@rollup/pluginutils": "^5.0.2", - "@storybook/builder-vite": "10.5.8", - "@storybook/react": "10.5.8", + "@storybook/builder-vite": "10.5.10", + "@storybook/react": "10.5.10", "empathic": "^2.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.2", @@ -2545,7 +2606,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "storybook": "^10.5.8", + "storybook": "^10.5.10", "typescript": ">= 4.9.x", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, @@ -2629,9 +2690,9 @@ "license": "MIT" }, "node_modules/@testing-library/user-event": { - "version": "14.6.4", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.4.tgz", - "integrity": "sha512-QCGwP6QrjypBLwyj5cuyfVamkaIEy/XGY+1VDehbtbQqOggYmTFpFOdWR5mPz14vX8vXLMVjDHlRNBcClyO9ew==", + "version": "14.6.6", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.6.tgz", + "integrity": "sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw==", "dev": true, "license": "MIT", "engines": { @@ -3111,6 +3172,53 @@ } } }, + "node_modules/@vitest/browser": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.11.tgz", + "integrity": "sha512-bwMovvAeuTFOK5kIFevw4VEf+1gVEICv4SYK4k3knJOxl6b1zEWud8mYKD73e1B0odAn174h1MofURy2TPWf3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@blazediff/core": "1.9.1", + "@vitest/mocker": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "tinyrainbow": "^3.1.0", + "ws": "^8.19.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.1.11" + } + }, + "node_modules/@vitest/browser-playwright": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.11.tgz", + "integrity": "sha512-riLBxPqwnJ0lWs2DN2WeUfYeKLoAjbP2Xx8cLQdSddzMi20sksIa6K2mPz79DyMZKKVKH2ksOC2yJvtNcZg8cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/browser": "4.1.11", + "@vitest/mocker": "4.1.11", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "4.1.11" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": false + } + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -3128,7 +3236,7 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/pretty-format": { + "node_modules/@vitest/expect/node_modules/@vitest/pretty-format": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", @@ -3141,7 +3249,7 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/spy": { + "node_modules/@vitest/expect/node_modules/@vitest/spy": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", @@ -3154,7 +3262,7 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils": { + "node_modules/@vitest/expect/node_modules/@vitest/utils": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", @@ -3169,6 +3277,121 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/expect/node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@webcontainer/env": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@webcontainer/env/-/env-1.1.1.tgz", @@ -3295,16 +3518,16 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/brace-expansion/node_modules/balanced-match": { @@ -3798,6 +4021,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", @@ -3953,9 +4183,9 @@ } }, "node_modules/eslint-plugin-storybook": { - "version": "10.5.8", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.8.tgz", - "integrity": "sha512-bf9W5nZyWdIaCUZf4aEZnEeD1mn+csNYX8dYUQjAo6L7/DkSLtr65R4zFZ1xeS4m6dOXO6UtUySesCSw4e8w1g==", + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-10.5.10.tgz", + "integrity": "sha512-NeOu3axmhNZfRuHRciFH0a/OVgvtAJLRHAwiJjcGkPAcElNmwEPp+pqoiwxytx7kHInz7mVVoBfPwVc8kPmOLw==", "dev": true, "license": "MIT", "dependencies": { @@ -3963,8 +4193,7 @@ "@typescript-eslint/utils": "^8.60.0" }, "peerDependencies": { - "eslint": ">=8", - "storybook": "^10.5.8" + "eslint": ">=8" } }, "node_modules/eslint-scope": { @@ -4120,6 +4349,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4232,9 +4471,9 @@ } }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4242,6 +4481,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -4807,18 +5047,18 @@ "lightningcss-win32-x64-msvc": "1.33.0" } }, - "node_modules/lightningcss-android-arm64": { + "node_modules/lightningcss-linux-x64-gnu": { "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MPL-2.0", "optional": true, "os": [ - "android" + "linux" ], "engines": { "node": ">= 12.0.0" @@ -4828,271 +5068,61 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", - "cpu": [ - "arm64" - ], + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, "engines": { - "node": ">= 12.0.0" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "bin": { + "loose-envify": "cli.js" } }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", - "cpu": [ - "x64" - ], + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" } }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", "bin": { @@ -5155,6 +5185,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5207,6 +5247,20 @@ "node": ">=0.10.0" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -5282,45 +5336,35 @@ "@oxc-parser/binding-win32-x64-msvc": "0.127.0" } }, - "node_modules/oxc-parser/node_modules/@oxc-project/types": { - "version": "0.127.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", - "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, "node_modules/oxc-resolver": { - "version": "11.24.2", - "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.24.2.tgz", - "integrity": "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==", + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.21.2.tgz", + "integrity": "sha512-w5tLwYN3Zo24w5EeWJjJWZOwhYqTtC8PS2B1tIt7BZUuqTIcU07sQValbDw+rq7+AuAGzOHklgK+ifsy4lpXfw==", "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxc-resolver/binding-android-arm-eabi": "11.24.2", - "@oxc-resolver/binding-android-arm64": "11.24.2", - "@oxc-resolver/binding-darwin-arm64": "11.24.2", - "@oxc-resolver/binding-darwin-x64": "11.24.2", - "@oxc-resolver/binding-freebsd-x64": "11.24.2", - "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", - "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", - "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", - "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", - "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", - "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", - "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", - "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", - "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", - "@oxc-resolver/binding-linux-x64-musl": "11.24.2", - "@oxc-resolver/binding-openharmony-arm64": "11.24.2", - "@oxc-resolver/binding-wasm32-wasi": "11.24.2", - "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", - "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" + "@oxc-resolver/binding-android-arm-eabi": "11.21.2", + "@oxc-resolver/binding-android-arm64": "11.21.2", + "@oxc-resolver/binding-darwin-arm64": "11.21.2", + "@oxc-resolver/binding-darwin-x64": "11.21.2", + "@oxc-resolver/binding-freebsd-x64": "11.21.2", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.2", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.2", + "@oxc-resolver/binding-linux-arm64-gnu": "11.21.2", + "@oxc-resolver/binding-linux-arm64-musl": "11.21.2", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.2", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.2", + "@oxc-resolver/binding-linux-riscv64-musl": "11.21.2", + "@oxc-resolver/binding-linux-s390x-gnu": "11.21.2", + "@oxc-resolver/binding-linux-x64-gnu": "11.21.2", + "@oxc-resolver/binding-linux-x64-musl": "11.21.2", + "@oxc-resolver/binding-openharmony-arm64": "11.21.2", + "@oxc-resolver/binding-wasm32-wasi": "11.21.2", + "@oxc-resolver/binding-win32-arm64-msvc": "11.21.2", + "@oxc-resolver/binding-win32-x64-msvc": "11.21.2" } }, "node_modules/p-limit": { @@ -5409,6 +5453,13 @@ "node": "20 || >=22" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pathval": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", @@ -5439,6 +5490,50 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/postcss": { "version": "8.5.26", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", @@ -5797,6 +5892,16 @@ "@rolldown/binding-win32-x64-msvc": "1.2.5" } }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -5858,6 +5963,28 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5878,10 +6005,24 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/storybook": { - "version": "10.5.8", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.8.tgz", - "integrity": "sha512-rR4oFMSiWBSqI0lvsJPtcQUPj8+hzj3TkLu+Mw61Wo6YxPSb5FsLSHai0jZnuaIdKIlmu25KCfwlSQl4e1uvnA==", + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.10.tgz", + "integrity": "sha512-Rz8k9ejFHsi7lbtJTaxZlhCUz4GkbJIKEoKDjXeLfr/ZhXip73E6keKxW0KH8iGeKiCqHAbJCV4YIQrxTOLiig==", "dev": true, "license": "MIT", "dependencies": { @@ -5897,7 +6038,7 @@ "jsonc-parser": "^3.3.1", "open": "^10.2.0", "oxc-parser": "^0.127.0", - "oxc-resolver": "^11.19.1", + "oxc-resolver": "11.21.2", "recast": "^0.23.5", "semver": "^7.7.3", "use-sync-external-store": "^1.5.0", @@ -5927,6 +6068,19 @@ } } }, + "node_modules/storybook/node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -6009,6 +6163,23 @@ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -6027,9 +6198,9 @@ } }, "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { @@ -6046,6 +6217,16 @@ "node": ">=14.0.0" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -6811,6 +6992,139 @@ } } }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitest": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", @@ -6834,6 +7148,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/api/src/main/webui/package.json b/api/src/main/webui/package.json index 4ec319983..da0cf340c 100644 --- a/api/src/main/webui/package.json +++ b/api/src/main/webui/package.json @@ -9,7 +9,8 @@ "preview": "vite preview", "lint": "eslint . --ext js,ts,tsx --report-unused-disable-directives --max-warnings 0", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "build-storybook": "storybook build", + "test-storybook": "vitest --config vitest.config.ts" }, "dependencies": { "@patternfly/patternfly": "^6.6.1", @@ -39,6 +40,7 @@ "@eslint/js": "10.0.1", "@storybook/addon-docs": "^10.5.8", "@storybook/addon-links": "^10.5.8", + "@storybook/addon-vitest": "^10.5.9", "@storybook/react": "^10.5.8", "@storybook/react-vite": "^10.5.8", "@types/react": "^19.2.18", @@ -46,6 +48,8 @@ "@typescript-eslint/eslint-plugin": "^8.67.0", "@typescript-eslint/parser": "^8.67.0", "@vitejs/plugin-react": "^6.1.0", + "@vitest/browser": "^4.1.10", + "@vitest/browser-playwright": "^4.1.10", "eslint": "^10.8.1", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.4", @@ -53,6 +57,7 @@ "storybook": "^10.5.8", "typescript": "^6.0.3", "typescript-eslint": "^8.67.0", - "vite": "^8.2.2" + "vite": "^8.2.2", + "vitest": "^4.1.10" } } diff --git a/api/src/main/webui/vite.config.ts b/api/src/main/webui/vite.config.ts index 46a4e6e41..eadfb78fc 100644 --- a/api/src/main/webui/vite.config.ts +++ b/api/src/main/webui/vite.config.ts @@ -36,5 +36,5 @@ export default defineConfig({ '@patternfly/react-data-view', 'react-json-view-lite', ] - } + }, }); diff --git a/api/src/main/webui/vitest.config.ts b/api/src/main/webui/vitest.config.ts new file mode 100644 index 000000000..05302cf73 --- /dev/null +++ b/api/src/main/webui/vitest.config.ts @@ -0,0 +1,31 @@ +import { mergeConfig } from 'vitest/config'; +import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; +import { playwright } from '@vitest/browser-playwright'; +import viteConfig from './vite.config.ts'; + +export default mergeConfig(viteConfig, { + plugins: [ + storybookTest(), + ], + // Bundle CJS-only packages imported by @storybook/addon-vitest's setup file. + // Scoped to the test environment — has no effect on the production build. + optimizeDeps: { + include: [ + 'aria-query', + 'lz-string', + 'pretty-format', + ], + }, + ssr: { + noExternal: true, + }, + test: { + name: 'storybook', + browser: { + enabled: true, + headless: true, + provider: playwright({}), + instances: [{ browser: 'chromium' }], + }, + }, +}); From 3823cb575946e4340ec6692a7e8c08670666465e Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Tue, 18 Aug 2026 10:57:06 -0400 Subject: [PATCH 22/25] Implement fixes from Bob review Assisted-by: IBM Bob Signed-off-by: Michael Edgar --- api/src/main/webui/src/api/types.ts | 4 +- .../common/ResourceListDataView.tsx | 2 +- .../kafka/nodes/BrokerImpactTable.tsx | 56 ++++++++++--------- .../components/kafka/nodes/NodeChartsCard.tsx | 3 +- .../nodes/charts/ChartNodeStorageUsage.tsx | 22 +++++--- .../charts/ChartPartitionDistribution.tsx | 2 +- api/src/main/webui/src/i18n/messages/en.json | 5 +- .../pages/kafka/nodes/NodesRebalancesTab.tsx | 23 +++++--- 8 files changed, 69 insertions(+), 48 deletions(-) diff --git a/api/src/main/webui/src/api/types.ts b/api/src/main/webui/src/api/types.ts index 36f497e42..e50f43c94 100644 --- a/api/src/main/webui/src/api/types.ts +++ b/api/src/main/webui/src/api/types.ts @@ -478,12 +478,12 @@ export interface BrokerCapacity { cpu: string | null; inboundNetwork: string | null; outboundNetwork: string | null; - overrides: [{ + overrides: Array<{ brokers: number[] | null; cpu: string | null; inboundNetwork: string | null; outboundNetwork: string | null; - }]; + }>; } export interface BrokerLoadImpact { diff --git a/api/src/main/webui/src/components/common/ResourceListDataView.tsx b/api/src/main/webui/src/components/common/ResourceListDataView.tsx index 756cd798d..e04c2497b 100644 --- a/api/src/main/webui/src/components/common/ResourceListDataView.tsx +++ b/api/src/main/webui/src/components/common/ResourceListDataView.tsx @@ -190,7 +190,7 @@ export interface ResourceListDataViewRowResult { } function isRowResult(v: DataViewTr | ResourceListDataViewRowResult): v is ResourceListDataViewRowResult { - return v !== null && typeof v === 'object' && !Array.isArray(v) && 'expandedRows' in v; + return v !== null && typeof v === 'object' && !Array.isArray(v) && 'row' in v; } export interface ResourceListDataViewRowMapper { diff --git a/api/src/main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx b/api/src/main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx index 6af9438f8..3223ec92a 100644 --- a/api/src/main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { Fragment, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, @@ -112,7 +112,7 @@ function formatDiff(diff: number): string { } function formatFixed(positions: number, val?: number): string { - if (val) { + if (val != null) { if (Number.isInteger(val)) { return val.toString(); } else { @@ -139,7 +139,7 @@ function DeltaCell({ {absDiff !== undefined ? <>{formatDiff(absDiff)} {absUnit} : <>} {absDiff !== undefined && pctDiff !== undefined ? <> /  : <>} - {pctDiff !== undefined ? <>{formatDiff(pctDiff ?? '-')}% : <>} + {pctDiff !== undefined ? <>{formatDiff(pctDiff)}% : <>} ); } @@ -210,8 +210,7 @@ export function BrokerImpactTable({ return Object.entries(brokerImpact) .map(([brokerId, metrics]) => ({ brokerId, metrics })) .sort((a, b) => { - // eslint-disable-next-line no-useless-assignment - let result = 0; + let result; if (sortKey === 'brokerId') { const aNum = parseInt(a.brokerId, 10); @@ -257,13 +256,13 @@ export function BrokerImpactTable({ return filteredRows.slice(start, start + perPage); }, [filteredRows, page, perPage]); - const toggleBroker = (brokerId: string) => { + const toggleBroker = useCallback((brokerId: string) => { setSelectedBrokers((prev) => prev.includes(brokerId) ? prev.filter((b) => b !== brokerId) : [...prev, brokerId], ); - }; + }, []); - const handleSort = (key: SortKey) => { + const handleSort = useCallback((key: SortKey) => { if (sortKey === key) { setSortDirection((d) => (d === 'asc' ? 'desc' : 'asc')); } else { @@ -271,7 +270,7 @@ export function BrokerImpactTable({ setSortDirection('asc'); } setPage(1); - }; + }, [sortKey]); const getSortParams = (key: SortKey): ThProps['sort'] => ({ sortBy: { @@ -292,7 +291,8 @@ export function BrokerImpactTable({ const brokerFilterLabels = selectedBrokers.map((b) => t('rebalancing.broker', { b })); - const colCount = 1 + activeGroups.length * (onlyDeltas ? 1 : 3); + // +1 for the sticky broker-ID column, +1 for the broker capacity column at the end + const colCount = 2 + activeGroups.length * (onlyDeltas ? 1 : 3); return ( <> @@ -387,25 +387,27 @@ export function BrokerImpactTable({ {t('rebalancing.brokerImpact.broker')} {activeGroups.map((g) => ( - <> + {!onlyDeltas && ( - + {g.label} {t('rebalancing.brokerImpact.before')} )} {!onlyDeltas && ( - + {g.label} {t('rebalancing.brokerImpact.after')} )} - + {g.label} Δ - + ))} - - {t('rebalancing.brokerImpact.brokerCapacity')} - + {!onlyDeltas && ( + + {t('rebalancing.brokerImpact.brokerCapacityConfig')} + + )} @@ -427,9 +429,9 @@ export function BrokerImpactTable({ const pctImpact = g.pctKey ? row.metrics[g.pctKey] : undefined; const absImpact = g.absKey ? row.metrics[g.absKey] : undefined; return ( - <> + {!onlyDeltas && ( - + {pctImpact ? )} {!onlyDeltas && ( - + {pctImpact ? {formatFixed(2, absImpact?.after)} {g?.absUnit}} )} - + - + ); })} - - {buildBrokerCapacity(row.brokerId, brokerCapacity)} - + {!onlyDeltas && ( + + {buildBrokerCapacity(row.brokerId, brokerCapacity)} + + )} )) )} diff --git a/api/src/main/webui/src/components/kafka/nodes/NodeChartsCard.tsx b/api/src/main/webui/src/components/kafka/nodes/NodeChartsCard.tsx index 1a76cb2e9..2293a02b3 100644 --- a/api/src/main/webui/src/components/kafka/nodes/NodeChartsCard.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/NodeChartsCard.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Card, @@ -22,7 +23,7 @@ export interface NodeChartsCardProps { export function NodeChartsCard({ nodeResult }: NodeChartsCardProps) { const { t } = useTranslation(); - const nodes = nodeResult.data?.data ?? []; + const nodes = useMemo(() => nodeResult.data?.data ?? [], [nodeResult.data]); return ( diff --git a/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx index eab24c069..15efd2327 100644 --- a/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/charts/ChartNodeStorageUsage.tsx @@ -96,13 +96,19 @@ export function ChartNodeStorageUsage({ nodes }: ChartNodeStorageUsageProps) { // Compute 5 evenly-spaced, round tick values from 0 to maxCapacity. // Victory's tickCount hint does not produce round values for byte ranges, // so we derive explicit tickValues instead. - const maxCapacity = Math.max(...storageNodes.map((n) => n.attributes.storageCapacity as number)); - const tickStep = maxCapacity / 4; - - // Round step up to a power-of-1024 boundary so labels stay in one unit. - const unitBoundary = Math.pow(1024, Math.floor(Math.log(tickStep) / Math.log(1024))); - const roundedStep = Math.ceil(tickStep / unitBoundary) * unitBoundary; - const tickValues = [0, 1, 2, 3, 4].map((i) => i * roundedStep); + // Math.max(...array) avoids potential stack overflow for very large datasets + // by using reduce instead of spread. + const tickValues = useMemo(() => { + const maxCapacity = storageNodes.reduce( + (max, n) => Math.max(max, n.attributes.storageCapacity as number), + 0, + ); + const tickStep = maxCapacity / 4; + // Round step up to a power-of-1024 boundary so labels stay in one unit. + const unitBoundary = Math.pow(1024, Math.floor(Math.log(tickStep) / Math.log(1024))); + const roundedStep = Math.ceil(tickStep / unitBoundary) * unitBoundary; + return [0, 1, 2, 3, 4].map((i) => i * roundedStep); + }, [storageNodes]); // Configure custom spacing dimensions const barWidth = 20; // Thickness of each individual bar @@ -187,7 +193,7 @@ export function ChartNodeStorageUsage({ nodes }: ChartNodeStorageUsageProps) { > formatBytes(d)} diff --git a/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx b/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx index bb9c0991c..5f1a1d913 100644 --- a/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/charts/ChartPartitionDistribution.tsx @@ -98,7 +98,7 @@ export function ChartPartitionDistribution({ nodes }: ChartPartitionDistribution > diff --git a/api/src/main/webui/src/i18n/messages/en.json b/api/src/main/webui/src/i18n/messages/en.json index c15d02d13..2a39bba24 100644 --- a/api/src/main/webui/src/i18n/messages/en.json +++ b/api/src/main/webui/src/i18n/messages/en.json @@ -522,6 +522,7 @@ "storageUsageTooltip": "Used and available storage per node.", "storageUsageNoData": "No storage usage data available", "storageUsageAriaTitle": "Node storage usage chart", + "storageUsageAxisLabel": "Storage", "storageUsageSeriesUsed": "Used", "storageUsageSeriesUsedLabel": "Used {{storageUsed}} ({{storageUsedPct}})% of {{storageTotal}}", "storageUsageSeriesAvailable": "Available", @@ -539,6 +540,7 @@ "partitionDistributionTooltip": "Total replicas and leader partitions per broker node.", "partitionDistributionNoData": "No partition data available", "partitionDistributionAriaTitle": "Partition distribution chart", + "partitionDistributionAxisLabel": "Partitions", "partitionDistributionSeriesLeaders": "Leaders", "partitionDistributionSeriesReplicas": "Replicas" } @@ -590,7 +592,8 @@ "selectBrokers": "Select brokers", "onlyShowDeltas": "Only show deltas", "noData": "No broker impact data available. A proposal must be generated first.", - "noResults": "No brokers match the current filters." + "noResults": "No brokers match the current filters.", + "brokerCapacityConfig": "Configured Broker Capacity" }, "proposalDetail": { "title": "Proposal detail", diff --git a/api/src/main/webui/src/pages/kafka/nodes/NodesRebalancesTab.tsx b/api/src/main/webui/src/pages/kafka/nodes/NodesRebalancesTab.tsx index fa3706288..9799b140e 100644 --- a/api/src/main/webui/src/pages/kafka/nodes/NodesRebalancesTab.tsx +++ b/api/src/main/webui/src/pages/kafka/nodes/NodesRebalancesTab.tsx @@ -20,11 +20,18 @@ import { Rebalance } from '@/api/types'; export function NodesRebalancesTab() { const { t } = useTranslation(); const { kafkaId } = useParams<{ kafkaId: string }>(); - const { data: clusterData } = useKafkaCluster(kafkaId, { fields: 'cruiseControlEnabled' }); - const cruiseControlEnabled = clusterData?.data?.attributes?.cruiseControlEnabled ?? true; + const { data: clusterData, isLoading: isClusterLoading } = useKafkaCluster(kafkaId, { fields: 'cruiseControlEnabled' }); + // While the cluster data is loading we don't yet know whether CC is enabled, + // so default to undefined (not true) to avoid a premature rebalances fetch or + // a false-positive "not enabled" flash. + const cruiseControlEnabled = isClusterLoading + ? undefined + : (clusterData?.data?.attributes?.cruiseControlEnabled ?? false); const [dataParams, setDataParams] = useState({}); - const rebalanceResult = useRebalances(kafkaId, dataParams); + // Disable the rebalances query until we know CC is enabled, avoiding a + // wasted network request on clusters where it is not configured. + const rebalanceResult = useRebalances(kafkaId, { ...dataParams, enabled: cruiseControlEnabled === true }); const handleDataViewChange = useCallback((params: ResourceListParams) => { setDataParams(params); @@ -55,20 +62,20 @@ export function NodesRebalancesTab() { setIsConfirmModalOpen(true); }, []); - const handleConfirmAction = () => { + const handleConfirmAction = useCallback(() => { if (pendingRebalance) { patchRebalance({ rebalanceId: pendingRebalance.id, action: pendingAction }); } setIsConfirmModalOpen(false); setPendingRebalance(null); - }; + }, [pendingRebalance, patchRebalance, pendingAction]); - const handleCancelAction = () => { + const handleCancelAction = useCallback(() => { setIsConfirmModalOpen(false); setPendingRebalance(null); - }; + }, []); - if (!cruiseControlEnabled) { + if (cruiseControlEnabled === false) { return ( From 7fbbc52ffc8aaefce2fb6e703d1d6a880350e231 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Tue, 18 Aug 2026 11:41:39 -0400 Subject: [PATCH 23/25] Update node overview and rebalance documentation Assisted-by: IBM Bob Signed-off-by: Michael Edgar --- docs/sources/modules/con-brokers-page.adoc | 20 ++++++----- .../modules/proc-managing-rebalances.adoc | 35 ++++++++++--------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/docs/sources/modules/con-brokers-page.adoc b/docs/sources/modules/con-brokers-page.adoc index 3b7e54a6f..cd166aecd 100644 --- a/docs/sources/modules/con-brokers-page.adoc +++ b/docs/sources/modules/con-brokers-page.adoc @@ -7,7 +7,12 @@ The *Nodes* page lists all nodes created for a Kafka cluster, including nodes that perform broker, controller, or dual roles. You can filter the list by node pool, role (broker or controller), or status. -For broker nodes, partition distribution across the cluster is shown, including the number of partition leaders and followers. +The *Nodes* page also shows two charts below the node list: + +Node storage usage:: Shows total storage used and available per node. +Partition distribution:: Shows the balance of partition leaders and replicas across broker nodes. + +NOTE: Consider rebalancing if partition distribution is uneven to ensure efficient resource utilization. Broker status is shown as one of the following: @@ -21,17 +26,14 @@ Unknown:: The broker's state is unknown, possibly due to an unexpected error or If the broker has a rack ID, it identifies the rack or datacenter in which the broker resides. -Controller status is shown as one of the following, describing the controller’s role within the metadata quorum: +Controller status is shown as one of the following, describing the controller's role within the metadata quorum: -Quorum leader:: The controller is the active leader, coordinating cluster metadata updates and managing operations like partition reassignments and broker registrations. -Quorum follower:: The controller is a follower in the metadata quorum, passively replicating updates from the leader while maintaining a synchronized state. It is ready to take over as the leader if needed. -Quorum follower lagged:: The controller is a follower but has fallen behind the leader. It is not fully up to date with the latest metadata and may be ineligible for leader election until it catches up. +Quorum Leader:: The controller is the active leader, coordinating cluster metadata updates and managing operations like partition reassignments and broker registrations. +Quorum Follower:: The controller is a follower in the metadata quorum, passively replicating updates from the leader while maintaining a synchronized state. It is ready to take over as the leader if needed. +Quorum Follower Lagged:: The controller is a follower but has fallen behind the leader. It is not fully up to date with the latest metadata and may be ineligible for leader election until it catches up. Unknown:: The controller's state is unknown, possibly due to an unexpected error or failure. To view more information: -* Click on the right arrow (>) next to a node name to view more information about the node, including its hostname and disk usage. * Click on a broker node ID to view configuration properties. -* Click on the *Rebalance* tab to show any rebalances taking place on the cluster. - -NOTE: Consider rebalancing if partition distribution is uneven to ensure efficient resource utilization. \ No newline at end of file +* Click on the *Rebalance* tab to show any rebalances taking place on the cluster. \ No newline at end of file diff --git a/docs/sources/modules/proc-managing-rebalances.adoc b/docs/sources/modules/proc-managing-rebalances.adoc index 072274a2c..9e244b851 100644 --- a/docs/sources/modules/proc-managing-rebalances.adoc +++ b/docs/sources/modules/proc-managing-rebalances.adoc @@ -8,34 +8,36 @@ When you configure `KafkaRebalance` resources to generate optimization proposals The *Rebalance* tab presents a chronological list of `KafkaRebalance` resources from which you can manage the optimization proposals. Filter the list by name, status, or rebalance mode. -NOTE: Cruise Control must be enabled to run alongside the Kafka cluster in order to use the *Rebalance* tab. For more information on setting up and using Cruise Control to generate proposals, see the link:{BookURLDeploying}[Strimzi documentation^]. +NOTE: Cruise Control must be enabled to run alongside the Kafka cluster in order to use the *Rebalance* tab. +If Cruise Control is not enabled, the *Rebalance* tab shows an empty state with a link to the Strimzi documentation to get started. +For more information on setting up and using Cruise Control to generate proposals, see the link:{BookURLDeploying}[Strimzi documentation^]. .Procedure -. Log in to the Kafka cluster in the StreamsHub Console, then click *Kafka nodes*. +. Log in to the Kafka cluster in the StreamsHub Console, then click *Kafka nodes*. . Check the information on the *Rebalance* tab. + -For each rebalance, view the status and the time it was last updated. +For each rebalance, the list shows the rebalance name, status, data to move, partitions to move, leadership updates, and the time it was last updated. + -- .Rebalance status descriptions [cols="1m,1",options="header"] |=== |Status |Description -|New|Resource has not been observed by the operator before -|PendingProposal|Optimization proposal not generated +|New|Resource has not been observed by the operator before +|PendingProposal|Optimization proposal not generated |ProposalReady|Optimization proposal is ready for approval |Rebalancing|Rebalance in progress |Stopped|Rebalance stopped -|NotReady|Error ocurred with the rebalance +|NotReady|Error occurred with the rebalance |Ready|Rebalance complete |ReconciliationPaused|Rebalance is paused |=== -NOTE: The status of the `KafkaRebalance` resource changes to `ReconciliationPaused` when the `strimzi.io/pause-reconciliation` annotation is set to `true` in its configuration. +NOTE: The status of the `KafkaRebalance` resource changes to `ReconciliationPaused` when the `strimzi.io/pause-reconciliation` annotation is set to `true` in its configuration. -- -. Click on the right arrow (>) next to a rebalance name to view more information about the broker, including its rebalance mode, and whether auto-approval is enabled. +. Click on the right arrow (>) next to a rebalance name to view more information, including the rebalance mode and whether auto-approval is enabled. If the rebalance involved brokers being removed or added, they are also listed. + Optimization proposals can be generated in one of three modes: @@ -46,24 +48,25 @@ Optimization proposals can be generated in one of three modes: * `remove-brokers` is the mode used before removing brokers when scaling down a Kafka cluster. -- + -If auto-approval is enabled for a proposal, a successfully generated proposal goes straight into a cluster rebalance. +If auto-approval is enabled for a proposal, a successfully generated proposal goes straight into a cluster rebalance. + -Viewing optimization proposals:: Click on the name of a `KafkaRebalance` resource to view a generated optimization proposal. -An optimization proposal is a summary of proposed changes that would produce a more balanced Kafka cluster, with partition workloads distributed more evenly among the brokers. +Viewing optimization proposals:: Click on the name of a `KafkaRebalance` resource to open the rebalance detail page. +The detail page shows the rebalance metadata (name, namespace, mode, goals, auto-approval, status), a broker impact table, and an expandable proposal detail section. +The broker impact table shows the before and after state for each broker across storage, CPU, partition leaders, replicas, and network throughput metrics. + For more information on the properties shown on the proposal and what they mean, see the link:{BookURLDeploying}[Strimzi documentation^]. + -Managing rebalances:: Select the options icon (three vertical dots) and click on an option to manage a rebalance. +Managing rebalances:: Select the options icon (three vertical dots) from the rebalance list, or use the action buttons on the rebalance detail page, to manage a rebalance. + -- -* Click *Approve* to approve a proposal. -The rebalance outlined in the proposal is performed on the Kafka cluster. -* Click *Refresh* to generate a fresh optimization proposal. +* Click *Approve* to approve a proposal. +The rebalance outlined in the proposal is performed on the Kafka cluster. +* Click *Refresh proposal* to generate a fresh optimization proposal. If there has been a gap between generating a proposal and approving it, refresh the proposal so that the current state of the cluster is taken into account with a rebalance. * Click *Stop* to stop a rebalance. Rebalances can take a long time and may impact the performance of your cluster. Stopping a rebalance can help avoid performance issues and allow you to revert changes if needed. -- + -NOTE: The options available depend on the status of the `KafkaBalance` resource. +NOTE: The options available depend on the status of the `KafkaRebalance` resource. For example, it's not possible to approve an optimization proposal if it's not ready. \ No newline at end of file From f3352ff1a848291a7fe24a2e708e20f98cbf7ecd Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Mon, 24 Aug 2026 11:52:49 -0400 Subject: [PATCH 24/25] Remove "Broker" label from impact table rows Signed-off-by: Michael Edgar --- .../main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx b/api/src/main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx index 3223ec92a..74b211dd2 100644 --- a/api/src/main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx +++ b/api/src/main/webui/src/components/kafka/nodes/BrokerImpactTable.tsx @@ -423,7 +423,7 @@ export function BrokerImpactTable({ pagedRows.map((row) => ( - {t('rebalancing.broker', { b: row.brokerId })} + {row.brokerId} {activeGroups.map((g) => { const pctImpact = g.pctKey ? row.metrics[g.pctKey] : undefined; From c0e7eadfd025591a8338277e7ddbb7ed4c2f05ab Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Mon, 24 Aug 2026 12:50:20 -0400 Subject: [PATCH 25/25] Refine after-PATCH query invalidation for Kafka Rebalance Signed-off-by: Michael Edgar --- .../main/webui/src/api/hooks/useRebalances.ts | 24 ++++++++++++++++--- .../webui/src/api/hooks/useResourceList.ts | 6 ++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/api/src/main/webui/src/api/hooks/useRebalances.ts b/api/src/main/webui/src/api/hooks/useRebalances.ts index 0148918b0..6101f0c2c 100644 --- a/api/src/main/webui/src/api/hooks/useRebalances.ts +++ b/api/src/main/webui/src/api/hooks/useRebalances.ts @@ -8,7 +8,7 @@ import { RebalanceResponse, Rebalance, } from '../types'; -import { ResourceListParams, useResourceList } from './useResourceList'; +import { ResourceListParams, resourceListQueryKeyPrefix, useResourceList } from './useResourceList'; const REBALANCE_FIELDS = 'name,namespace,creationTimestamp,status,mode,brokers,optimizationResult,conditions'; const REBALANCE_DETAIL_FIELDS = `${REBALANCE_FIELDS},brokerCapacity,goals,optimizationProposal,sessionId`; @@ -85,9 +85,27 @@ export function usePatchRebalance(kafkaId: string) { }, }); }, - onSuccess: () => { + onSuccess: (_, { rebalanceId }) => { // Invalidate rebalances queries to refetch - queryClient.invalidateQueries({ queryKey: ['rebalances', kafkaId] }); + queryClient.invalidateQueries({ + predicate: (query) => { + const key = query.queryKey; + if (key.length > 1) { + const listKey = [ resourceListQueryKeyPrefix('kafkaRebalances'), `/api/kafkas/${kafkaId}/rebalances`]; + + if (key[0] === listKey[0] && key[1] === listKey[1]) { + return true; + } + + const singleKey = ['rebalance', kafkaId, rebalanceId]; + + if (key.length === singleKey.length && singleKey.every((v, i) => v === key[i])) { + return true; + } + } + return false; + } + }); }, }); } \ No newline at end of file diff --git a/api/src/main/webui/src/api/hooks/useResourceList.ts b/api/src/main/webui/src/api/hooks/useResourceList.ts index 78981a08a..50ac7ebc2 100644 --- a/api/src/main/webui/src/api/hooks/useResourceList.ts +++ b/api/src/main/webui/src/api/hooks/useResourceList.ts @@ -73,6 +73,10 @@ function updatePageParams(page: ResourceListPageParams, searchParams: URLSearchP } } +export function resourceListQueryKeyPrefix(resourceType: string): string { + return resourceType + '-resource-list-query'; +} + export function useResourceList( resourceType: string, path: string, @@ -80,7 +84,7 @@ export function useResourceList