From 21ae704da41059625823bcdcc3d727b786c73ca8 Mon Sep 17 00:00:00 2001 From: raintygao Date: Tue, 29 Aug 2023 17:35:17 +0800 Subject: [PATCH] update preview panel for external models (#252) * feat: update preview panel for external models Signed-off-by: tygao * feat: update preview panel for external models Signed-off-by: tygao --------- Signed-off-by: tygao (cherry picked from commit 9bbd25f34c74982d324efc8eac221b42417f1174) --- .../__tests__/connector_details.test.tsx | 36 ++++++ .../__tests__/nodes_table.test.tsx | 5 +- .../__tests__/preview_panel.test.tsx | 33 ++++-- .../preview_panel/connector_details.tsx | 62 ++++++++++ public/components/preview_panel/index.tsx | 107 ++++++++++++------ .../components/preview_panel/nodes_table.tsx | 43 ++++--- 6 files changed, 231 insertions(+), 55 deletions(-) create mode 100644 public/components/preview_panel/__tests__/connector_details.test.tsx create mode 100644 public/components/preview_panel/connector_details.tsx diff --git a/public/components/preview_panel/__tests__/connector_details.test.tsx b/public/components/preview_panel/__tests__/connector_details.test.tsx new file mode 100644 index 00000000..22290d35 --- /dev/null +++ b/public/components/preview_panel/__tests__/connector_details.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { render, screen } from '../../../../test/test_utils'; +import { ConnectorDetails } from '../connector_details'; + +function setup({ name = 'name', id = 'id', description = 'description' }) { + const user = userEvent.setup({}); + render(); + return { user }; +} + +describe('', () => { + it('should render connector details', () => { + setup({}); + expect(screen.getByText('Connector name')).toBeInTheDocument(); + expect(screen.getByText('Connector ID')).toBeInTheDocument(); + expect(screen.getByText('Connector description')).toBeInTheDocument(); + }); + + it('should render - when id is empty', () => { + setup({ id: '' }); + expect(screen.getByText('-')).toBeInTheDocument(); + expect(screen.queryByTestId('copyable-text-div')).not.toBeInTheDocument(); + }); + + it('should render id and copy id button when id is not empty', () => { + setup({ id: 'connector-id' }); + expect(screen.getByText('connector-id')).toBeInTheDocument(); + expect(screen.queryByTestId('copyable-text-div')).toBeInTheDocument(); + }); +}); diff --git a/public/components/preview_panel/__tests__/nodes_table.test.tsx b/public/components/preview_panel/__tests__/nodes_table.test.tsx index 2a9a2feb..dfba1868 100644 --- a/public/components/preview_panel/__tests__/nodes_table.test.tsx +++ b/public/components/preview_panel/__tests__/nodes_table.test.tsx @@ -19,9 +19,9 @@ const NODES = [ }, ]; -function setup({ nodes = NODES, loading = false }) { +function setup({ nodes = NODES, loading = false, nodesStatus = 'Responding on 1 of 2 nodes' }) { const user = userEvent.setup({}); - render(); + render(); return { user }; } @@ -31,6 +31,7 @@ describe('', () => { expect(screen.getAllByRole('columnheader').length).toBe(2); expect(screen.getByText('id1')).toBeInTheDocument(); expect(screen.getByText('id2')).toBeInTheDocument(); + expect(screen.getByText('Responding on 1 of 2 nodes')).toBeInTheDocument(); }); it('should render status at first column with asc by default', () => { diff --git a/public/components/preview_panel/__tests__/preview_panel.test.tsx b/public/components/preview_panel/__tests__/preview_panel.test.tsx index fed8a902..7da93f4b 100644 --- a/public/components/preview_panel/__tests__/preview_panel.test.tsx +++ b/public/components/preview_panel/__tests__/preview_panel.test.tsx @@ -13,9 +13,6 @@ const MODEL = { id: 'id1', name: 'test', planningWorkerNodes: ['node-1', 'node-2', 'node-3'], - connector: { - name: 'Connector', - }, }; function setup({ model = MODEL, onClose = jest.fn() }) { @@ -29,11 +26,30 @@ describe('', () => { jest.clearAllMocks(); }); - it('should render id, name and source in panel', () => { + it('should render id, name in panel', () => { setup({}); expect(screen.getByText('test')).toBeInTheDocument(); expect(screen.getByText('id1')).toBeInTheDocument(); + }); + + it('source should be local and should not render connector details when no connector params passed', async () => { + setup({}); + expect(screen.getByText('Local')).toBeInTheDocument(); + expect(screen.queryByText('Connector details')).not.toBeInTheDocument(); + }); + + it('source should be external and should not render nodes details when connector params passed', async () => { + const modelWithConntector = { + ...MODEL, + connector: { + name: 'connector', + }, + }; + setup({ + model: modelWithConntector, + }); expect(screen.getByText('External')).toBeInTheDocument(); + expect(screen.queryByText('Status by node')).not.toBeInTheDocument(); }); it('should call onClose when close panel', async () => { @@ -44,7 +60,7 @@ describe('', () => { expect(onClose).toHaveBeenCalled(); }); - it('should render loading when not responding and render partially state when responding', async () => { + it('should render loading when local model not responding and render partially state when responding', async () => { const request = jest.spyOn(APIProvider.getAPI('profile'), 'getModel'); const mockResult = { id: 'model-1-id', @@ -55,9 +71,10 @@ describe('', () => { request.mockResolvedValue(mockResult); setup({}); expect(screen.getByTestId('preview-panel-color-loading-text')).toBeInTheDocument(); - await waitFor(() => - expect(screen.getByText('Partially responding on 2 of 3 nodes')).toBeInTheDocument() - ); + await waitFor(() => { + expect(screen.getByText('Partially responding')).toBeInTheDocument(); + expect(screen.getByText('Responding on 2 of 3 nodes')).toBeInTheDocument(); + }); }); it('should render not responding when no model profile API response', async () => { diff --git a/public/components/preview_panel/connector_details.tsx b/public/components/preview_panel/connector_details.tsx new file mode 100644 index 00000000..0d48f881 --- /dev/null +++ b/public/components/preview_panel/connector_details.tsx @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + EuiDescriptionList, + EuiDescriptionListTitle, + EuiTitle, + EuiSpacer, + EuiDescriptionListDescription, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { CopyableText } from '../common'; + +export const ConnectorDetails = (props: { name?: string; id?: string; description?: string }) => { + const { name, id, description } = props; + return ( + <> + + +

Connector details

+
+ + + + + + +
Connector name
+
+
+ {name} +
+ + + +
Connector ID
+
+
+ + {id ? ( + + ) : ( + '-' + )} + +
+
+ + + +
Connector description
+
+
+ {description} +
+ + ); +}; diff --git a/public/components/preview_panel/index.tsx b/public/components/preview_panel/index.tsx index 47b26ea5..406dda5e 100644 --- a/public/components/preview_panel/index.tsx +++ b/public/components/preview_panel/index.tsx @@ -15,12 +15,15 @@ import { EuiDescriptionListDescription, EuiSpacer, EuiTextColor, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { APIProvider } from '../../apis/api_provider'; import { useFetcher } from '../../hooks/use_fetcher'; import { NodesTable } from './nodes_table'; import { CopyableText } from '../common'; import { ModelDeploymentProfile } from '../../apis/profile'; +import { ConnectorDetails } from './connector_details'; export interface INode { id: string; @@ -60,33 +63,45 @@ export const PreviewPanel = ({ onClose, model }: Props) => { const respondingStatus = useMemo(() => { if (loading) { - return ( - - Loading... - - ); + return { + overall: ( + + Loading... + + ), + nodes: 'Loading...', + }; } const deployedNodesNum = nodes.filter(({ deployed }) => deployed).length; const targetNodesNum = nodes.length; if (deployedNodesNum === 0) { - return ( - - Not responding on {targetNodesNum} of {targetNodesNum} nodes - - ); + return { + overall: ( + + Not responding + + ), + nodes: `Not responding on ${targetNodesNum} of ${targetNodesNum} nodes`, + }; } if (deployedNodesNum < targetNodesNum) { - return ( - - Partially responding on {deployedNodesNum} of {targetNodesNum} nodes - - ); + return { + overall: ( + + Partially responding + + ), + nodes: `Responding on ${deployedNodesNum} of ${targetNodesNum} nodes`, + }; } - return ( - - Responding on {deployedNodesNum} of {targetNodesNum} nodes - - ); + return { + overall: ( + + Responding + + ), + nodes: `Responding on ${deployedNodesNum} of ${targetNodesNum} nodes`, + }; }, [nodes, loading]); const onCloseFlyout = useCallback(() => { @@ -95,26 +110,54 @@ export const PreviewPanel = ({ onClose, model }: Props) => { return ( - - -

{name}

+ + +

{name}

- Model ID + + + + +
Status
+
+
+ + {respondingStatus.overall} + +
+ + + +
Source
+
+
+ + {connector ? 'External' : 'Local'} + +
+
+ + + +
Model ID
+
+
- Source - - {connector ? 'External' : 'Local'} - - Model status by node - {respondingStatus}
- - + {connector ? ( + + ) : ( + + )}
); diff --git a/public/components/preview_panel/nodes_table.tsx b/public/components/preview_panel/nodes_table.tsx index 1383fe5b..6355d5dd 100644 --- a/public/components/preview_panel/nodes_table.tsx +++ b/public/components/preview_panel/nodes_table.tsx @@ -14,11 +14,16 @@ import { EuiEmptyPrompt, EuiCopy, EuiText, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiTitle, + EuiSpacer, + EuiDescriptionListDescription, } from '@elastic/eui'; import { INode } from './'; -export function NodesTable(props: { nodes: INode[]; loading: boolean }) { - const { nodes, loading } = props; +export function NodesTable(props: { nodes: INode[]; loading: boolean; nodesStatus: string }) { + const { nodes, loading, nodesStatus } = props; const [sort, setSort] = useState<{ field: keyof INode; direction: Direction }>({ field: 'deployed', direction: 'asc', @@ -108,16 +113,28 @@ export function NodesTable(props: { nodes: INode[]; loading: boolean }) { ); return ( - - columns={columns} - items={items} - sorting={{ sort }} - pagination={pagination} - onChange={handleTableChange} - loading={loading} - noItemsMessage={ - loading ? Loading...} aria-label="loading nodes" /> : undefined - } - /> + <> + + + + +

Status by node

+
+
+ {nodesStatus} +
+ + + columns={columns} + items={items} + sorting={{ sort }} + pagination={pagination} + onChange={handleTableChange} + loading={loading} + noItemsMessage={ + loading ? Loading...} aria-label="loading nodes" /> : undefined + } + /> + ); }