Skip to content

feat(PaginatedStorage): add grouping #1364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Sep 30, 2024
16 changes: 11 additions & 5 deletions src/components/PaginatedTable/PaginatedTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import './PaginatedTable.scss';

export interface PaginatedTableProps<T, F> {
limit: number;
initialEntitiesCount?: number;
fetchData: FetchData<T, F>;
filters?: F;
tableName: string;
Expand All @@ -42,6 +43,7 @@ export interface PaginatedTableProps<T, F> {

export const PaginatedTable = <T, F>({
limit,
initialEntitiesCount,
fetchData,
filters,
tableName,
Expand All @@ -56,9 +58,12 @@ export const PaginatedTable = <T, F>({
renderEmptyDataMessage,
containerClassName,
}: PaginatedTableProps<T, F>) => {
const initialTotal = initialEntitiesCount || limit;
const initialFound = initialEntitiesCount || 0;

const [sortParams, setSortParams] = React.useState<SortParams | undefined>(initialSortParams);
const [totalEntities, setTotalEntities] = React.useState(limit);
const [foundEntities, setFoundEntities] = React.useState(0);
const [totalEntities, setTotalEntities] = React.useState(initialTotal);
const [foundEntities, setFoundEntities] = React.useState(initialFound);
const [activeChunks, setActiveChunks] = React.useState<number[]>([]);
const [isInitialLoad, setIsInitialLoad] = React.useState(true);

Expand All @@ -82,8 +87,8 @@ export const PaginatedTable = <T, F>({

// reset table on filters change
React.useLayoutEffect(() => {
setTotalEntities(limit);
setFoundEntities(0);
setTotalEntities(initialTotal);
setFoundEntities(initialFound);
setIsInitialLoad(true);
if (parentContainer) {
parentContainer.scrollTo(0, 0);
Expand All @@ -92,7 +97,7 @@ export const PaginatedTable = <T, F>({
}

setActiveChunks([0]);
}, [filters, limit, parentContainer]);
}, [filters, initialFound, initialTotal, limit, parentContainer]);

const renderChunks = () => {
if (!observer) {
Expand All @@ -117,6 +122,7 @@ export const PaginatedTable = <T, F>({
key={value}
id={value}
limit={limit}
totalLength={totalLength}
rowHeight={rowHeight}
columns={columns}
fetchData={fetchData}
Expand Down
8 changes: 7 additions & 1 deletion src/components/PaginatedTable/TableChunk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const typedMemo: <T>(Component: T) => T = React.memo;
interface TableChunkProps<T, F> {
id: number;
limit: number;
totalLength: number;
rowHeight: number;
columns: Column<T>[];
filters?: F;
Expand All @@ -35,6 +36,7 @@ interface TableChunkProps<T, F> {
export const TableChunk = typedMemo(function TableChunk<T, F>({
id,
limit,
totalLength,
rowHeight,
columns,
fetchData,
Expand Down Expand Up @@ -101,7 +103,11 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
}
}, [currentData, isActive, onDataFetched]);

const dataLength = currentData?.data?.length || limit;
const chunkOffset = id * limit;
const remainingLenght = totalLength - chunkOffset;
const calculatedChunkLength = remainingLenght < limit ? remainingLenght : limit;
Comment on lines +106 to +108
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows not to show 50 (limit) loading rows for the last chunk, when we know how many entities are left. Also it allows to set some predefined initial length to table


const dataLength = currentData?.data?.length || calculatedChunkLength;

const renderContent = () => {
if (!isActive) {
Expand Down
8 changes: 7 additions & 1 deletion src/components/TableSkeleton/TableSkeleton.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
.table-skeleton {
width: 100%;
&__wrapper {
width: 100%;

&_hidden {
visibility: hidden;
}
}

&__row {
display: flex;
Expand Down
36 changes: 21 additions & 15 deletions src/components/TableSkeleton/TableSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Skeleton} from '@gravity-ui/uikit';

import {cn} from '../../utils/cn';
import {useDelayed} from '../../utils/hooks/useDelayed';

import './TableSkeleton.scss';

Expand All @@ -9,21 +10,26 @@ const b = cn('table-skeleton');
interface TableSkeletonProps {
className?: string;
rows?: number;
delay?: number;
}

export const TableSkeleton = ({rows = 2, className}: TableSkeletonProps) => (
<div className={b(null, className)}>
<div className={b('row')}>
<Skeleton className={b('col-1')} />
<Skeleton className={b('col-2')} />
<Skeleton className={b('col-3')} />
<Skeleton className={b('col-4')} />
<Skeleton className={b('col-5')} />
</div>
{[...new Array(rows)].map((_, index) => (
<div className={b('row')} key={`skeleton-row-${index}`}>
<Skeleton className={b('col-full')} />
export const TableSkeleton = ({rows = 2, delay = 600, className}: TableSkeletonProps) => {
const [show] = useDelayed(delay);

return (
<div className={b('wrapper', {hidden: !show}, className)}>
<div className={b('row')}>
<Skeleton className={b('col-1')} />
<Skeleton className={b('col-2')} />
<Skeleton className={b('col-3')} />
<Skeleton className={b('col-4')} />
<Skeleton className={b('col-5')} />
</div>
))}
</div>
);
{[...new Array(rows)].map((_, index) => (
<div className={b('row')} key={`skeleton-row-${index}`}>
<Skeleton className={b('col-full')} />
</div>
))}
</div>
);
};
5 changes: 2 additions & 3 deletions src/containers/Cluster/Cluster.scss
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
@import '../../styles/mixins.scss';

.cluster {
position: relative;

overflow: auto;
flex-grow: 1;

height: 100%;
padding: 0 20px;

@include flex-container();

&__header {
position: sticky;
left: 0;
Expand Down
156 changes: 8 additions & 148 deletions src/containers/Storage/PaginatedStorage.tsx
Original file line number Diff line number Diff line change
@@ -1,162 +1,22 @@
import {StringParam, useQueryParams} from 'use-query-params';
import {PaginatedStorageGroups} from './PaginatedStorageGroups';
import {PaginatedStorageNodes} from './PaginatedStorageNodes';
import {useStorageQueryParams} from './useStorageQueryParams';

import {AccessDenied} from '../../components/Errors/403/AccessDenied';
import {ResponseError} from '../../components/Errors/ResponseError/ResponseError';
import type {RenderControls, RenderErrorMessage} from '../../components/PaginatedTable';
import {useClusterBaseInfo} from '../../store/reducers/cluster/cluster';
import {VISIBLE_ENTITIES} from '../../store/reducers/storage/constants';
import {storageTypeSchema, visibleEntitiesSchema} from '../../store/reducers/storage/types';
import type {StorageType, VisibleEntities} from '../../store/reducers/storage/types';
import {NodesUptimeFilterValues, nodesUptimeFilterValuesSchema} from '../../utils/nodes';
import {useAdditionalNodeProps} from '../AppWithClusters/useClusterData';

import {StorageControls} from './StorageControls/StorageControls';
import {PaginatedStorageGroups} from './StorageGroups/PaginatedStorageGroups';
import {useStorageGroupsSelectedColumns} from './StorageGroups/columns/hooks';
import {PaginatedStorageNodes} from './StorageNodes/PaginatedStorageNodes';
import {useStorageNodesSelectedColumns} from './StorageNodes/columns/hooks';

interface PaginatedStorageProps {
export interface PaginatedStorageProps {
database?: string;
nodeId?: string;
groupId?: string;
parentContainer?: Element | null;
}

export const PaginatedStorage = ({
database,
nodeId,
groupId,
parentContainer,
}: PaginatedStorageProps) => {
const {balancer} = useClusterBaseInfo();
const {additionalNodesProps} = useAdditionalNodeProps({balancer});
export const PaginatedStorage = (props: PaginatedStorageProps) => {
const {storageType} = useStorageQueryParams();

const [queryParams, setQueryParams] = useQueryParams({
type: StringParam,
visible: StringParam,
search: StringParam,
uptimeFilter: StringParam,
});
const storageType = storageTypeSchema.parse(queryParams.type);
const isGroups = storageType === 'groups';
const isNodes = storageType === 'nodes';

const visibleEntities = visibleEntitiesSchema.parse(queryParams.visible);
const searchValue = queryParams.search ?? '';
const nodesUptimeFilter = nodesUptimeFilterValuesSchema.parse(queryParams.uptimeFilter);

const {
columnsToShow: storageNodesColumnsToShow,
columnsToSelect: storageNodesColumnsToSelect,
setColumns: setStorageNodesSelectedColumns,
} = useStorageNodesSelectedColumns({
additionalNodesProps,
visibleEntities,
database,
groupId,
});

const {
columnsToShow: storageGroupsColumnsToShow,
columnsToSelect: storageGroupsColumnsToSelect,
setColumns: setStorageGroupsSelectedColumns,
} = useStorageGroupsSelectedColumns(visibleEntities);

const handleTextFilterChange = (value: string) => {
setQueryParams({search: value || undefined}, 'replaceIn');
};

const handleGroupVisibilityChange = (value: VisibleEntities) => {
setQueryParams({visible: value}, 'replaceIn');
};

const handleStorageTypeChange = (value: StorageType) => {
setQueryParams({type: value}, 'replaceIn');
};

const handleUptimeFilterChange = (value: NodesUptimeFilterValues) => {
setQueryParams({uptimeFilter: value}, 'replaceIn');
};

const handleShowAllGroups = () => {
handleGroupVisibilityChange(VISIBLE_ENTITIES.all);
};

const handleShowAllNodes = () => {
setQueryParams(
{
visible: VISIBLE_ENTITIES.all,
uptimeFilter: NodesUptimeFilterValues.All,
},
'replaceIn',
);
};

const renderControls: RenderControls = ({totalEntities, foundEntities, inited}) => {
const columnsToSelect = isGroups
? storageGroupsColumnsToSelect
: storageNodesColumnsToSelect;

const handleSelectedColumnsUpdate = isGroups
? setStorageGroupsSelectedColumns
: setStorageNodesSelectedColumns;

return (
<StorageControls
searchValue={searchValue}
handleSearchValueChange={handleTextFilterChange}
withTypeSelector
storageType={storageType}
handleStorageTypeChange={handleStorageTypeChange}
visibleEntities={visibleEntities}
handleVisibleEntitiesChange={handleGroupVisibilityChange}
nodesUptimeFilter={nodesUptimeFilter}
handleNodesUptimeFilterChange={handleUptimeFilterChange}
entitiesCountCurrent={foundEntities}
entitiesCountTotal={totalEntities}
entitiesLoading={!inited}
columnsToSelect={columnsToSelect}
handleSelectedColumnsUpdate={handleSelectedColumnsUpdate}
/>
);
};

const renderErrorMessage: RenderErrorMessage = (error) => {
if (error.status === 403) {
return <AccessDenied position="left" />;
}

return <ResponseError error={error} />;
};

if (isNodes) {
return (
<PaginatedStorageNodes
searchValue={searchValue}
visibleEntities={visibleEntities}
nodesUptimeFilter={nodesUptimeFilter}
database={database}
onShowAll={handleShowAllNodes}
parentContainer={parentContainer}
renderControls={renderControls}
renderErrorMessage={renderErrorMessage}
columns={storageNodesColumnsToShow}
/>
);
return <PaginatedStorageNodes {...props} />;
}

return (
<PaginatedStorageGroups
searchValue={searchValue}
visibleEntities={visibleEntities}
database={database}
nodeId={nodeId}
onShowAll={handleShowAllGroups}
parentContainer={parentContainer}
renderControls={renderControls}
renderErrorMessage={renderErrorMessage}
columns={storageGroupsColumnsToShow}
/>
);
return <PaginatedStorageGroups {...props} />;
};
Loading
Loading