Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -200,6 +204,49 @@ describe('Healthcheck drawer extension', () => {
});
});

test('passes explicit target without mounting actions and updates it without remounting the extension', () => {
const targets = jest.fn();
configureUIFactory({
healthcheck: {
renderDrawerExtension: (props) => {
targets(props?.target);
return <Extension />;
},
},
});
const alpha: HealthcheckAssistantTarget = {
scope: 'cluster',
request: {clusterName: 'alpha'},
};
const beta: HealthcheckAssistantTarget = {scope: 'cluster', request: {clusterName: 'beta'}};
mockHealthcheck = {...mockHealthcheck, loading: true, successful: false};
const {rerender} = render(<DrawerFixture target={alpha} />);
const extension = screen.getByTestId('drawer-extension');
expect(targets).toHaveBeenLastCalledWith(alpha);
for (const state of [
{loading: false, error: {message: 'Unavailable'}},
{
error: undefined,
successful: true,
selfCheckResult: SelfCheckResult.GOOD,
issues: [],
leavesIssues: [],
},
]) {
mockHealthcheck = {...mockHealthcheck, ...state};
rerender(<DrawerFixture target={alpha} />);
expect(targets).toHaveBeenLastCalledWith(alpha);
expect(screen.getByTestId('drawer-extension')).toBe(extension);
expect(screen.queryByRole('button', {name: 'Diagnostics'})).not.toBeInTheDocument();
}
rerender(<DrawerFixture target={beta} />);
expect(targets).toHaveBeenLastCalledWith(beta);
expect(screen.getByTestId('drawer-extension')).toBe(extension);
expect(onInsetChange.mock.calls).toEqual([[434]]);
rerender(<DrawerFixture />);
expect(targets).toHaveBeenLastCalledWith(undefined);
});

test('renders the extension after the header and before Healthcheck', () => {
render(<DrawerFixture />);
const extension = screen.getByTestId('drawer-extension');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import React from 'react';

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

import type {PreparedTenant} from '../../../../../store/reducers/tenants/types';
import {configureUIFactory, uiFactory} from '../../../../../uiFactory/uiFactory';
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';

jest.mock('../../../../../components/Drawer', () => ({
DrawerWrapper: ({
isDrawerVisible,
renderDrawerContent,
children,
}: {
isDrawerVisible: boolean;
renderDrawerContent: () => React.ReactNode;
children: React.ReactNode;
}) => (
<div>
{children}
{isDrawerVisible && renderDrawerContent()}
</div>
),
}));
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: () => <div>Loading healthcheck</div>}));

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', () => {
const originalExtension = uiFactory.healthcheck.renderDrawerExtension;
const targets = jest.fn();
beforeEach(() => {
targets.mockClear();
configureUIFactory({
healthcheck: {
renderDrawerExtension: (props) => {
targets(props?.target);
return null;
},
},
});
});
afterEach(() => configureUIFactory({healthcheck: {renderDrawerExtension: originalExtension}}));

test.each<{name: string; element: React.ReactElement; target: HealthcheckAssistantTarget}>([
{
name: 'all clusters',
element: (
<ClustersDrawer clusterName="alpha" isVisible onClose={jest.fn()}>
Page
</ClustersDrawer>
),
target: {scope: 'cluster', request: {clusterName: 'alpha'}},
},
{
name: 'cluster page',
element: (
<ClusterPageDrawer clusterName="alpha" database="/Root">
Page
</ClusterPageDrawer>
),
target: {scope: 'cluster', request: {clusterName: 'alpha', database: '/Root'}},
},
{
name: 'database page',
element: <TenantDrawerHealthcheck clusterName="alpha">Page</TenantDrawerHealthcheck>,
target: {scope: 'database', request: {clusterName: 'alpha', database: '/Root/db'}},
},
])('provides $name identity before data resolves', ({element, target}) => {
render(element);
expect(targets).toHaveBeenLastCalledWith(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>,
);
expect(targets).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', {name: 'Open healthcheck'}));
expect(targets).toHaveBeenLastCalledWith({
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