Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

- **healthcheck:** expose the drawer target to extensions independently of loading and issue state

## [22.2.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v22.1.0...v22.2.0) (2026-09-24)


Expand Down
6 changes: 6 additions & 0 deletions src/containers/Cluster/ClusterDrawerHealthcheck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import {useTypedSelector} from '../../utils/hooks';
import {Healthcheck} from '../Tenant/Healthcheck/Healthcheck';
import {HealthcheckDrawer} from '../Tenant/Healthcheck/components/HealthcheckDrawer';
import {getDatabaseHealthcheckAssistantTarget} from '../Tenant/Healthcheck/utils';

import i18n from './i18n';

Expand Down Expand Up @@ -184,6 +185,11 @@ export function ClusterDrawerHealthcheck({

return (
<HealthcheckDrawer
target={getDatabaseHealthcheckAssistantTarget({
database,
clusterName,
scope: 'cluster',
})}
isDrawerVisible={Boolean(showHealthcheck) && Boolean(database)}
onCloseDrawer={handleCloseDrawer}
onTransitionInComplete={
Expand Down
1 change: 1 addition & 0 deletions src/containers/Clusters/ClusterDrawerHealthcheck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function ClusterDrawerHealthcheck({

return (
<HealthcheckDrawer
target={clusterName ? {scope: 'cluster', request: {clusterName}} : undefined}
isDrawerVisible={isVisible && Boolean(clusterName)}
onCloseDrawer={handleCloseDrawer}
renderDrawerContent={renderDrawerContent}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import {EnableFullscreenButton} from '../../../../components/EnableFullscreenBut
import type {SelfCheckResult} from '../../../../types/api/healthcheck';
import {uiFactory} from '../../../../uiFactory/uiFactory';
import {createAndDownloadJsonFile} from '../../../../utils/downloadFile';
import type {HealthcheckAssistantTarget} from '../types';

import {HealthcheckDrawerTitle} from './HealthcheckDrawerTitle';

interface HealthcheckDrawerProps {
target?: HealthcheckAssistantTarget;
children: React.ReactNode;
isDrawerVisible: boolean;
onCloseDrawer: () => void;
Expand All @@ -29,6 +31,7 @@ interface HealthcheckDrawerProps {
}

export function HealthcheckDrawer({
target,
children,
isDrawerVisible,
onCloseDrawer,
Expand All @@ -51,11 +54,11 @@ export function HealthcheckDrawer({

return (
<React.Fragment>
{renderDrawerExtension()}
{renderDrawerExtension({target})}
{renderDrawerContent()}
</React.Fragment>
);
}, [renderDrawerContent, renderDrawerExtension]);
}, [renderDrawerContent, renderDrawerExtension, target]);

const handleDownload = React.useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import {SelfCheckResult} from '../../../../../types/api/healthcheck';
import {configureUIFactory, uiFactory} from '../../../../../uiFactory/uiFactory';
import {Healthcheck} from '../../Healthcheck';
import type {HealthcheckAssistantTarget} from '../../types';
import type {useHealthcheck} from '../../useHealthcheck';
import {HealthcheckDrawer} from '../HealthcheckDrawer';

Expand Down Expand Up @@ -48,15 +49,18 @@ function DrawerFixture({
open = true,
database = '/test',
clusterName = 'cluster',
target,
}: {
open?: boolean;
database?: string;
clusterName?: string;
target?: HealthcheckAssistantTarget;
}) {
return (
<ThemeProvider theme="light">
<DrawerContextProvider onRightInsetChange={onInsetChange}>
<HealthcheckDrawer
target={target}
isDrawerVisible={open}
onCloseDrawer={jest.fn()}
renderDrawerContent={() => (
Expand Down Expand Up @@ -147,9 +151,17 @@ describe('Healthcheck drawer extension', () => {
);
});

test('retains the extension and inset across data states, then cleans up on close', () => {
test('preserves the extension, target and inset across data states, then cleans up on close', () => {
const target: HealthcheckAssistantTarget = {
scope: 'cluster',
request: {clusterName: 'alpha'},
};
const renderExtension = jest.fn(() => <Extension />);
configureUIFactory({healthcheck: {renderDrawerExtension: renderExtension}});
const degraded = mockHealthcheck;
const {rerender, unmount} = render(<DrawerFixture />);
const {rerender, unmount} = render(<DrawerFixture target={target} />);
const extension = screen.getByTestId('drawer-extension');
expect(renderExtension).toHaveBeenLastCalledWith({target});
expect(screen.getByTestId('right-inset')).toHaveTextContent('434');
expect(screen.getByRole('button', {name: 'Diagnostics'})).toBeInTheDocument();

Expand All @@ -161,24 +173,32 @@ describe('Healthcheck drawer extension', () => {
];
for (const state of states) {
mockHealthcheck = {...mockHealthcheck, ...state};
rerender(<DrawerFixture />);
expect(screen.getByTestId('drawer-extension')).toBeInTheDocument();
rerender(<DrawerFixture target={target} />);
expect(screen.getByTestId('drawer-extension')).toBe(extension);
expect(renderExtension).toHaveBeenLastCalledWith({target});
expect(screen.getByTestId('right-inset')).toHaveTextContent('434');
expect(screen.queryByRole('button', {name: 'Diagnostics'})).not.toBeInTheDocument();
expect(onInsetChange.mock.calls).toEqual([[434]]);
}

mockHealthcheck = degraded;
rerender(<DrawerFixture />);
rerender(<DrawerFixture target={target} />);
expect(screen.getByRole('button', {name: 'Diagnostics'})).toBeInTheDocument();
expect(onInsetChange.mock.calls).toEqual([[434]]);

const nextTarget: HealthcheckAssistantTarget = {...target, request: {clusterName: 'beta'}};
rerender(<DrawerFixture target={nextTarget} />);
expect(renderExtension).toHaveBeenLastCalledWith({target: nextTarget});
expect(screen.getByTestId('drawer-extension')).toBe(extension);
expect(onInsetChange.mock.calls).toEqual([[434]]);

rerender(<DrawerFixture open={false} />);
expect(screen.queryByTestId('drawer-extension')).not.toBeInTheDocument();
expect(screen.getByTestId('right-inset')).toHaveTextContent('0');
expect(onInsetChange.mock.calls).toEqual([[434], [0]]);

rerender(<DrawerFixture />);
expect(renderExtension).toHaveBeenLastCalledWith({target: undefined});
expect(onInsetChange.mock.calls).toEqual([[434], [0], [434]]);
unmount();
expect(onInsetChange.mock.calls).toEqual([[434], [0], [434], [0]]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React from 'react';

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

import type {PreparedTenant} from '../../../../../store/reducers/tenants/types';
import {ClusterDrawerHealthcheck as ClusterPageDrawer} from '../../../../Cluster/ClusterDrawerHealthcheck';
import {ClusterDrawerHealthcheck as ClustersDrawer} from '../../../../Clusters/ClusterDrawerHealthcheck';
import {DatabaseDrawerHealthcheck} from '../../../../Tenants/DatabaseDrawerHealthcheck';
import {TenantDrawerHealthcheck} from '../../../TenantDrawerHealthcheck';
import type {HealthcheckAssistantTarget} from '../../types';
import {HealthcheckDrawer} from '../HealthcheckDrawer';

jest.mock('../HealthcheckDrawer', () => ({
HealthcheckDrawer: jest.fn(({children}: {children: React.ReactNode}) => children),
}));
jest.mock('../../../../../utils/hooks', () => ({useTypedSelector: () => undefined}));
jest.mock('../../../TenantContext', () => ({useCurrentSchema: () => ({database: '/Root/db'})}));
jest.mock('../../../useTenantQueryParams', () => ({
useTenantQueryParams: () => ({
showHealthcheck: true,
handleShowHealthcheckChange: jest.fn(),
handleIssuesFilterChange: jest.fn(),
handleHealthcheckViewChange: jest.fn(),
}),
}));
jest.mock('use-query-params', () => ({
BooleanParam: {},
StringParam: {},
useQueryParams: () => [{showHealthcheck: true}, jest.fn()],
}));
jest.mock('../../Healthcheck', () => ({Healthcheck: () => null}));

const tenant: PreparedTenant = {
Name: '/Root/db',
Cluster: 'tenant-cluster',
Type: 'Dedicated',
sharedTenantName: undefined,
sharedNodeIds: undefined,
controlPlaneName: '',
cpu: undefined,
memory: undefined,
storage: undefined,
nodesCount: 0,
groupsCount: 0,
};

describe('Healthcheck drawer target owners', () => {
beforeEach(() => jest.clearAllMocks());

test.each<[string, React.ReactElement, HealthcheckAssistantTarget]>([
[
'all clusters',
<ClustersDrawer clusterName="alpha" isVisible onClose={jest.fn()} children={null} />,
{scope: 'cluster', request: {clusterName: 'alpha'}},
],
[
'cluster page',
<ClusterPageDrawer clusterName="alpha" database="/Root" children={null} />,
{scope: 'cluster', request: {clusterName: 'alpha', database: '/Root'}},
],
[
'database page',
<TenantDrawerHealthcheck clusterName="alpha" children={null} />,
{scope: 'database', request: {clusterName: 'alpha', database: '/Root/db'}},
],
])('passes the %s target before data resolves', (_name, element, target) => {
render(element);
expect(jest.mocked(HealthcheckDrawer).mock.lastCall?.[0].target).toEqual(target);
});

test.each([undefined, 'explicit-cluster'])(
'database list uses selected identity, with explicit cluster %s',
(clusterName) => {
render(
<DatabaseDrawerHealthcheck clusterName={clusterName}>
{(onStatusClick) => (
<button onClick={() => onStatusClick(tenant, '/Root/db')}>
Open healthcheck
</button>
)}
</DatabaseDrawerHealthcheck>,
);
fireEvent.click(screen.getByRole('button', {name: 'Open healthcheck'}));
expect(jest.mocked(HealthcheckDrawer).mock.lastCall?.[0].target).toEqual({
scope: 'database',
request: {database: '/Root/db', clusterName: clusterName ?? 'tenant-cluster'},
});
},
);
});
10 changes: 10 additions & 0 deletions src/containers/Tenant/TenantDrawerHealthcheck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {useTypedSelector} from '../../utils/hooks';

import {Healthcheck} from './Healthcheck/Healthcheck';
import {HealthcheckDrawer} from './Healthcheck/components/HealthcheckDrawer';
import {getDatabaseHealthcheckAssistantTarget} from './Healthcheck/utils';
import {useCurrentSchema} from './TenantContext';
import i18n from './i18n';
import {useTenantQueryParams} from './useTenantQueryParams';
Expand Down Expand Up @@ -46,6 +47,15 @@ export function TenantDrawerHealthcheck({children, clusterName}: TenantDrawerHea

return (
<HealthcheckDrawer
target={
database
? getDatabaseHealthcheckAssistantTarget({
database,
clusterName,
scope: 'database',
})
: undefined
}
isDrawerVisible={Boolean(showHealthcheck)}
onCloseDrawer={handleCloseDrawer}
renderDrawerContent={renderDrawerContent}
Expand Down
10 changes: 10 additions & 0 deletions src/containers/Tenants/DatabaseDrawerHealthcheck.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {PreparedTenant} from '../../store/reducers/tenants/types';
import {useTypedSelector} from '../../utils/hooks';
import {Healthcheck} from '../Tenant/Healthcheck/Healthcheck';
import {HealthcheckDrawer} from '../Tenant/Healthcheck/components/HealthcheckDrawer';
import {getDatabaseHealthcheckAssistantTarget} from '../Tenant/Healthcheck/utils';
import tenantI18n from '../Tenant/i18n';
import {useTenantQueryParams} from '../Tenant/useTenantQueryParams';

Expand Down Expand Up @@ -78,6 +79,15 @@ export function DatabaseDrawerHealthcheck({children, clusterName}: DatabaseDrawe

return (
<HealthcheckDrawer
target={
database
? getDatabaseHealthcheckAssistantTarget({
database,
clusterName: selectedClusterName,
scope: 'database',
})
: undefined
}
isDrawerVisible={Boolean(database)}
onCloseDrawer={handleCloseDrawer}
renderDrawerContent={renderDrawerContent}
Expand Down
1 change: 1 addition & 0 deletions src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export type {
IllustrationComponent,
IllustrationName,
RenderHealthcheckAssistantAction,
HealthcheckDrawerExtensionProps,
} from './uiFactory/types';
export type {
HealthcheckAssistantActionProps,
Expand Down
11 changes: 9 additions & 2 deletions src/uiFactory/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import type {
GetHealthcheckViewTitles,
GetHealthcheckViewsOrder,
} from '../containers/Tenant/Healthcheck/shared';
import type {HealthcheckAssistantActionProps} from '../containers/Tenant/Healthcheck/types';
import type {
HealthcheckAssistantActionProps,
HealthcheckAssistantTarget,
} from '../containers/Tenant/Healthcheck/types';
import type {ClusterInfo} from '../store/reducers/cluster/cluster';
import type {PreparedStorageNode} from '../store/reducers/storage/types';
import type {PreparedTenant} from '../store/reducers/tenants/types';
Expand Down Expand Up @@ -70,7 +73,7 @@ export interface UIFactory<H extends string = CommonIssueCategory, T extends str
* Renders after the drawer header and before Healthcheck content.
* Stays mounted while the drawer is open, independent of loading, error or issue state.
*/
renderDrawerExtension?: () => React.ReactNode;
renderDrawerExtension?: (props: HealthcheckDrawerExtensionProps) => React.ReactNode;
};
hasAccess: HasAccess;
hideGrantAccess?: boolean;
Expand Down Expand Up @@ -211,6 +214,10 @@ export type RenderNodeTooltipActions = (props: {data?: PreparedStorageNode}) =>

export type RenderChatPanel = () => React.ReactNode;

export interface HealthcheckDrawerExtensionProps {
target?: HealthcheckAssistantTarget;
}

export type RenderHealthcheckAssistantAction = (
props: HealthcheckAssistantActionProps,
) => React.ReactNode;
Expand Down
Loading