diff --git a/src/components/PaginatedTable/PaginatedTable.tsx b/src/components/PaginatedTable/PaginatedTable.tsx index 01dc6a998e..b0e357d5e2 100644 --- a/src/components/PaginatedTable/PaginatedTable.tsx +++ b/src/components/PaginatedTable/PaginatedTable.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import {TableWithControlsLayout} from '../TableWithControlsLayout/TableWithControlsLayout'; - +import {usePaginatedTableState} from './PaginatedTableContext'; import {TableChunk} from './TableChunk'; import {TableHead} from './TableHead'; import {DEFAULT_TABLE_ROW_HEIGHT} from './constants'; @@ -12,7 +11,6 @@ import type { GetRowClassName, HandleTableColumnsResize, PaginatedTableData, - RenderControls, RenderEmptyDataMessage, RenderErrorMessage, SortParams, @@ -30,10 +28,9 @@ export interface PaginatedTableProps { columns: Column[]; getRowClassName?: GetRowClassName; rowHeight?: number; - parentRef: React.RefObject; + scrollContainerRef: React.RefObject; initialSortParams?: SortParams; onColumnsResize?: HandleTableColumnsResize; - renderControls?: RenderControls; renderEmptyDataMessage?: RenderEmptyDataMessage; renderErrorMessage?: RenderErrorMessage; containerClassName?: string; @@ -52,28 +49,43 @@ export const PaginatedTable = ({ columns, getRowClassName, rowHeight = DEFAULT_TABLE_ROW_HEIGHT, - parentRef, + scrollContainerRef, initialSortParams, onColumnsResize, - renderControls, renderErrorMessage, renderEmptyDataMessage, containerClassName, onDataFetched, keepCache = true, }: PaginatedTableProps) => { - const initialTotal = initialEntitiesCount || 0; - const initialFound = initialEntitiesCount || 1; + // Get state and setters from context + const {tableState, setSortParams, setTotalEntities, setFoundEntities, setIsInitialLoad} = + usePaginatedTableState(); + + const {sortParams, foundEntities} = tableState; + + // Initialize state with props if available + React.useEffect(() => { + if (initialSortParams) { + setSortParams(initialSortParams); + } - const [sortParams, setSortParams] = React.useState(initialSortParams); - const [totalEntities, setTotalEntities] = React.useState(initialTotal); - const [foundEntities, setFoundEntities] = React.useState(initialFound); - const [isInitialLoad, setIsInitialLoad] = React.useState(true); + if (initialEntitiesCount) { + setTotalEntities(initialEntitiesCount); + setFoundEntities(initialEntitiesCount); + } + }, [ + setSortParams, + setTotalEntities, + setFoundEntities, + initialSortParams, + initialEntitiesCount, + ]); const tableRef = React.useRef(null); const activeChunks = useScrollBasedChunks({ - parentRef, + scrollContainerRef, tableRef, totalItems: foundEntities, rowHeight, @@ -105,18 +117,18 @@ export const PaginatedTable = ({ onDataFetched?.(data); } }, - [onDataFetched], + [onDataFetched, setFoundEntities, setIsInitialLoad, setTotalEntities], ); - // reset table on filters change + // Reset table on filters change React.useLayoutEffect(() => { - setTotalEntities(initialTotal); - setFoundEntities(initialFound); + const defaultTotal = initialEntitiesCount || 0; + const defaultFound = initialEntitiesCount || 1; + + setTotalEntities(defaultTotal); + setFoundEntities(defaultFound); setIsInitialLoad(true); - if (parentRef?.current) { - parentRef.current.scrollTo(0, 0); - } - }, [rawFilters, initialFound, initialTotal, parentRef]); + }, [initialEntitiesCount, setTotalEntities, setFoundEntities, setIsInitialLoad]); const renderChunks = () => { return activeChunks.map((isActive, index) => ( @@ -148,24 +160,9 @@ export const PaginatedTable = ({ ); - const renderContent = () => { - if (renderControls) { - return ( - - - {renderControls({inited: !isInitialLoad, totalEntities, foundEntities})} - - {renderTable()} - - ); - } - - return renderTable(); - }; - return (
- {renderContent()} + {renderTable()}
); }; diff --git a/src/components/PaginatedTable/PaginatedTableContext.tsx b/src/components/PaginatedTable/PaginatedTableContext.tsx new file mode 100644 index 0000000000..b7339f9cca --- /dev/null +++ b/src/components/PaginatedTable/PaginatedTableContext.tsx @@ -0,0 +1,98 @@ +import React from 'react'; + +import type {PaginatedTableState} from './types'; + +// Default state for the table +const defaultTableState: PaginatedTableState = { + sortParams: undefined, + totalEntities: 0, + foundEntities: 0, + isInitialLoad: true, +}; + +// Context type definition +interface PaginatedTableStateContextType { + // State + tableState: PaginatedTableState; + + // Granular setters + setSortParams: (params: PaginatedTableState['sortParams']) => void; + setTotalEntities: (total: number) => void; + setFoundEntities: (found: number) => void; + setIsInitialLoad: (isInitial: boolean) => void; +} + +// Creating the context with default values +export const PaginatedTableStateContext = React.createContext({ + tableState: defaultTableState, + setSortParams: () => undefined, + setTotalEntities: () => undefined, + setFoundEntities: () => undefined, + setIsInitialLoad: () => undefined, +}); + +// Provider component props +interface PaginatedTableStateProviderProps { + children: React.ReactNode; + initialState?: Partial; +} + +// Provider component +export const PaginatedTableProvider = ({ + children, + initialState = {}, +}: PaginatedTableStateProviderProps) => { + // Use individual state variables for each field + const [sortParams, setSortParams] = React.useState( + initialState.sortParams ?? defaultTableState.sortParams, + ); + const [totalEntities, setTotalEntities] = React.useState( + initialState.totalEntities ?? defaultTableState.totalEntities, + ); + const [foundEntities, setFoundEntities] = React.useState( + initialState.foundEntities ?? defaultTableState.foundEntities, + ); + const [isInitialLoad, setIsInitialLoad] = React.useState( + initialState.isInitialLoad ?? defaultTableState.isInitialLoad, + ); + + // Construct tableState from individual state variables + const tableState = React.useMemo( + () => ({ + sortParams, + totalEntities, + foundEntities, + isInitialLoad, + }), + [sortParams, totalEntities, foundEntities, isInitialLoad], + ); + + // Create the context value with the constructed tableState and direct setters + const contextValue = React.useMemo( + () => ({ + tableState, + setSortParams, + setTotalEntities, + setFoundEntities, + setIsInitialLoad, + }), + [tableState, setSortParams, setTotalEntities, setFoundEntities, setIsInitialLoad], + ); + + return ( + + {children} + + ); +}; + +// Custom hook for consuming the context +export const usePaginatedTableState = () => { + const context = React.useContext(PaginatedTableStateContext); + + if (context === undefined) { + throw new Error('usePaginatedTableState must be used within a PaginatedTableStateProvider'); + } + + return context; +}; diff --git a/src/components/PaginatedTable/PaginatedTableWithLayout.tsx b/src/components/PaginatedTable/PaginatedTableWithLayout.tsx new file mode 100644 index 0000000000..0e5053d6cd --- /dev/null +++ b/src/components/PaginatedTable/PaginatedTableWithLayout.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import {TableWithControlsLayout} from '../TableWithControlsLayout/TableWithControlsLayout'; +import type {TableProps} from '../TableWithControlsLayout/TableWithControlsLayout'; + +import {PaginatedTableProvider} from './PaginatedTableContext'; +import type {PaginatedTableState} from './types'; + +export interface PaginatedTableWithLayoutProps { + controls: React.ReactNode; + table: React.ReactNode; + tableProps?: TableProps; + error?: React.ReactNode; + initialState?: Partial; + fullHeight?: boolean; +} + +export const PaginatedTableWithLayout = ({ + controls, + table, + tableProps, + error, + initialState, + fullHeight = true, +}: PaginatedTableWithLayoutProps) => ( + + + {controls} + {error} + + {table} + + + +); diff --git a/src/components/PaginatedTable/types.ts b/src/components/PaginatedTable/types.ts index cd15eb1887..63031186f9 100644 --- a/src/components/PaginatedTable/types.ts +++ b/src/components/PaginatedTable/types.ts @@ -58,13 +58,14 @@ export type FetchData = ( export type OnError = (error?: IResponseError) => void; -interface ControlsParams { +export interface PaginatedTableState { + sortParams?: SortParams; totalEntities: number; foundEntities: number; - inited: boolean; + isInitialLoad: boolean; } -export type RenderControls = (params: ControlsParams) => React.ReactNode; +export type RenderControls = () => React.ReactNode; export type RenderEmptyDataMessage = () => React.ReactNode; export type RenderErrorMessage = (error: IResponseError) => React.ReactNode; diff --git a/src/components/PaginatedTable/useScrollBasedChunks.ts b/src/components/PaginatedTable/useScrollBasedChunks.ts index 1abf3de008..b05b5d4587 100644 --- a/src/components/PaginatedTable/useScrollBasedChunks.ts +++ b/src/components/PaginatedTable/useScrollBasedChunks.ts @@ -3,7 +3,7 @@ import React from 'react'; import {calculateElementOffsetTop, rafThrottle} from './utils'; interface UseScrollBasedChunksProps { - parentRef: React.RefObject; + scrollContainerRef: React.RefObject; tableRef: React.RefObject; totalItems: number; rowHeight: number; @@ -14,7 +14,7 @@ interface UseScrollBasedChunksProps { const DEFAULT_OVERSCAN_COUNT = 1; export const useScrollBasedChunks = ({ - parentRef, + scrollContainerRef, tableRef, totalItems, rowHeight, @@ -32,7 +32,7 @@ export const useScrollBasedChunks = ({ ); const calculateVisibleRange = React.useCallback(() => { - const container = parentRef?.current; + const container = scrollContainerRef?.current; const table = tableRef.current; if (!container || !table) { return null; @@ -49,7 +49,7 @@ export const useScrollBasedChunks = ({ Math.max(chunksCount - 1, 0), ); return {start, end}; - }, [parentRef, tableRef, rowHeight, chunkSize, overscanCount, chunksCount]); + }, [scrollContainerRef, tableRef, rowHeight, chunkSize, overscanCount, chunksCount]); const updateVisibleChunks = React.useCallback(() => { const newRange = calculateVisibleRange(); @@ -80,7 +80,7 @@ export const useScrollBasedChunks = ({ }, [updateVisibleChunks]); React.useEffect(() => { - const container = parentRef?.current; + const container = scrollContainerRef?.current; if (!container) { return undefined; } @@ -91,7 +91,7 @@ export const useScrollBasedChunks = ({ return () => { container.removeEventListener('scroll', throttledHandleScroll); }; - }, [handleScroll, parentRef]); + }, [handleScroll, scrollContainerRef]); return React.useMemo(() => { // boolean array that represents active chunks diff --git a/src/components/TableWithControlsLayout/TableWithControlsLayout.scss b/src/components/TableWithControlsLayout/TableWithControlsLayout.scss index 69d71bebdf..168698bfab 100644 --- a/src/components/TableWithControlsLayout/TableWithControlsLayout.scss +++ b/src/components/TableWithControlsLayout/TableWithControlsLayout.scss @@ -1,13 +1,18 @@ @use '../../styles/mixins.scss'; .ydb-table-with-controls-layout { - --data-table-sticky-top-offset: 62px; + // Total height of all fixed elements above table for sticky header positioning + --data-table-sticky-header-offset: 62px; display: inline-block; box-sizing: border-box; min-width: 100%; + &_full-height { + min-height: calc(100% - var(--sticky-tabs-height, 0px)); + } + &__controls-wrapper { z-index: 3; @@ -33,11 +38,11 @@ } .ydb-paginated-table__head { - top: var(--data-table-sticky-top-offset, 62px); + top: var(--data-table-sticky-header-offset, 62px); } .data-table__sticky_moving { // Place table head right after controls - top: var(--data-table-sticky-top-offset, 62px) !important; + top: var(--data-table-sticky-header-offset, 62px) !important; } } diff --git a/src/components/TableWithControlsLayout/TableWithControlsLayout.tsx b/src/components/TableWithControlsLayout/TableWithControlsLayout.tsx index 23dd5bbcd3..6e644bad91 100644 --- a/src/components/TableWithControlsLayout/TableWithControlsLayout.tsx +++ b/src/components/TableWithControlsLayout/TableWithControlsLayout.tsx @@ -1,27 +1,35 @@ +import React from 'react'; + import {Flex} from '@gravity-ui/uikit'; import {cn} from '../../utils/cn'; import {TableSkeleton} from '../TableSkeleton/TableSkeleton'; +import {useTableScroll} from './useTableScroll'; + import './TableWithControlsLayout.scss'; const b = cn('ydb-table-with-controls-layout'); interface TableWithControlsLayoutItemProps { - children: React.ReactNode; + children?: React.ReactNode; renderExtraControls?: () => React.ReactNode; className?: string; + fullHeight?: boolean; } -interface TableProps extends TableWithControlsLayoutItemProps { +export interface TableProps extends TableWithControlsLayoutItemProps { loading?: boolean; + scrollContainerRef?: React.RefObject; + scrollDependencies?: any[]; } export const TableWithControlsLayout = ({ children, className, + fullHeight, }: TableWithControlsLayoutItemProps) => { - return
{children}
; + return
{children}
; }; TableWithControlsLayout.Controls = function TableControls({ @@ -42,10 +50,30 @@ TableWithControlsLayout.Controls = function TableControls({ ); }; -TableWithControlsLayout.Table = function Table({children, loading, className}: TableProps) { +TableWithControlsLayout.Table = function Table({ + children, + loading, + className, + scrollContainerRef, + scrollDependencies = [], +}: TableProps) { + // Create an internal ref for the table container + const tableContainerRef = React.useRef(null); + + // Use the internal ref for scrolling + useTableScroll({ + tableContainerRef, + scrollContainerRef, + dependencies: scrollDependencies, + }); + if (loading) { return ; } - return
{children}
; + return ( +
+ {children} +
+ ); }; diff --git a/src/components/TableWithControlsLayout/useTableScroll.ts b/src/components/TableWithControlsLayout/useTableScroll.ts new file mode 100644 index 0000000000..2f168b2020 --- /dev/null +++ b/src/components/TableWithControlsLayout/useTableScroll.ts @@ -0,0 +1,121 @@ +import React from 'react'; + +interface UseTableScrollProps { + /** + * Reference to the table container element. This is the element that contains + * the table and whose position will be adjusted. + */ + tableContainerRef?: React.RefObject; + + /** + * Reference to the scrollable container element. This is the parent element + * that has scrolling capabilities. + */ + scrollContainerRef?: React.RefObject; + + /** + * Array of values that, when changed, will trigger the scroll adjustment. + * Include all values that might affect table height or position to ensure + * proper scroll adjustment (e.g., filters, sorting, pagination state). + */ + dependencies?: unknown[]; +} + +/** + * A hook that manages scrolling behavior for tables within a scrollable container. + * + * This hook ensures proper positioning of tables when their content changes, + * particularly when using sticky headers, by automatically adjusting the scroll position. + * It reads the `--data-table-sticky-header-offset` CSS variable from the table container + * to determine the sticky header offset for correct positioning. + * + * The scroll adjustment is triggered whenever any of the dependencies change, + * but is skipped on the first render to prevent unwanted initial scrolling. + * + * + * @param props - The hook parameters + * @returns An object containing the handleTableScroll function that can be called manually + */ +export const useTableScroll = ({ + tableContainerRef, + scrollContainerRef, + dependencies = [], +}: UseTableScrollProps) => { + /** + * Retrieves the sticky header offset from CSS variables. + * + * Reads the `--data-table-sticky-header-offset` CSS variable from the table container + * element and converts it to a number. This value is used to adjust the scroll position + * to account for sticky headers. + * + * @returns The sticky header offset in pixels, or 0 if not defined + */ + const getStickyTopOffset = React.useCallback(() => { + // Try to get the variable from parent elements + if (tableContainerRef?.current) { + const computedStyle = window.getComputedStyle(tableContainerRef.current); + // Read the sticky header offset variable for correct scroll positioning + const stickyTopOffset = computedStyle.getPropertyValue( + '--data-table-sticky-header-offset', + ); + + return stickyTopOffset ? parseInt(stickyTopOffset, 10) : 0; + } + return 0; + }, [tableContainerRef]); + + /** + * Adjusts the scroll position of the container to properly position the table. + * + * This function calculates the correct scroll position based on: + * - The relative position of the table within the scroll container + * - The sticky header offset (if any) + * + * It only adjusts the scroll position if the table would be partially hidden + * behind the sticky header. + */ + const handleTableScroll = React.useCallback(() => { + // Only proceed if both refs and their current properties are available + if (tableContainerRef?.current && scrollContainerRef?.current) { + // Get the sticky top offset value + const stickyTopOffset = getStickyTopOffset(); + + // Scroll the parent container to the position of the table container + const tableRect = tableContainerRef.current.getBoundingClientRect(); + const scrollContainerRect = scrollContainerRef.current.getBoundingClientRect(); + const scrollTop = + tableRect.top - scrollContainerRect.top + scrollContainerRef.current.scrollTop; + if (tableRect.top - stickyTopOffset < scrollContainerRect.top) { + // Adjust scroll position to account for sticky offset + scrollContainerRef.current.scrollTo(0, scrollTop - stickyTopOffset); + } + } + }, [scrollContainerRef, tableContainerRef, getStickyTopOffset]); + + /** + * Triggers the scroll adjustment when dependencies change. + * + * Uses useLayoutEffect to adjust the scroll position before browser paint, + * preventing visual jumps. The adjustment is only performed if both refs + * are available. + */ + React.useLayoutEffect(() => { + // Only proceed if both refs are available + if (!tableContainerRef || !scrollContainerRef) { + return; + } + + // Only scroll on subsequent renders when dependencies change + handleTableScroll(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [handleTableScroll, tableContainerRef, scrollContainerRef, ...dependencies]); + + return { + /** + * Function to manually trigger the table scroll adjustment. + * This can be useful in cases where you need to adjust the scroll + * position outside of the dependency changes. + */ + handleTableScroll, + }; +}; diff --git a/src/containers/Cluster/Cluster.scss b/src/containers/Cluster/Cluster.scss index 97fd7848b5..95ea6b7228 100644 --- a/src/containers/Cluster/Cluster.scss +++ b/src/containers/Cluster/Cluster.scss @@ -2,6 +2,7 @@ .ydb-cluster { --cluster-side-padding: var(--g-spacing-5); + --sticky-tabs-height: 40px; position: relative; overflow: auto; @@ -22,6 +23,7 @@ &__content { width: calc(100% - var(--cluster-side-padding)); + min-height: calc(100% - var(--sticky-tabs-height, 0px)); //allows controls of TableWithControlsLayout to stick properly transform: translateX(var(--cluster-side-padding)); } @@ -75,6 +77,7 @@ } .ydb-table-with-controls-layout { - --data-table-sticky-top-offset: 102px; + // Total height of all fixed elements above table for sticky header positioning + --data-table-sticky-header-offset: 102px; } } diff --git a/src/containers/Cluster/Cluster.tsx b/src/containers/Cluster/Cluster.tsx index 096db82212..6d85f748ae 100644 --- a/src/containers/Cluster/Cluster.tsx +++ b/src/containers/Cluster/Cluster.tsx @@ -167,7 +167,11 @@ export function Cluster({ .pathname } > - + - + - + - + ; + return ( + + ); } case 'structure': { diff --git a/src/containers/Nodes/Nodes.tsx b/src/containers/Nodes/Nodes.tsx index 93c26d04db..6b0fb97776 100644 --- a/src/containers/Nodes/Nodes.tsx +++ b/src/containers/Nodes/Nodes.tsx @@ -1,32 +1,14 @@ import React from 'react'; -import {ResponseError} from '../../components/Errors/ResponseError'; -import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper'; -import type {Column, RenderControls} from '../../components/PaginatedTable'; -import {TableWithControlsLayout} from '../../components/TableWithControlsLayout/TableWithControlsLayout'; -import { - NODES_COLUMNS_TITLES, - isMonitoringUserNodesColumn, -} from '../../components/nodesColumns/constants'; +import type {Column} from '../../components/PaginatedTable'; +import {isMonitoringUserNodesColumn} from '../../components/nodesColumns/constants'; import type {NodesColumnId} from '../../components/nodesColumns/constants'; -import { - useCapabilitiesLoaded, - useViewerNodesHandlerHasGrouping, -} from '../../store/reducers/capabilities/hooks'; -import {nodesApi} from '../../store/reducers/nodes/nodes'; import type {NodesPreparedEntity} from '../../store/reducers/nodes/types'; -import {useProblemFilter} from '../../store/reducers/settings/hooks'; import type {AdditionalNodesProps} from '../../types/additionalProps'; import type {NodesGroupByField} from '../../types/api/nodes'; -import {useAutoRefreshInterval} from '../../utils/hooks'; import {useIsUserAllowedToMakeChanges} from '../../utils/hooks/useIsUserAllowedToMakeChanges'; -import {useSelectedColumns} from '../../utils/hooks/useSelectedColumns'; -import {NodesUptimeFilterValues} from '../../utils/nodes'; -import {TableGroup} from '../Storage/TableGroup/TableGroup'; -import {useExpandedGroups} from '../Storage/TableGroup/useExpandedTableGroups'; -import {NodesControls} from './NodesControls/NodesControls'; -import {NodesTable} from './NodesTable'; +import {PaginatedNodes} from './PaginatedNodes'; import {getNodesColumns} from './columns/columns'; import { ALL_NODES_GROUP_BY_PARAMS, @@ -34,20 +16,15 @@ import { NODES_TABLE_SELECTED_COLUMNS_LS_KEY, REQUIRED_NODES_COLUMNS, } from './columns/constants'; -import i18n from './i18n'; -import {b} from './shared'; -import {useNodesPageQueryParams} from './useNodesPageQueryParams'; import './Nodes.scss'; export interface NodesProps { path?: string; database?: string; - parentRef: React.RefObject; + scrollContainerRef: React.RefObject; additionalNodesProps?: AdditionalNodesProps; - withPeerRoleFilter?: boolean; - columns?: Column[]; defaultColumnsIds?: NodesColumnId[]; requiredColumnsIds?: NodesColumnId[]; @@ -58,7 +35,7 @@ export interface NodesProps { export function Nodes({ path, database, - parentRef, + scrollContainerRef, additionalNodesProps, withPeerRoleFilter, columns = getNodesColumns({database, getNodeRef: additionalNodesProps?.getNodeRef}), @@ -67,12 +44,6 @@ export function Nodes({ selectedColumnsKey = NODES_TABLE_SELECTED_COLUMNS_LS_KEY, groupByParams = ALL_NODES_GROUP_BY_PARAMS, }: NodesProps) { - const {uptimeFilter, groupByParam, handleUptimeFilterChange} = - useNodesPageQueryParams(groupByParams); - const {problemFilter, handleProblemFilterChange} = useProblemFilter(); - - const capabilitiesLoaded = useCapabilitiesLoaded(); - const viewerNodesHandlerHasGrouping = useViewerNodesHandlerHasGrouping(); const isUserAllowedToMakeChanges = useIsUserAllowedToMakeChanges(); const preparedColumns = React.useMemo(() => { @@ -82,228 +53,17 @@ export function Nodes({ return columns.filter((column) => !isMonitoringUserNodesColumn(column.name)); }, [columns, isUserAllowedToMakeChanges]); - // Other filters do not fit with grouping - // Reset them if grouping available - React.useEffect(() => { - if ( - viewerNodesHandlerHasGrouping && - (problemFilter !== 'All' || uptimeFilter !== NodesUptimeFilterValues.All) - ) { - handleProblemFilterChange('All'); - handleUptimeFilterChange(NodesUptimeFilterValues.All); - } - }, [ - handleProblemFilterChange, - handleUptimeFilterChange, - problemFilter, - uptimeFilter, - viewerNodesHandlerHasGrouping, - ]); - - const renderContent = () => { - if (viewerNodesHandlerHasGrouping && groupByParam) { - return ( - - ); - } - - return ( - - ); - }; - - return {renderContent()}; -} - -interface NodesComponentProps { - path?: string; - database?: string; - parentRef: React.RefObject; - - withPeerRoleFilter?: boolean; - - columns: Column[]; - defaultColumnsIds: NodesColumnId[]; - requiredColumnsIds: NodesColumnId[]; - selectedColumnsKey: string; - groupByParams: NodesGroupByField[]; -} - -function NodesComponent({ - path, - database, - parentRef, - withPeerRoleFilter, - columns, - defaultColumnsIds, - requiredColumnsIds, - selectedColumnsKey, - groupByParams, -}: NodesComponentProps) { - const {searchValue, uptimeFilter, peerRoleFilter} = useNodesPageQueryParams(groupByParams); - const {problemFilter} = useProblemFilter(); - const viewerNodesHandlerHasGrouping = useViewerNodesHandlerHasGrouping(); - - const {columnsToShow, columnsToSelect, setColumns} = useSelectedColumns( - columns, - selectedColumnsKey, - NODES_COLUMNS_TITLES, - defaultColumnsIds, - requiredColumnsIds, - ); - - const renderControls: RenderControls = ({totalEntities, foundEntities, inited}) => { - return ( - - ); - }; - return ( - ); } - -function GroupedNodesComponent({ - path, - database, - parentRef, - withPeerRoleFilter, - columns, - defaultColumnsIds, - requiredColumnsIds, - selectedColumnsKey, - groupByParams, -}: NodesComponentProps) { - const {searchValue, peerRoleFilter, groupByParam} = useNodesPageQueryParams(groupByParams); - const [autoRefreshInterval] = useAutoRefreshInterval(); - - const {columnsToShow, columnsToSelect, setColumns} = useSelectedColumns( - columns, - selectedColumnsKey, - NODES_COLUMNS_TITLES, - defaultColumnsIds, - requiredColumnsIds, - ); - - const {currentData, isFetching, error} = nodesApi.useGetNodesQuery( - { - path, - database, - filter: searchValue, - filter_peer_role: peerRoleFilter, - group: groupByParam, - limit: 0, - }, - { - pollingInterval: autoRefreshInterval, - }, - ); - - const isLoading = currentData === undefined && isFetching; - const { - NodeGroups: tableGroups, - FoundNodes: found = 0, - TotalNodes: total = 0, - } = currentData || {}; - - const {expandedGroups, setIsGroupExpanded} = useExpandedGroups(tableGroups); - - const renderControls = () => { - return ( - - ); - }; - - const renderGroups = () => { - if (tableGroups?.length) { - return tableGroups.map(({name, count}) => { - const isExpanded = expandedGroups[name]; - - return ( - - - - ); - }); - } - - return i18n('no-nodes-groups'); - }; - - return ( - - {renderControls()} - {error ? : null} - - {renderGroups()} - - - ); -} diff --git a/src/containers/Nodes/NodesTable.tsx b/src/containers/Nodes/NodesTable.tsx index c64f0f99c5..96c71f0f7f 100644 --- a/src/containers/Nodes/NodesTable.tsx +++ b/src/containers/Nodes/NodesTable.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {Illustration} from '../../components/Illustration'; -import type {RenderControls} from '../../components/PaginatedTable'; import {ResizeablePaginatedTable} from '../../components/PaginatedTable'; import {NODES_COLUMNS_WIDTH_LS_KEY} from '../../components/nodesColumns/constants'; import type {NodesFilters, NodesPreparedEntity} from '../../store/reducers/nodes/types'; @@ -28,9 +27,8 @@ interface NodesTableProps { filterGroupBy?: NodesGroupByField; columns: Column[]; - parentRef: React.RefObject; + scrollContainerRef: React.RefObject; - renderControls?: RenderControls; initialEntitiesCount?: number; } @@ -44,8 +42,7 @@ export function NodesTable({ filterGroup, filterGroupBy, columns, - parentRef, - renderControls, + scrollContainerRef, initialEntitiesCount, }: NodesTableProps) { const tableFilters: NodesFilters = React.useMemo(() => { @@ -81,11 +78,10 @@ export function NodesTable({ return ( []; + scrollContainerRef: React.RefObject; + onIsExpandedChange: (name: string, isExpanded: boolean) => void; +} + +const NodeGroup = React.memo(function NodeGroup({ + name, + count, + isExpanded, + path, + database, + searchValue, + peerRoleFilter, + groupByParam, + columns, + scrollContainerRef, + onIsExpandedChange, +}: NodeGroupProps) { + return ( + + + + ); +}); + +interface GroupedNodesComponentProps { + path?: string; + database?: string; + scrollContainerRef: React.RefObject; + withPeerRoleFilter?: boolean; + columns: Column[]; + defaultColumnsIds: NodesColumnId[]; + requiredColumnsIds: NodesColumnId[]; + selectedColumnsKey: string; + groupByParams: NodesGroupByField[]; +} + +export function GroupedNodesComponent({ + path, + database, + scrollContainerRef, + withPeerRoleFilter, + columns, + defaultColumnsIds, + requiredColumnsIds, + selectedColumnsKey, + groupByParams, +}: GroupedNodesComponentProps) { + const {searchValue, peerRoleFilter, groupByParam} = useNodesPageQueryParams(groupByParams); + const [autoRefreshInterval] = useAutoRefreshInterval(); + + const {columnsToShow, columnsToSelect, setColumns} = useSelectedColumns( + columns, + selectedColumnsKey, + NODES_COLUMNS_TITLES, + defaultColumnsIds, + requiredColumnsIds, + ); + + const {currentData, isFetching, error} = nodesApi.useGetNodesQuery( + { + path, + database, + filter: searchValue, + filter_peer_role: peerRoleFilter, + group: groupByParam, + limit: 0, + }, + { + pollingInterval: autoRefreshInterval, + }, + ); + + const isLoading = currentData === undefined && isFetching; + const { + NodeGroups: tableGroups, + FoundNodes: found = 0, + TotalNodes: total = 0, + } = currentData || {}; + + const {expandedGroups, setIsGroupExpanded} = useExpandedGroups(tableGroups); + + // Initialize the table state with the API data + const initialState = React.useMemo( + () => ({ + foundEntities: found, + totalEntities: total, + isInitialLoad: isLoading, + sortParams: undefined, + }), + [found, total, isLoading], + ); + + const renderGroups = () => { + if (tableGroups?.length) { + return tableGroups.map(({name, count}) => { + const isExpanded = expandedGroups[name]; + + return ( + + ); + }); + } + + return i18n('no-nodes-groups'); + }; + + return ( + + } + error={error ? : null} + table={renderGroups()} + tableProps={{ + scrollContainerRef, + scrollDependencies: [searchValue, groupByParam, tableGroups, peerRoleFilter], + loading: isLoading, + className: b('groups-wrapper'), + }} + fullHeight + /> + ); +} diff --git a/src/containers/Nodes/PaginatedNodes/NodesComponent.tsx b/src/containers/Nodes/PaginatedNodes/NodesComponent.tsx new file mode 100644 index 0000000000..34b8bbb874 --- /dev/null +++ b/src/containers/Nodes/PaginatedNodes/NodesComponent.tsx @@ -0,0 +1,82 @@ +import React from 'react'; + +import type {Column} from '../../../components/PaginatedTable'; +import {PaginatedTableWithLayout} from '../../../components/PaginatedTable/PaginatedTableWithLayout'; +import {NODES_COLUMNS_TITLES} from '../../../components/nodesColumns/constants'; +import type {NodesColumnId} from '../../../components/nodesColumns/constants'; +import {useViewerNodesHandlerHasGrouping} from '../../../store/reducers/capabilities/hooks'; +import type {NodesPreparedEntity} from '../../../store/reducers/nodes/types'; +import {useProblemFilter} from '../../../store/reducers/settings/hooks'; +import type {NodesGroupByField} from '../../../types/api/nodes'; +import {useSelectedColumns} from '../../../utils/hooks/useSelectedColumns'; +import {NodesTable} from '../NodesTable'; +import {useNodesPageQueryParams} from '../useNodesPageQueryParams'; + +import {NodesControlsWithTableState} from './NodesControlsWithTableState'; + +interface NodesComponentProps { + path?: string; + database?: string; + scrollContainerRef: React.RefObject; + withPeerRoleFilter?: boolean; + columns: Column[]; + defaultColumnsIds: NodesColumnId[]; + requiredColumnsIds: NodesColumnId[]; + selectedColumnsKey: string; + groupByParams: NodesGroupByField[]; +} + +export function NodesComponent({ + path, + database, + scrollContainerRef, + withPeerRoleFilter, + columns, + defaultColumnsIds, + requiredColumnsIds, + selectedColumnsKey, + groupByParams, +}: NodesComponentProps) { + const {searchValue, uptimeFilter, peerRoleFilter} = useNodesPageQueryParams(groupByParams); + const {problemFilter} = useProblemFilter(); + const viewerNodesHandlerHasGrouping = useViewerNodesHandlerHasGrouping(); + + const {columnsToShow, columnsToSelect, setColumns} = useSelectedColumns( + columns, + selectedColumnsKey, + NODES_COLUMNS_TITLES, + defaultColumnsIds, + requiredColumnsIds, + ); + + return ( + + } + table={ + + } + tableProps={{ + scrollContainerRef, + scrollDependencies: [searchValue, problemFilter, uptimeFilter, peerRoleFilter], + }} + fullHeight + /> + ); +} diff --git a/src/containers/Nodes/PaginatedNodes/NodesControlsWithTableState.tsx b/src/containers/Nodes/PaginatedNodes/NodesControlsWithTableState.tsx new file mode 100644 index 0000000000..8a80313af6 --- /dev/null +++ b/src/containers/Nodes/PaginatedNodes/NodesControlsWithTableState.tsx @@ -0,0 +1,36 @@ +import type {TableColumnSetupItem} from '@gravity-ui/uikit'; + +import {usePaginatedTableState} from '../../../components/PaginatedTable/PaginatedTableContext'; +import type {NodesGroupByField} from '../../../types/api/nodes'; +import {NodesControls} from '../NodesControls/NodesControls'; + +interface NodesControlsWithTableStateProps { + withGroupBySelect: boolean; + groupByParams: NodesGroupByField[]; + withPeerRoleFilter?: boolean; + columnsToSelect: TableColumnSetupItem[]; + handleSelectedColumnsUpdate: (updated: TableColumnSetupItem[]) => void; +} + +export function NodesControlsWithTableState({ + withGroupBySelect, + groupByParams, + withPeerRoleFilter, + columnsToSelect, + handleSelectedColumnsUpdate, +}: NodesControlsWithTableStateProps) { + const {tableState} = usePaginatedTableState(); + + return ( + + ); +} diff --git a/src/containers/Nodes/PaginatedNodes/PaginatedNodes.tsx b/src/containers/Nodes/PaginatedNodes/PaginatedNodes.tsx new file mode 100644 index 0000000000..2e50366497 --- /dev/null +++ b/src/containers/Nodes/PaginatedNodes/PaginatedNodes.tsx @@ -0,0 +1,70 @@ +import React from 'react'; + +import {LoaderWrapper} from '../../../components/LoaderWrapper/LoaderWrapper'; +import type {Column} from '../../../components/PaginatedTable'; +import type {NodesColumnId} from '../../../components/nodesColumns/constants'; +import { + useCapabilitiesLoaded, + useViewerNodesHandlerHasGrouping, +} from '../../../store/reducers/capabilities/hooks'; +import type {NodesPreparedEntity} from '../../../store/reducers/nodes/types'; +import {useProblemFilter} from '../../../store/reducers/settings/hooks'; +import type {NodesGroupByField} from '../../../types/api/nodes'; +import {NodesUptimeFilterValues} from '../../../utils/nodes'; +import {useNodesPageQueryParams} from '../useNodesPageQueryParams'; + +import {GroupedNodesComponent} from './GroupedNodesComponent'; +import {NodesComponent} from './NodesComponent'; + +import '../Nodes.scss'; + +export interface PaginatedNodesProps { + path?: string; + database?: string; + scrollContainerRef: React.RefObject; + withPeerRoleFilter?: boolean; + columns: Column[]; + defaultColumnsIds: NodesColumnId[]; + requiredColumnsIds: NodesColumnId[]; + selectedColumnsKey: string; + groupByParams: NodesGroupByField[]; +} + +export function PaginatedNodes(props: PaginatedNodesProps) { + const {uptimeFilter, groupByParam, handleUptimeFilterChange} = useNodesPageQueryParams( + props.groupByParams, + ); + + const {problemFilter, handleProblemFilterChange} = useProblemFilter(); + + const capabilitiesLoaded = useCapabilitiesLoaded(); + const viewerNodesHandlerHasGrouping = useViewerNodesHandlerHasGrouping(); + + // Other filters do not fit with grouping + // Reset them if grouping available + React.useEffect(() => { + if ( + viewerNodesHandlerHasGrouping && + (problemFilter !== 'All' || uptimeFilter !== NodesUptimeFilterValues.All) + ) { + handleProblemFilterChange('All'); + handleUptimeFilterChange(NodesUptimeFilterValues.All); + } + }, [ + handleProblemFilterChange, + handleUptimeFilterChange, + problemFilter, + uptimeFilter, + viewerNodesHandlerHasGrouping, + ]); + + const renderContent = () => { + if (viewerNodesHandlerHasGrouping && groupByParam) { + return ; + } + + return ; + }; + + return {renderContent()}; +} diff --git a/src/containers/Nodes/PaginatedNodes/index.ts b/src/containers/Nodes/PaginatedNodes/index.ts new file mode 100644 index 0000000000..88cbb400d9 --- /dev/null +++ b/src/containers/Nodes/PaginatedNodes/index.ts @@ -0,0 +1 @@ +export {PaginatedNodes} from './PaginatedNodes'; diff --git a/src/containers/PDiskPage/PDiskPage.tsx b/src/containers/PDiskPage/PDiskPage.tsx index 6a1fc91ee9..91248f4a0f 100644 --- a/src/containers/PDiskPage/PDiskPage.tsx +++ b/src/containers/PDiskPage/PDiskPage.tsx @@ -249,7 +249,7 @@ export function PDiskPage() { ; + scrollContainerRef: React.RefObject; initialEntitiesCount?: number; } diff --git a/src/containers/Storage/PaginatedStorageGroups.tsx b/src/containers/Storage/PaginatedStorageGroups.tsx deleted file mode 100644 index 6c4a91bf96..0000000000 --- a/src/containers/Storage/PaginatedStorageGroups.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import React from 'react'; - -import {ResponseError} from '../../components/Errors/ResponseError/ResponseError'; -import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper'; -import type {RenderControls} from '../../components/PaginatedTable'; -import {TableWithControlsLayout} from '../../components/TableWithControlsLayout/TableWithControlsLayout'; -import { - useCapabilitiesLoaded, - useStorageGroupsHandlerHasGrouping, -} from '../../store/reducers/capabilities/hooks'; -import {storageApi} from '../../store/reducers/storage/storage'; -import {useAutoRefreshInterval} from '../../utils/hooks'; -import {renderPaginatedTableErrorMessage} from '../../utils/renderPaginatedTableErrorMessage'; - -import type {PaginatedStorageProps} from './PaginatedStorage'; -import {StorageGroupsControls} from './StorageControls/StorageControls'; -import {PaginatedStorageGroupsTable} from './StorageGroups/PaginatedStorageGroupsTable'; -import {useStorageGroupsSelectedColumns} from './StorageGroups/columns/hooks'; -import {TableGroup} from './TableGroup/TableGroup'; -import {useExpandedGroups} from './TableGroup/useExpandedTableGroups'; -import i18n from './i18n'; -import {b} from './shared'; -import {useStorageQueryParams} from './useStorageQueryParams'; - -import './Storage.scss'; - -export function PaginatedStorageGroups(props: PaginatedStorageProps) { - const {storageGroupsGroupByParam, visibleEntities, handleShowAllGroups} = - useStorageQueryParams(); - - const capabilitiesLoaded = useCapabilitiesLoaded(); - const storageGroupsHandlerHasGroupping = useStorageGroupsHandlerHasGrouping(); - - // Other filters do not fit with grouping - // Reset them if grouping available - React.useEffect(() => { - if (storageGroupsHandlerHasGroupping && visibleEntities !== 'all') { - handleShowAllGroups(); - } - }, [handleShowAllGroups, storageGroupsHandlerHasGroupping, visibleEntities]); - - const renderContent = () => { - if (storageGroupsHandlerHasGroupping && storageGroupsGroupByParam) { - return ; - } - - return ; - }; - - return {renderContent()}; -} - -function StorageGroupsComponent({ - database, - nodeId, - groupId, - pDiskId, - viewContext, - parentRef, - initialEntitiesCount, -}: PaginatedStorageProps) { - const {searchValue, visibleEntities, handleShowAllGroups} = useStorageQueryParams(); - - const storageGroupsHandlerHasGroupping = useStorageGroupsHandlerHasGrouping(); - - const {columnsToShow, columnsToSelect, setColumns} = useStorageGroupsSelectedColumns({ - visibleEntities, - viewContext, - }); - - const renderControls: RenderControls = ({totalEntities, foundEntities, inited}) => { - return ( - - ); - }; - - return ( - - ); -} - -function GroupedStorageGroupsComponent({ - database, - nodeId, - groupId, - pDiskId, - parentRef, - viewContext, -}: PaginatedStorageProps) { - const [autoRefreshInterval] = useAutoRefreshInterval(); - const {searchValue, storageGroupsGroupByParam, visibleEntities, handleShowAllGroups} = - useStorageQueryParams(); - - const {columnsToShow, columnsToSelect, setColumns} = useStorageGroupsSelectedColumns({ - visibleEntities, - viewContext, - }); - - const {currentData, isFetching, error} = storageApi.useGetStorageGroupsInfoQuery( - { - database, - with: 'all', - nodeId, - groupId, - pDiskId, - filter: searchValue, - shouldUseGroupsHandler: true, - group: storageGroupsGroupByParam, - }, - { - pollingInterval: autoRefreshInterval, - }, - ); - - const isLoading = currentData === undefined && isFetching; - const {tableGroups, found = 0, total = 0} = currentData || {}; - - const {expandedGroups, setIsGroupExpanded} = useExpandedGroups(tableGroups); - - const renderControls = () => { - return ( - - ); - }; - - const renderGroups = () => { - if (tableGroups?.length) { - return tableGroups.map(({name, count}) => { - const isExpanded = expandedGroups[name]; - - return ( - - - - ); - }); - } - - return i18n('no-groups'); - }; - - return ( - - {renderControls()} - {error ? : null} - - {renderGroups()} - - - ); -} diff --git a/src/containers/Storage/PaginatedStorageGroups/GroupedStorageGroupsComponent.tsx b/src/containers/Storage/PaginatedStorageGroups/GroupedStorageGroupsComponent.tsx new file mode 100644 index 0000000000..07e2bae4c1 --- /dev/null +++ b/src/containers/Storage/PaginatedStorageGroups/GroupedStorageGroupsComponent.tsx @@ -0,0 +1,181 @@ +import React from 'react'; + +import {ResponseError} from '../../../components/Errors/ResponseError/ResponseError'; +import {PaginatedTableWithLayout} from '../../../components/PaginatedTable/PaginatedTableWithLayout'; +import {storageApi} from '../../../store/reducers/storage/storage'; +import type {GroupsGroupByField} from '../../../types/api/storage'; +import {useAutoRefreshInterval} from '../../../utils/hooks'; +import {renderPaginatedTableErrorMessage} from '../../../utils/renderPaginatedTableErrorMessage'; +import type {PaginatedStorageProps} from '../PaginatedStorage'; +import {PaginatedStorageGroupsTable} from '../PaginatedStorageGroupsTable'; +import {useStorageGroupsSelectedColumns} from '../PaginatedStorageGroupsTable/columns/hooks'; +import type {StorageGroupsColumn} from '../PaginatedStorageGroupsTable/columns/types'; +import {TableGroup} from '../TableGroup/TableGroup'; +import {useExpandedGroups} from '../TableGroup/useExpandedTableGroups'; +import i18n from '../i18n'; +import {b} from '../shared'; +import {useStorageQueryParams} from '../useStorageQueryParams'; + +import {StorageGroupsControlsWithTableState} from './StorageGroupsControls'; + +interface StorageGroupGroupProps { + name: string; + count: number; + isExpanded: boolean; + database?: string; + nodeId?: string | number; + groupId?: string | number; + pDiskId?: string | number; + searchValue: string; + visibleEntities: 'all'; + filterGroupBy?: GroupsGroupByField; + columns: StorageGroupsColumn[]; + scrollContainerRef: React.RefObject; + onIsExpandedChange: (name: string, isExpanded: boolean) => void; + handleShowAllGroups: VoidFunction; +} + +export const StorageGroupGroup = React.memo(function StorageGroupGroup({ + name, + count, + isExpanded, + database, + nodeId, + groupId, + pDiskId, + searchValue, + scrollContainerRef, + filterGroupBy, + columns, + onIsExpandedChange, + handleShowAllGroups, +}: StorageGroupGroupProps) { + return ( + + + + ); +}); + +export function GroupedStorageGroupsComponent({ + database, + nodeId, + groupId, + pDiskId, + scrollContainerRef, + viewContext, +}: PaginatedStorageProps) { + const [autoRefreshInterval] = useAutoRefreshInterval(); + const {searchValue, storageGroupsGroupByParam, visibleEntities, handleShowAllGroups} = + useStorageQueryParams(); + + const {columnsToShow, columnsToSelect, setColumns} = useStorageGroupsSelectedColumns({ + visibleEntities, + viewContext, + }); + + const {currentData, isFetching, error} = storageApi.useGetStorageGroupsInfoQuery( + { + database, + with: 'all', + nodeId, + groupId, + pDiskId, + filter: searchValue, + shouldUseGroupsHandler: true, + group: storageGroupsGroupByParam, + }, + { + pollingInterval: autoRefreshInterval, + }, + ); + + const isLoading = currentData === undefined && isFetching; + const {tableGroups, found = 0, total = 0} = currentData || {}; + + const {expandedGroups, setIsGroupExpanded} = useExpandedGroups(tableGroups); + + // Initialize the table state with the API data + const initialState = React.useMemo( + () => ({ + foundEntities: found, + totalEntities: total, + isInitialLoad: isLoading, + sortParams: undefined, + }), + [found, total, isLoading], + ); + + const renderGroups = () => { + if (tableGroups?.length) { + return tableGroups.map(({name, count}) => { + const isExpanded = expandedGroups[name]; + + return ( + + ); + }); + } + + return i18n('no-groups'); + }; + + return ( + + } + error={error ? : null} + table={renderGroups()} + initialState={initialState} + tableProps={{ + scrollContainerRef, + scrollDependencies: [searchValue, storageGroupsGroupByParam, tableGroups], + loading: isLoading, + className: b('groups-wrapper'), + }} + /> + ); +} diff --git a/src/containers/Storage/PaginatedStorageGroups/PaginatedStorageGroups.tsx b/src/containers/Storage/PaginatedStorageGroups/PaginatedStorageGroups.tsx new file mode 100644 index 0000000000..bc8757c762 --- /dev/null +++ b/src/containers/Storage/PaginatedStorageGroups/PaginatedStorageGroups.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import {LoaderWrapper} from '../../../components/LoaderWrapper/LoaderWrapper'; +import { + useCapabilitiesLoaded, + useStorageGroupsHandlerHasGrouping, +} from '../../../store/reducers/capabilities/hooks'; +import type {PaginatedStorageProps} from '../PaginatedStorage'; +import {useStorageQueryParams} from '../useStorageQueryParams'; + +import {GroupedStorageGroupsComponent} from './GroupedStorageGroupsComponent'; +import {StorageGroupsComponent} from './StorageGroupsComponent'; + +import '../Storage.scss'; + +export function PaginatedStorageGroups(props: PaginatedStorageProps) { + const {storageGroupsGroupByParam, visibleEntities, handleShowAllGroups} = + useStorageQueryParams(); + + const capabilitiesLoaded = useCapabilitiesLoaded(); + const storageGroupsHandlerHasGrouping = useStorageGroupsHandlerHasGrouping(); + + // Other filters do not fit with grouping + // Reset them if grouping available + React.useEffect(() => { + if (storageGroupsHandlerHasGrouping && visibleEntities !== 'all') { + handleShowAllGroups(); + } + }, [handleShowAllGroups, storageGroupsHandlerHasGrouping, visibleEntities]); + + const renderContent = () => { + if (storageGroupsHandlerHasGrouping && storageGroupsGroupByParam) { + return ; + } + + return ; + }; + + return {renderContent()}; +} diff --git a/src/containers/Storage/PaginatedStorageGroups/StorageGroupsComponent.tsx b/src/containers/Storage/PaginatedStorageGroups/StorageGroupsComponent.tsx new file mode 100644 index 0000000000..0864a81c6d --- /dev/null +++ b/src/containers/Storage/PaginatedStorageGroups/StorageGroupsComponent.tsx @@ -0,0 +1,61 @@ +import {PaginatedTableWithLayout} from '../../../components/PaginatedTable/PaginatedTableWithLayout'; +import {useStorageGroupsHandlerHasGrouping} from '../../../store/reducers/capabilities/hooks'; +import {renderPaginatedTableErrorMessage} from '../../../utils/renderPaginatedTableErrorMessage'; +import type {PaginatedStorageProps} from '../PaginatedStorage'; +import {PaginatedStorageGroupsTable} from '../PaginatedStorageGroupsTable'; +import {useStorageGroupsSelectedColumns} from '../PaginatedStorageGroupsTable/columns/hooks'; +import {useStorageQueryParams} from '../useStorageQueryParams'; + +import {StorageGroupsControlsWithTableState} from './StorageGroupsControls'; + +export function StorageGroupsComponent({ + database, + nodeId, + groupId, + pDiskId, + viewContext, + scrollContainerRef, + initialEntitiesCount, +}: PaginatedStorageProps) { + const {searchValue, visibleEntities, handleShowAllGroups} = useStorageQueryParams(); + + const storageGroupsHandlerHasGrouping = useStorageGroupsHandlerHasGrouping(); + + const {columnsToShow, columnsToSelect, setColumns} = useStorageGroupsSelectedColumns({ + visibleEntities, + viewContext, + }); + + return ( + + } + table={ + + } + tableProps={{ + scrollContainerRef, + scrollDependencies: [searchValue, visibleEntities], + }} + fullHeight + /> + ); +} diff --git a/src/containers/Storage/PaginatedStorageGroups/StorageGroupsControls.tsx b/src/containers/Storage/PaginatedStorageGroups/StorageGroupsControls.tsx new file mode 100644 index 0000000000..aafb89fe86 --- /dev/null +++ b/src/containers/Storage/PaginatedStorageGroups/StorageGroupsControls.tsx @@ -0,0 +1,132 @@ +import React from 'react'; + +import type {TableColumnSetupItem} from '@gravity-ui/uikit'; +import {Select, TableColumnSetup, Text} from '@gravity-ui/uikit'; + +import {EntitiesCount} from '../../../components/EntitiesCount/EntitiesCount'; +import {usePaginatedTableState} from '../../../components/PaginatedTable/PaginatedTableContext'; +import {Search} from '../../../components/Search/Search'; +import {useIsUserAllowedToMakeChanges} from '../../../utils/hooks/useIsUserAllowedToMakeChanges'; +import {STORAGE_GROUPS_GROUP_BY_OPTIONS} from '../PaginatedStorageGroupsTable/columns/constants'; +import {StorageTypeFilter} from '../StorageTypeFilter/StorageTypeFilter'; +import {StorageVisibleEntitiesFilter} from '../StorageVisibleEntitiesFilter/StorageVisibleEntitiesFilter'; +import i18n from '../i18n'; +import {b} from '../shared'; +import {useStorageQueryParams} from '../useStorageQueryParams'; + +interface StorageControlsProps { + withTypeSelector?: boolean; + withGroupBySelect?: boolean; + + entitiesCountCurrent: number; + entitiesCountTotal?: number; + entitiesLoading: boolean; + + columnsToSelect: TableColumnSetupItem[]; + handleSelectedColumnsUpdate: (updated: TableColumnSetupItem[]) => void; +} + +export function StorageGroupsControls({ + withTypeSelector, + withGroupBySelect, + + entitiesCountCurrent, + entitiesCountTotal, + entitiesLoading, + + columnsToSelect, + handleSelectedColumnsUpdate, +}: StorageControlsProps) { + const { + searchValue, + storageType, + visibleEntities, + storageGroupsGroupByParam, + handleTextFilterChange, + handleStorageTypeChange, + handleVisibleEntitiesChange, + handleStorageGroupsGroupByParamChange, + } = useStorageQueryParams(); + + const isUserAllowedToMakeChanges = useIsUserAllowedToMakeChanges(); + + const handleGroupBySelectUpdate = (value: string[]) => { + handleStorageGroupsGroupByParamChange(value[0]); + }; + + const displayTypeSelector = withTypeSelector && isUserAllowedToMakeChanges; + + return ( + + + {displayTypeSelector && ( + + )} + {withGroupBySelect ? null : ( + + )} + + {withGroupBySelect ? ( + + {i18n('controls_group-by-placeholder')} + - - ) : null} - - - ); -} - export function StorageNodesControls({ withTypeSelector, withGroupBySelect, @@ -185,3 +105,29 @@ export function StorageNodesControls({ ); } + +export function StorageNodesControlsWithTableState({ + withTypeSelector, + withGroupBySelect, + columnsToSelect, + handleSelectedColumnsUpdate, +}: { + withTypeSelector?: boolean; + withGroupBySelect?: boolean; + columnsToSelect: any[]; + handleSelectedColumnsUpdate: (updated: any[]) => void; +}) { + const {tableState} = usePaginatedTableState(); + + return ( + + ); +} diff --git a/src/containers/Storage/PaginatedStorageNodes/index.ts b/src/containers/Storage/PaginatedStorageNodes/index.ts new file mode 100644 index 0000000000..36ccb0f1ba --- /dev/null +++ b/src/containers/Storage/PaginatedStorageNodes/index.ts @@ -0,0 +1 @@ +export {PaginatedStorageNodes} from './PaginatedStorageNodes'; diff --git a/src/containers/Storage/PaginatedStorageNodes/useStorageNodesColumnsToSelect.ts b/src/containers/Storage/PaginatedStorageNodes/useStorageNodesColumnsToSelect.ts new file mode 100644 index 0000000000..f7de340333 --- /dev/null +++ b/src/containers/Storage/PaginatedStorageNodes/useStorageNodesColumnsToSelect.ts @@ -0,0 +1,26 @@ +import {useAdditionalNodesProps} from '../../../utils/hooks/useAdditionalNodesProps'; +import {useStorageNodesSelectedColumns} from '../PaginatedStorageNodesTable/columns/hooks'; +import type {StorageNodesColumnsSettings} from '../PaginatedStorageNodesTable/columns/types'; +import type {StorageViewContext} from '../types'; +import {useStorageQueryParams} from '../useStorageQueryParams'; + +export function useStorageNodesColumnsToSelect({ + database, + viewContext, + columnsSettings, +}: { + database?: string; + viewContext?: StorageViewContext; + columnsSettings?: StorageNodesColumnsSettings; +}) { + const additionalNodesProps = useAdditionalNodesProps(); + const {visibleEntities} = useStorageQueryParams(); + + return useStorageNodesSelectedColumns({ + additionalNodesProps, + visibleEntities, + database, + viewContext, + columnsSettings, + }); +} diff --git a/src/containers/Storage/StorageNodes/PaginatedStorageNodesTable.tsx b/src/containers/Storage/PaginatedStorageNodesTable/PaginatedStorageNodesTable.tsx similarity index 91% rename from src/containers/Storage/StorageNodes/PaginatedStorageNodesTable.tsx rename to src/containers/Storage/PaginatedStorageNodesTable/PaginatedStorageNodesTable.tsx index 248b2f1083..29e48fa1b6 100644 --- a/src/containers/Storage/StorageNodes/PaginatedStorageNodesTable.tsx +++ b/src/containers/Storage/PaginatedStorageNodesTable/PaginatedStorageNodesTable.tsx @@ -1,10 +1,6 @@ import React from 'react'; -import type { - PaginatedTableData, - RenderControls, - RenderErrorMessage, -} from '../../../components/PaginatedTable'; +import type {PaginatedTableData, RenderErrorMessage} from '../../../components/PaginatedTable'; import {ResizeablePaginatedTable} from '../../../components/PaginatedTable'; import {VISIBLE_ENTITIES} from '../../../store/reducers/storage/constants'; import type {PreparedStorageNode, VisibleEntities} from '../../../store/reducers/storage/types'; @@ -40,8 +36,7 @@ interface PaginatedStorageNodesTableProps { nodesUptimeFilter: NodesUptimeFilterValues; onShowAll: VoidFunction; - parentRef: React.RefObject; - renderControls?: RenderControls; + scrollContainerRef: React.RefObject; renderErrorMessage: RenderErrorMessage; initialEntitiesCount?: number; onDataFetched?: (data: PaginatedTableData) => void; @@ -58,8 +53,7 @@ export const PaginatedStorageNodesTable = ({ visibleEntities, nodesUptimeFilter, onShowAll, - parentRef, - renderControls, + scrollContainerRef, renderErrorMessage, initialEntitiesCount, onDataFetched, @@ -106,12 +100,11 @@ export const PaginatedStorageNodesTable = ({ return ( void; } -export function TableGroup({ +export const TableGroup = ({ children, title, entityName, count, expanded = false, onIsExpandedChange, -}: TableGroupProps) { +}: TableGroupProps) => { const toggleCollapsed = () => { onIsExpandedChange(title, !expanded); }; @@ -60,4 +60,6 @@ export function TableGroup({ {renderContent()} ); -} +}; + +TableGroup.displayName = 'TableGroup'; diff --git a/src/containers/Storage/useStorageQueryParams.ts b/src/containers/Storage/useStorageQueryParams.ts index 92e4e6e2c3..3caf92987e 100644 --- a/src/containers/Storage/useStorageQueryParams.ts +++ b/src/containers/Storage/useStorageQueryParams.ts @@ -4,8 +4,8 @@ import type {StorageType, VisibleEntities} from '../../store/reducers/storage/ty import {storageTypeSchema, visibleEntitiesSchema} from '../../store/reducers/storage/types'; import {NodesUptimeFilterValues, nodesUptimeFilterValuesSchema} from '../../utils/nodes'; -import {storageGroupsGroupByParamSchema} from './StorageGroups/columns/constants'; -import {storageNodesGroupByParamSchema} from './StorageNodes/columns/constants'; +import {storageGroupsGroupByParamSchema} from './PaginatedStorageGroupsTable/columns/constants'; +import {storageNodesGroupByParamSchema} from './PaginatedStorageNodesTable/columns/constants'; export function useStorageQueryParams() { const [queryParams, setQueryParams] = useQueryParams({ diff --git a/src/containers/Storage/utils/useStorageColumnsSettings.ts b/src/containers/Storage/utils/useStorageColumnsSettings.ts index 133e3c77f5..d166fee074 100644 --- a/src/containers/Storage/utils/useStorageColumnsSettings.ts +++ b/src/containers/Storage/utils/useStorageColumnsSettings.ts @@ -1,6 +1,6 @@ import React from 'react'; -import type {StorageNodesColumnsSettings} from '../StorageNodes/columns/types'; +import type {StorageNodesColumnsSettings} from '../PaginatedStorageNodesTable/columns/types'; import type {StorageNodesPaginatedTableData} from '../types'; const PDISK_VDISK_WIDTH = 3; diff --git a/src/containers/StorageGroupPage/StorageGroupPage.tsx b/src/containers/StorageGroupPage/StorageGroupPage.tsx index edfc9cb668..46426f2715 100644 --- a/src/containers/StorageGroupPage/StorageGroupPage.tsx +++ b/src/containers/StorageGroupPage/StorageGroupPage.tsx @@ -112,7 +112,7 @@ export function StorageGroupPage() {