From f5e0dbb0aeb97a926488631b852c2a801502cee9 Mon Sep 17 00:00:00 2001 From: Pavel Makarichev Date: Sat, 8 Nov 2025 18:08:05 +0300 Subject: [PATCH] FE: Recalculation connectors stat on client/backed filter change --- .../List/ConnectorsTable/ConnectorsTable.tsx | 9 ++- .../src/components/Connect/List/ListPage.tsx | 7 ++- .../Connect/List/Statistics/Statistics.tsx | 14 ++--- .../Statistics/__tests__/Statistics.spec.tsx | 7 ++- .../Statistics/models/computeStatistics.ts | 2 +- .../Connect/List/__tests__/List.spec.tsx | 5 +- .../model/FilteredConnectorsProvider.tsx | 58 +++++++++++++++++++ .../src/components/common/NewTable/Table.tsx | 13 ++++- 8 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 frontend/src/components/Connect/model/FilteredConnectorsProvider.tsx diff --git a/frontend/src/components/Connect/List/ConnectorsTable/ConnectorsTable.tsx b/frontend/src/components/Connect/List/ConnectorsTable/ConnectorsTable.tsx index 44014c2b2..4a2e45382 100644 --- a/frontend/src/components/Connect/List/ConnectorsTable/ConnectorsTable.tsx +++ b/frontend/src/components/Connect/List/ConnectorsTable/ConnectorsTable.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { FullConnectorInfo } from 'generated-sources'; import Table from 'components/common/NewTable'; import { useLocalStoragePersister } from 'components/common/NewTable/ColumnResizer/lib'; import { useQueryPersister } from 'components/common/NewTable/ColumnFilter'; import { VisibilityState } from '@tanstack/react-table'; +import { useFilteredConnectorsDispatch } from 'components/Connect/model/FilteredConnectorsProvider'; import { connectorsColumns } from './connectorsColumns/columns'; @@ -21,11 +22,16 @@ export const ConnectorsTable = ({ columnSizingPersistKey = 'KafkaConnect', columnVisibility, }: ConnectorsTableProps) => { + const dispath = useFilteredConnectorsDispatch(); const filterPersister = useQueryPersister(connectorsColumns); const columnSizingPersister = useLocalStoragePersister( columnSizingPersistKey ); + const onFilterRows = useCallback((rows: FullConnectorInfo[]) => { + dispath({ type: 'updated', connectors: rows }); + }, []); + return ( ); }; diff --git a/frontend/src/components/Connect/List/ListPage.tsx b/frontend/src/components/Connect/List/ListPage.tsx index 39d87e1de..dab3fe3f1 100644 --- a/frontend/src/components/Connect/List/ListPage.tsx +++ b/frontend/src/components/Connect/List/ListPage.tsx @@ -8,6 +8,7 @@ import { useSearchParams } from 'react-router-dom'; import useFts from 'components/common/Fts/useFts'; import Fts from 'components/common/Fts/Fts'; import { FullConnectorInfo } from 'generated-sources'; +import { FilteredConnectorsProvider } from 'components/Connect/model/FilteredConnectorsProvider'; import * as S from './ListPage.styled'; import List from './List'; @@ -26,8 +27,8 @@ const ListPage: React.FC = () => { ); return ( - <> - + + { }> - + ); }; diff --git a/frontend/src/components/Connect/List/Statistics/Statistics.tsx b/frontend/src/components/Connect/List/Statistics/Statistics.tsx index f6b58bcb1..13463d6d0 100644 --- a/frontend/src/components/Connect/List/Statistics/Statistics.tsx +++ b/frontend/src/components/Connect/List/Statistics/Statistics.tsx @@ -1,18 +1,18 @@ import React, { FC, useMemo } from 'react'; import * as Statistics from 'components/common/Statistics'; -import { FullConnectorInfo } from 'generated-sources'; +import { useFilteredConnectors } from 'components/Connect/model/FilteredConnectorsProvider'; import { computeStatistics } from './models/computeStatistics'; interface ConnectorsStatisticsProps { - connectors: FullConnectorInfo[]; isLoading: boolean; } -const ConnectorsStatistics: FC = ({ - connectors, - isLoading, -}) => { - const statistics = useMemo(() => computeStatistics(connectors), [connectors]); +const ConnectorsStatistics: FC = ({ isLoading }) => { + const connectors = useFilteredConnectors(); + + const statistics = useMemo(() => { + return computeStatistics(connectors); + }, [connectors]); return ( diff --git a/frontend/src/components/Connect/List/Statistics/__tests__/Statistics.spec.tsx b/frontend/src/components/Connect/List/Statistics/__tests__/Statistics.spec.tsx index b4056d7e4..a98b7a160 100644 --- a/frontend/src/components/Connect/List/Statistics/__tests__/Statistics.spec.tsx +++ b/frontend/src/components/Connect/List/Statistics/__tests__/Statistics.spec.tsx @@ -5,6 +5,7 @@ import ConnectorsStatistics from 'components/Connect/List/Statistics/Statistics' import { useConnectors } from 'lib/hooks/api/kafkaConnect'; import { connectors } from 'lib/fixtures/kafkaConnect'; import { FullConnectorInfo } from 'generated-sources'; +import { FilteredConnectorsProvider } from 'components/Connect/model/FilteredConnectorsProvider'; jest.mock('lib/hooks/api/kafkaConnect'); jest.mock('lib/hooks/useAppParams', () => ({ @@ -25,7 +26,11 @@ describe('Kafka Connect Connectors Statistics', () => { function renderComponent({ data = [], isLoading }: RenderComponentProps) { // eslint-disable-next-line @typescript-eslint/no-explicit-any useConnectorsMock.mockReturnValue({ data, isLoading } as any); - render(); + render( + + + + ); } describe('when data loading', () => { diff --git a/frontend/src/components/Connect/List/Statistics/models/computeStatistics.ts b/frontend/src/components/Connect/List/Statistics/models/computeStatistics.ts index ad3b85a1d..0311994bf 100644 --- a/frontend/src/components/Connect/List/Statistics/models/computeStatistics.ts +++ b/frontend/src/components/Connect/List/Statistics/models/computeStatistics.ts @@ -1,6 +1,6 @@ import { ConnectorState, FullConnectorInfo } from 'generated-sources'; -interface Statistic { +export interface Statistic { connectorsCount: number; failedConnectorsCount: number; tasksCount: number; diff --git a/frontend/src/components/Connect/List/__tests__/List.spec.tsx b/frontend/src/components/Connect/List/__tests__/List.spec.tsx index 679c4299e..a489f2fb7 100644 --- a/frontend/src/components/Connect/List/__tests__/List.spec.tsx +++ b/frontend/src/components/Connect/List/__tests__/List.spec.tsx @@ -16,6 +16,7 @@ import { useUpdateConnectorState, } from 'lib/hooks/api/kafkaConnect'; import { FullConnectorInfo } from 'generated-sources'; +import { FilteredConnectorsProvider } from 'components/Connect/model/FilteredConnectorsProvider'; const mockedUsedNavigate = jest.fn(); const mockDelete = jest.fn(); @@ -42,7 +43,9 @@ const renderComponent = ( render( - + + + , { initialEntries: [clusterConnectorsPath(clusterName)] } diff --git a/frontend/src/components/Connect/model/FilteredConnectorsProvider.tsx b/frontend/src/components/Connect/model/FilteredConnectorsProvider.tsx new file mode 100644 index 000000000..1952e9ab1 --- /dev/null +++ b/frontend/src/components/Connect/model/FilteredConnectorsProvider.tsx @@ -0,0 +1,58 @@ +import { FullConnectorInfo } from 'generated-sources'; +import React, { + createContext, + Dispatch, + FC, + PropsWithChildren, + useContext, + useReducer, +} from 'react'; + +const initialConnectors: FullConnectorInfo[] = []; +type Action = { type: 'updated'; connectors: FullConnectorInfo[] }; + +function reducer(connectors: FullConnectorInfo[], action: Action) { + switch (action.type) { + case 'updated': { + return action.connectors; + } + default: { + throw Error(`Unknown action: ${action.type}`); + } + } +} + +const ConnectorsContext = createContext(null); +const ConnectorsDispatchContext = createContext | null>(null); + +export const FilteredConnectorsProvider: FC< + PropsWithChildren<{ initialData?: FullConnectorInfo[] }> +> = ({ children, initialData }) => { + const [connectors, dispatch] = useReducer( + reducer, + initialData ?? initialConnectors + ); + return ( + + + {children} + + + ); +}; + +export const useFilteredConnectors = () => { + const context = useContext(ConnectorsContext); + if (!context) { + throw new Error('useCounter must be used within a CounterProvider'); + } + return context; +}; + +export const useFilteredConnectorsDispatch = () => { + const context = useContext(ConnectorsDispatchContext); + if (!context) { + throw new Error('useCounter must be used within a CounterProvider'); + } + return context; +}; diff --git a/frontend/src/components/common/NewTable/Table.tsx b/frontend/src/components/common/NewTable/Table.tsx index b77b0e758..665f141c6 100644 --- a/frontend/src/components/common/NewTable/Table.tsx +++ b/frontend/src/components/common/NewTable/Table.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import type { ColumnDef, ColumnFiltersState, @@ -77,6 +77,8 @@ export interface TableProps { onMouseLeave?: () => void; setRowId?: (originalRow: TData) => string; + + onFilterRows?: (rows: TData[]) => void; } type UpdaterFn = (previousState: T) => T; @@ -163,6 +165,7 @@ function Table({ filterPersister, resetPaginationOnFilter = true, columnVisibility, + onFilterRows, }: TableProps) { const [searchParams, setSearchParams] = useSearchParams(); const location = useLocation(); @@ -273,6 +276,14 @@ function Table({ return colSizes; }, [table.getState().columnSizingInfo, table.getState().columnSizing]); + useEffect(() => { + if (onFilterRows) { + const filteredRows = table.getFilteredRowModel().rows; + const filteredData = filteredRows.map((row) => row.original); + onFilterRows(filteredData); + } + }, [table.getState().columnFilters]); + const handleRowClick = (row: Row) => (e: React.MouseEvent) => { // If row selection is enabled do not handle row click. if (enableRowSelection) return undefined;