Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/containers/Nodes/getNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {NodesRequestParams} from '../../types/api/nodes';
import {prepareSortValue} from '../../utils/filters';
import {getUptimeParamValue} from '../../utils/nodes';
import {getRequiredDataFields} from '../../utils/tableUtils/getRequiredDataFields';
import {getStorageNodesSelectionKey} from '../Storage/utils/getStorageNodesSelectionKey';

export const getNodes: FetchData<
PreparedStorageNode,
Expand Down Expand Up @@ -44,6 +45,8 @@ export const getNodes: FetchData<
? {path, databaseFullPath, useMetaProxy}
: undefined;

const selectionKey = getStorageNodesSelectionKey(filters, type, storage);

const response = await window.api.viewer.getNodes({
type,
storage,
Expand All @@ -66,5 +69,7 @@ export const getNodes: FetchData<
data: preparedResponse.nodes || [],
found: preparedResponse.found || 0,
total: preparedResponse.total || 0,
columnsSettings: preparedResponse.columnsSettings,
selectionKey,
};
};
9 changes: 8 additions & 1 deletion src/containers/Storage/PDisk/PDisk.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ interface PDiskProps {
onHidePopup?: VoidFunction;
className?: string;
progressBarClassName?: string;
linkRef?: React.Ref<HTMLAnchorElement>;
viewContext?: StorageViewContext;
width?: number;
delayOpen?: number;
Expand All @@ -192,6 +193,7 @@ export const PDisk = ({
onHidePopup,
className,
progressBarClassName,
linkRef,
viewContext,
width,
delayOpen = DISKS_POPUP_DEBOUNCE_TIMEOUT,
Expand Down Expand Up @@ -278,7 +280,12 @@ export const PDisk = ({
delayOpen={delayOpen}
delayClose={delayClose}
>
<InternalLink to={pDiskPath} aria-label={accessibleName} className={b('content')}>
<InternalLink
to={pDiskPath}
innerRef={linkRef}
aria-label={accessibleName}
className={b('content')}
>
<DiskStateProgressBar
allocation={allocatedPercent}
tone={tone}
Expand Down
43 changes: 43 additions & 0 deletions src/containers/Storage/PDisks/PDisksPreview.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
@use '../../../styles/mixins';

.ydb-storage-pdisks-preview {
&__color {
@include mixins.entity-state-colors(&);
}

&__control {
// Match the SVG height and avoid adding padding to the compact disk grid.
--g-button-height: 41px;
--g-button-padding: 0;
--g-button-background-color-hover: transparent;

flex-shrink: 0;
}

&__svg {
display: block;
}

&__pdisk-background,
&__vdisk-background {
fill: var(--entity-state-background-color, var(--g-color-base-misc-light));
}

&__pdisk-fill,
&__vdisk-fill {
fill: var(--entity-state-fill-color, var(--g-color-base-misc-medium));
}

&__pdisk-border {
fill: none;
stroke: var(--entity-state-border-color, var(--g-color-base-misc-heavy));
}

&__vdisk-fill {
stroke: var(
--entity-state-shadow-color,
var(--entity-state-border-color, var(--g-color-base-misc-heavy))
);
stroke-width: 2px;
}
}
140 changes: 140 additions & 0 deletions src/containers/Storage/PDisks/PDisksPreview.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import React from 'react';

import {act, fireEvent, render, screen} from '@testing-library/react';

import type {PreparedPDisk, PreparedVDisk} from '../../../utils/disks/types';
import type {PDisk} from '../PDisk';

import {PDisksPreview, getPDisksPreviewColumnWidth} from './PDisksPreview';

let mockInverted = false;
const mockDetailsById = new Map<string, React.ComponentProps<typeof PDisk>>();
let mockDetailsProps: React.ComponentProps<typeof PDisk>;

jest.mock('../../../utils/hooks/useSetting', () => ({useSetting: () => [mockInverted]}));
jest.mock('../../../store', () => ({singleClusterMode: true}));
jest.mock('../PDisk', () => ({
PDisk: (props: React.ComponentProps<typeof PDisk>) => {
mockDetailsProps = props;
mockDetailsById.set(props.data?.StringifiedId || '', props);
return <button>Expanded PDisk</button>;
},
}));

const pDisk: PreparedPDisk = {NodeId: 1, PDiskId: 1, StringifiedId: '1-1', AllocatedPercent: 25};
const vDisks: PreparedVDisk[] = Array.from({length: 12}, (_, index) => ({
NodeId: 1,
PDiskId: 1,
VDiskSlotId: index,
Severity: 1,
}));

beforeEach(() => {
mockInverted = false;
});

test('preview column width sums actual slot counts and includes gaps and cell padding', () => {
expect(getPDisksPreviewColumnWidth({})).toBe(0);
expect(getPDisksPreviewColumnWidth({PDisks: [pDisk], VDisks: vDisks})).toBe(40);
expect(
getPDisksPreviewColumnWidth({
PDisks: [pDisk, {...pDisk, PDiskId: 2}],
VDisks: [...vDisks, {PDiskId: 99}],
}),
).toBe(48);
});

test('groups identical VDisks and updates the preview on data refresh', () => {
const {container, rerender} = render(<PDisksPreview pDisks={[pDisk]} vDisks={vDisks} />);
const svg = container.querySelector('svg');
if (!svg) {
throw new Error('Missing PDisk preview SVG');
}
expect(screen.getByRole('button', {name: 'Show PDisk 1-1 details'})).toBeVisible();
expect(svg.querySelectorAll('*')).toHaveLength(5);
expect(svg.querySelector('path')?.getAttribute('d')?.match(/M/g)).toHaveLength(12);
expect(svg.querySelector('.ydb-storage-pdisks-preview__pdisk-fill')).toHaveAttribute(
'height',
'10.25',
);

rerender(
<PDisksPreview
pDisks={[{...pDisk, AllocatedPercent: 80, Severity: 5}]}
vDisks={[...vDisks.slice(0, 5), {...vDisks[5], Severity: 5}]}
/>,
);
expect(svg).toHaveAttribute('width', '13');
expect(svg.querySelectorAll('path')).toHaveLength(4);
expect(
Number(
svg.querySelector('.ydb-storage-pdisks-preview__pdisk-fill')?.getAttribute('height'),
),
).toBeCloseTo(32.8);
expect(svg.querySelector('.ydb-storage-pdisks-preview__pdisk-border')).toHaveClass(
'ydb-storage-pdisks-preview__color_red',
);
});

test('inverts PDisk allocation and keeps unrelated disks dimmed', () => {
mockInverted = true;
const {container} = render(
<PDisksPreview pDisks={[pDisk]} vDisks={vDisks} viewContext={{nodeId: '2'}} />,
);
expect(container.querySelector('svg g')).toHaveAttribute('opacity', '0.5');
expect(container.querySelector('.ydb-storage-pdisks-preview__pdisk-fill')).toHaveAttribute(
'height',
'30.75',
);
expect(container.querySelector('.ydb-storage-pdisks-preview__pdisk-fill')).toHaveAttribute(
'y',
'0',
);
expect(container.querySelector('path')?.parentElement).toHaveAttribute('opacity', '0.5');
});

test('closing details clears highlight and ignores callbacks from the previous expansion', () => {
render(<PDisksPreview pDisks={[pDisk]} vDisks={vDisks} />);
fireEvent.click(screen.getByRole('button', {name: 'Show PDisk 1-1 details'}));
expect(mockDetailsProps.highlighted).toBe(false);
const delayedShow = mockDetailsProps.onShowPopup;
fireEvent.click(screen.getByRole('button', {name: 'Expanded PDisk'}), {detail: 1});
fireEvent.click(screen.getByRole('button', {name: 'Show PDisk 1-1 details'}));

act(() => delayedShow?.());
expect(mockDetailsProps.highlighted).toBe(false);
act(() => mockDetailsProps.onShowPopup?.());
expect(mockDetailsProps.highlighted).toBe(true);
});

test('collapsing another group and its delayed hide callback preserve the highlighted owner', () => {
const secondDisk = {...pDisk, PDiskId: 2, StringifiedId: '1-2'};
render(<PDisksPreview pDisks={[pDisk, secondDisk]} vDisks={vDisks} />);
fireEvent.click(screen.getByRole('button', {name: 'Show PDisk 1-1 details'}));
fireEvent.click(screen.getByRole('button', {name: 'Show PDisk 1-2 details'}));
const firstHide = mockDetailsById.get('1-1')?.onHidePopup;
act(() => mockDetailsById.get('1-2')?.onShowPopup?.());
act(() => firstHide?.());
expect(mockDetailsById.get('1-2')?.highlighted).toBe(true);
fireEvent.click(screen.getAllByRole('button', {name: 'Expanded PDisk'})[0], {detail: 1});
expect(mockDetailsById.get('1-2')?.highlighted).toBe(true);
});

test('keyboard activation is not cancelled and Escape collapses only the focused disk', () => {
const outerKeyDown = jest.fn();
render(
<div onKeyDown={outerKeyDown}>
<PDisksPreview pDisks={[pDisk]} vDisks={vDisks} />
</div>,
);
const preview = screen.getByRole('button', {name: 'Show PDisk 1-1 details'});
fireEvent.click(preview);
const details = screen.getByRole('button', {name: 'Expanded PDisk'});
expect(fireEvent.click(details, {detail: 0})).toBe(true);
expect(details).toBeVisible();

fireEvent.keyDown(details, {key: 'Escape'});
expect(outerKeyDown).not.toHaveBeenCalled();
expect(screen.queryByRole('button', {name: 'Expanded PDisk'})).not.toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Show PDisk 1-1 details'})).toHaveFocus();
});
Loading
Loading