From 007747647997c1ad852272acb67a68b816fb6908 Mon Sep 17 00:00:00 2001 From: arbulu89 Date: Wed, 10 Jul 2024 13:29:41 +0200 Subject: [PATCH 1/3] Create ClusterTypeLabel component --- .../ClusterTypeLabel/ClusterTypeLabel.jsx | 56 +++++++++++++++++ .../ClusterTypeLabel.test.jsx | 61 +++++++++++++++++++ assets/js/common/ClusterTypeLabel/index.js | 3 + assets/js/lib/model/clusters.js | 3 + 4 files changed, 123 insertions(+) create mode 100644 assets/js/common/ClusterTypeLabel/ClusterTypeLabel.jsx create mode 100644 assets/js/common/ClusterTypeLabel/ClusterTypeLabel.test.jsx create mode 100644 assets/js/common/ClusterTypeLabel/index.js diff --git a/assets/js/common/ClusterTypeLabel/ClusterTypeLabel.jsx b/assets/js/common/ClusterTypeLabel/ClusterTypeLabel.jsx new file mode 100644 index 0000000000..d87e6770a1 --- /dev/null +++ b/assets/js/common/ClusterTypeLabel/ClusterTypeLabel.jsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import { Link } from 'react-router-dom'; +import { EOS_INFO_OUTLINED, EOS_STAR } from 'eos-icons-react'; + +import Tooltip from '@common/Tooltip'; +import { + ANGI_ARCHITECTURE, + CLASSIC_ARCHITECTURE, + getClusterTypeLabel, +} from '@lib/model/clusters'; + +const MIGRATION_URL = + 'https://www.suse.com/c/how-to-upgrade-to-saphanasr-angi/'; +const ANGI_TOOLTIP_MESSAGE = 'Angi architecture'; +const CLASSIC_TOOLTIP_MESSAGE = ( + <> + Classic architecture. Recommended{' '} + + migration + {' '} + to Angi architecture + +); + +const icons = { + [ANGI_ARCHITECTURE]: ( + + + + ), + [CLASSIC_ARCHITECTURE]: ( + + + + ), +}; + +function ClusterTypeLabel({ clusterType, architectureType }) { + return ( + + {icons[architectureType]} + {getClusterTypeLabel(clusterType)} + + ); +} + +export default ClusterTypeLabel; diff --git a/assets/js/common/ClusterTypeLabel/ClusterTypeLabel.test.jsx b/assets/js/common/ClusterTypeLabel/ClusterTypeLabel.test.jsx new file mode 100644 index 0000000000..ede2b47624 --- /dev/null +++ b/assets/js/common/ClusterTypeLabel/ClusterTypeLabel.test.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; + +import { renderWithRouter } from '@lib/test-utils'; + +import ClusterTypeLabel from './ClusterTypeLabel'; + +describe('ClusterTypeLabel component', () => { + it.each([ + { clusterType: 'hana_scale_up', label: 'HANA Scale Up' }, + { clusterType: 'hana_scale_out', label: 'HANA Scale Out' }, + { clusterType: 'ascs_ers', label: 'ASCS/ERS' }, + ])( + 'should display the correct $clusterType cluster type label', + ({ clusterType, label }) => { + render( + + ); + expect(screen.getByText(label)).toBeVisible(); + } + ); + + it('should display a green star icon when the cluster uses angi architecture', async () => { + const user = userEvent.setup(); + + render( + + ); + expect(screen.getByText('HANA Scale Up')).toBeTruthy(); + + const icon = screen.getByTestId('eos-svg-component'); + expect(icon.classList.toString()).toContain('fill-jungle-green-500'); + + await user.hover(icon); + expect(screen.getByText('Angi architecture')).toBeInTheDocument(); + }); + + it('should display an info icon when the cluster uses classic architecture', async () => { + const user = userEvent.setup(); + + renderWithRouter( + + ); + expect(screen.getByText('HANA Scale Up')).toBeTruthy(); + + const icon = screen.getByTestId('eos-svg-component'); + + await user.hover(icon); + expect( + screen.getByText('Classic architecture', { exact: false }) + ).toBeInTheDocument(); + }); +}); diff --git a/assets/js/common/ClusterTypeLabel/index.js b/assets/js/common/ClusterTypeLabel/index.js new file mode 100644 index 0000000000..1c4ac7d027 --- /dev/null +++ b/assets/js/common/ClusterTypeLabel/index.js @@ -0,0 +1,3 @@ +import ClusterTypeLabel from './ClusterTypeLabel'; + +export default ClusterTypeLabel; diff --git a/assets/js/lib/model/clusters.js b/assets/js/lib/model/clusters.js index 6c78a3ff51..e9d9af0533 100644 --- a/assets/js/lib/model/clusters.js +++ b/assets/js/lib/model/clusters.js @@ -16,6 +16,9 @@ const clusterTypeLabels = { export const getClusterTypeLabel = (type) => clusterTypeLabels[type] || 'Unknown'; +export const ANGI_ARCHITECTURE = 'angi'; +export const CLASSIC_ARCHITECTURE = 'classic'; + export const FS_TYPE_RESOURCE_MANAGED = 'resource_managed'; export const FS_TYPE_SIMPLE_MOUNT = 'simple_mount'; export const FS_TYPE_MIXED = 'mixed_fs_types'; From 8490cbe5fa63615dc07f9cd61126926236d22b96 Mon Sep 17 00:00:00 2001 From: arbulu89 Date: Wed, 10 Jul 2024 13:30:14 +0200 Subject: [PATCH 2/3] Update checks env functions to include new arch type field --- assets/js/lib/checks/env.js | 2 ++ assets/js/lib/checks/env.test.js | 28 ++++++++++++++++++++++------ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/assets/js/lib/checks/env.js b/assets/js/lib/checks/env.js index 188da93e48..25861cb707 100644 --- a/assets/js/lib/checks/env.js +++ b/assets/js/lib/checks/env.js @@ -6,6 +6,7 @@ export const buildEnv = ({ cluster_type, ensa_version, filesystem_type, + architecture_type, }) => { switch (cluster_type) { case ASCS_ERS: { @@ -22,6 +23,7 @@ export const buildEnv = ({ provider, target_type, cluster_type, + architecture_type, }; } } diff --git a/assets/js/lib/checks/env.test.js b/assets/js/lib/checks/env.test.js index 672567e725..4268d92352 100644 --- a/assets/js/lib/checks/env.test.js +++ b/assets/js/lib/checks/env.test.js @@ -1,37 +1,53 @@ import { faker } from '@faker-js/faker'; -import { has } from 'lodash'; +import { pick } from 'lodash'; import { ASCS_ERS, HANA_SCALE_UP } from '@lib/model/clusters'; import { buildEnv } from '.'; describe('buildEnv', () => { - it('should build and env with filesystem type and ensa version for ASCS/ERS clusters', () => { + it('should build and env for ASCS/ERS clusters', () => { const payload = { cluster_type: ASCS_ERS, provider: faker.color.rgb(), target_type: faker.hacker.noun(), ensa_version: faker.number.int(), filesystem_type: faker.animal.dog(), + architecture_type: faker.hacker.noun(), }; + const expectedPayload = pick(payload, [ + 'cluster_type', + 'provider', + 'target_type', + 'ensa_version', + 'filesystem_type', + ]); + const env = buildEnv(payload); - expect(env).toEqual(payload); + expect(env).toEqual(expectedPayload); }); - it('should build and env without filesystem type and ensa version for other clusters', () => { + it('should build and env for HANA clusters', () => { const payload = { cluster_type: HANA_SCALE_UP, provider: faker.color.rgb(), target_type: faker.hacker.noun(), ensa_version: faker.number.int(), filesystem_type: faker.animal.dog(), + architecture_type: faker.hacker.noun(), }; + const expectedPayload = pick(payload, [ + 'cluster_type', + 'provider', + 'target_type', + 'architecture_type', + ]); + const env = buildEnv(payload); - expect(has(env, 'ensa_version')).toBe(false); - expect(has(env, 'filesystem_type')).toBe(false); + expect(env).toEqual(expectedPayload); }); }); From 4aefb0852cc8e075b98589e27eef4fd665ac497f Mon Sep 17 00:00:00 2001 From: arbulu89 Date: Wed, 10 Jul 2024 14:27:32 +0200 Subject: [PATCH 3/3] Use ClusterTypeLabel in other views --- .../common/ClusterInfoBox/ClusterInfoBox.jsx | 14 ++++-- .../ClusterInfoBox/ClusterInfoBox.test.jsx | 34 +++++++++++++- .../js/lib/test-utils/factories/clusters.js | 4 ++ .../ClusterDetails/ClusterDetailsPage.jsx | 4 +- .../ClusterDetails/HanaClusterDetails.jsx | 10 +++- .../HanaClusterDetails.stories.jsx | 16 ++++++- .../HanaClusterDetails.test.jsx | 47 +++++++++++++++++++ .../ClusterSettingsPage.jsx | 8 +++- .../pages/ExecutionResults/TargetInfoBox.jsx | 9 +++- 9 files changed, 134 insertions(+), 12 deletions(-) diff --git a/assets/js/common/ClusterInfoBox/ClusterInfoBox.jsx b/assets/js/common/ClusterInfoBox/ClusterInfoBox.jsx index c2ac80be46..10f82564b4 100644 --- a/assets/js/common/ClusterInfoBox/ClusterInfoBox.jsx +++ b/assets/js/common/ClusterInfoBox/ClusterInfoBox.jsx @@ -1,12 +1,10 @@ import React from 'react'; -import { getClusterTypeLabel } from '@lib/model/clusters'; - import ListView from '@common/ListView'; import ProviderLabel from '@common/ProviderLabel'; +import ClusterTypeLabel from '@common/ClusterTypeLabel'; -// eslint-disable-next-line import/prefer-default-export -function ClusterInfoBox({ haScenario, provider }) { +function ClusterInfoBox({ haScenario, provider, architectureType }) { return (
( + + ), }, { title: 'Provider', diff --git a/assets/js/common/ClusterInfoBox/ClusterInfoBox.test.jsx b/assets/js/common/ClusterInfoBox/ClusterInfoBox.test.jsx index e1339b3374..1a6499f0ee 100644 --- a/assets/js/common/ClusterInfoBox/ClusterInfoBox.test.jsx +++ b/assets/js/common/ClusterInfoBox/ClusterInfoBox.test.jsx @@ -1,6 +1,10 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { screen, render } from '@testing-library/react'; import '@testing-library/jest-dom'; +import userEvent from '@testing-library/user-event'; + +import { renderWithRouter } from '@lib/test-utils'; + import ClusterInfoBox from './ClusterInfoBox'; describe('Cluster Info Box', () => { @@ -62,4 +66,32 @@ describe('Cluster Info Box', () => { expect(getByText(haScenarioText)).toBeTruthy(); }); }); + + it.each([ + { architectureType: 'classic', tooltip: 'Classic architecture' }, + { architectureType: 'angi', tooltip: 'Angi architecture' }, + ])( + 'should display architecture type icon', + async ({ architectureType, tooltip }) => { + const user = userEvent.setup(); + + renderWithRouter( + + ); + + const icon = screen.getByTestId('eos-svg-component'); + + await user.hover(icon); + expect(screen.getByText(tooltip, { exact: false })).toBeInTheDocument(); + } + ); + + it('should not display architecture type icon if architecture is unknown', () => { + render(); + expect(screen.queryByTestId('eos-svg-component')).not.toBeInTheDocument(); + }); }); diff --git a/assets/js/lib/test-utils/factories/clusters.js b/assets/js/lib/test-utils/factories/clusters.js index 5f4bab1bf3..7889b6258f 100644 --- a/assets/js/lib/test-utils/factories/clusters.js +++ b/assets/js/lib/test-utils/factories/clusters.js @@ -17,6 +17,9 @@ const hanaStatus = () => faker.helpers.arrayElement(['Primary', 'Failed']); const ascsErsRole = () => faker.helpers.arrayElement(['ascs', 'ers']); +const hanaArchitectureTypeEnum = () => + faker.helpers.arrayElement(['classic', 'angi']); + export const sbdDevicesFactory = Factory.define(() => ({ device: faker.system.filePath(), status: faker.helpers.arrayElement(['healthy', 'unhealthy']), @@ -72,6 +75,7 @@ export const hanaClusterDetailsFactory = Factory.define(() => { system_replication_mode: 'sync', system_replication_operation_mode: 'logreplay', maintenance_mode: false, + architecture_type: hanaArchitectureTypeEnum(), }; }); diff --git a/assets/js/pages/ClusterDetails/ClusterDetailsPage.jsx b/assets/js/pages/ClusterDetails/ClusterDetailsPage.jsx index 4902eacf11..d6a808c6ed 100644 --- a/assets/js/pages/ClusterDetails/ClusterDetailsPage.jsx +++ b/assets/js/pages/ClusterDetails/ClusterDetailsPage.jsx @@ -32,6 +32,7 @@ export function ClusterDetailsPage() { const provider = get(cluster, 'provider'); const type = get(cluster, 'type'); + const architectureType = get(cluster, 'details.architecture_type'); const catalog = useSelector(getCatalog()); @@ -51,13 +52,14 @@ export function ClusterDetailsPage() { cluster_type: type, ensa_version: ensaVersion, filesystem_type: filesystemType, + architecture_type: architectureType, }); if (provider && type) { dispatch(updateCatalog(env)); dispatch(updateLastExecution(clusterID)); } - }, [dispatch, provider, type, ensaVersion, filesystemType]); + }, [dispatch, provider, type, ensaVersion, filesystemType, architectureType]); const clusterHosts = useSelector((state) => getClusterHosts(state, clusterID) diff --git a/assets/js/pages/ClusterDetails/HanaClusterDetails.jsx b/assets/js/pages/ClusterDetails/HanaClusterDetails.jsx index c9861ae9f3..b9b3bf1bdd 100644 --- a/assets/js/pages/ClusterDetails/HanaClusterDetails.jsx +++ b/assets/js/pages/ClusterDetails/HanaClusterDetails.jsx @@ -2,7 +2,6 @@ import React from 'react'; import { get, capitalize, sortBy } from 'lodash'; import classNames from 'classnames'; -import { getClusterTypeLabel } from '@lib/model/clusters'; import { RUNNING_STATES } from '@state/lastExecutions'; import BackButton from '@common/BackButton'; @@ -10,6 +9,7 @@ import Button from '@common/Button'; import ListView from '@common/ListView'; import PageHeader from '@common/PageHeader'; import ProviderLabel from '@common/ProviderLabel'; +import ClusterTypeLabel from '@common/ClusterTypeLabel'; import SapSystemLink from '@common/SapSystemLink'; import Tooltip from '@common/Tooltip'; import DisabledGuard from '@common/DisabledGuard'; @@ -162,7 +162,13 @@ function HanaClusterDetails({ }, { title: 'Cluster type', - content: getClusterTypeLabel(clusterType), + content: clusterType, + render: (content) => ( + + ), }, { title: 'Cluster maintenance', diff --git a/assets/js/pages/ClusterDetails/HanaClusterDetails.stories.jsx b/assets/js/pages/ClusterDetails/HanaClusterDetails.stories.jsx index 741b57d17f..5d77cb02de 100644 --- a/assets/js/pages/ClusterDetails/HanaClusterDetails.stories.jsx +++ b/assets/js/pages/ClusterDetails/HanaClusterDetails.stories.jsx @@ -27,11 +27,15 @@ const { provider, cib_last_written: cibLastWritten, details, -} = clusterFactory.build({ type: 'hana_scale_up' }); +} = clusterFactory.build({ + type: 'hana_scale_up', + details: { architecture_type: 'classic' }, +}); const scaleOutSites = hanaClusterSiteFactory.buildList(2); const scaleOutDetails = hanaClusterDetailsFactory.build({ + architecture_type: 'classic', sites: scaleOutSites, nodes: [ hanaClusterDetailsNodesFactory.build({ @@ -213,3 +217,13 @@ export const WithNoSBDDevices = { }, }, }; + +export const AngiArchitecture = { + args: { + ...Hana.args, + details: { + ...Hana.args.details, + architecture_type: 'angi', + }, + }, +}; diff --git a/assets/js/pages/ClusterDetails/HanaClusterDetails.test.jsx b/assets/js/pages/ClusterDetails/HanaClusterDetails.test.jsx index 580fd7bb85..4aa7b28181 100644 --- a/assets/js/pages/ClusterDetails/HanaClusterDetails.test.jsx +++ b/assets/js/pages/ClusterDetails/HanaClusterDetails.test.jsx @@ -466,6 +466,53 @@ describe('HanaClusterDetails component', () => { } ); + it.each([ + { arch: 'angi', tooltip: 'Angi architecture' }, + { arch: 'classic', tooltip: 'Classic architecture' }, + ])( + 'should show cluster type with $arch architecture', + async ({ arch, tooltip }) => { + const user = userEvent.setup(); + + const { + clusterID, + clusterName, + cib_last_written: cibLastWritten, + type: clusterType, + sid, + provider, + details, + } = clusterFactory.build({ + type: 'hana_scale_up', + details: { architecture_type: arch }, + }); + + const hosts = hostFactory.buildList(2, { cluster_id: clusterID }); + + renderWithRouter( + + ); + + const icon = screen.getByText('HANA Scale Up').children.item(0); + await user.hover(icon); + expect(screen.getByText(tooltip, { exact: false })).toBeInTheDocument(); + } + ); + describe('forbidden actions', () => { it('should disable the check execution button when the user abilities are not compatible', async () => { const user = userEvent.setup(); diff --git a/assets/js/pages/ClusterSettingsPage/ClusterSettingsPage.jsx b/assets/js/pages/ClusterSettingsPage/ClusterSettingsPage.jsx index 3bcd0f28d9..0ac2fc2946 100644 --- a/assets/js/pages/ClusterSettingsPage/ClusterSettingsPage.jsx +++ b/assets/js/pages/ClusterSettingsPage/ClusterSettingsPage.jsx @@ -78,6 +78,7 @@ function ClusterSettingsPage() { const provider = get(cluster, 'provider'); const type = get(cluster, 'type'); + const architectureType = get(cluster, 'details.architecture_type'); const refreshCatalog = () => { const env = buildEnv({ @@ -86,6 +87,7 @@ function ClusterSettingsPage() { cluster_type: type, ensa_version: ensaVersion, filesystem_type: filesystemType, + architecture_type: architectureType, }); dispatch(updateCatalog(env)); @@ -130,7 +132,11 @@ function ClusterSettingsPage() { onStartExecution={requestChecksExecution} /> {catalogBanner[provider]} - + + ); case TARGET_HOST: return (