Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 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
1 change: 1 addition & 0 deletions src/containers/Nodes/getNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,6 @@ export const getNodes: FetchData<
data: preparedResponse.nodes || [],
found: preparedResponse.found || 0,
total: preparedResponse.total || 0,
columnsSettings: preparedResponse.columnsSettings,
};
};
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;
}
}
110 changes: 110 additions & 0 deletions src/containers/Storage/PDisks/PDisksPreview.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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';
Comment thread
StekPerepolnen marked this conversation as resolved.

import {PDisksPreview} 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('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'}));
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]);
expect(mockDetailsById.get('1-2')?.highlighted).toBe(true);
});
Loading
Loading