Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display cluster details by type #1456

Merged
merged 6 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
65 changes: 31 additions & 34 deletions assets/js/components/ClusterDetails/ClusterDetailsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,13 @@ import React, { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';

import {
getCluster,
getClusterHosts,
getClusterHostIDs,
} from '@state/selectors/cluster';
import { getCluster, getClusterHosts } from '@state/selectors/cluster';
import {
updateLastExecution,
executionRequested,
} from '@state/actions/lastExecutions';
import { getLastExecution } from '@state/selectors/lastExecutions';
import ClusterDetails from './ClusterDetails';
import HanaClusterDetails from './HanaClusterDetails';
import { getClusterName } from '../ClusterLink';

export function ClusterDetailsPage() {
Expand All @@ -23,7 +19,6 @@ export function ClusterDetailsPage() {

const dispatch = useDispatch();
const lastExecution = useSelector(getLastExecution(clusterID));
const hosts = useSelector(getClusterHostIDs(clusterID));
useEffect(() => {
dispatch(updateLastExecution(clusterID));
}, [dispatch]);
Expand All @@ -34,33 +29,35 @@ export function ClusterDetailsPage() {
return <div>Loading...</div>;
}

const renderedNodes = cluster.details?.nodes?.map((node) => ({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving this to the HanaClusterDetails view, as it depends on the details format, which in this case is HANA related. ASCS/ERS clusters have other details, so better to do the enrichment specifically for each type

...node,
...clusterHosts.find(({ hostname }) => hostname === node.name),
}));

const hasSelectedChecks = cluster.selected_checks.length > 0;

return (
<ClusterDetails
clusterID={clusterID}
clusterName={getClusterName(cluster)}
selectedChecks={cluster.selected_checks}
hasSelectedChecks={hasSelectedChecks}
hosts={hosts}
clusterType={cluster.type}
cibLastWritten={cluster.cib_last_written}
sid={cluster.sid}
provider={cluster.provider}
clusterNodes={renderedNodes}
details={cluster.details}
lastExecution={lastExecution}
onStartExecution={(_, hostList, checks, navigateFunction) =>
dispatch(
executionRequested(clusterID, hostList, checks, navigateFunction)
)
}
navigate={navigate}
/>
);
switch (cluster.type) {
case 'hana_scale_up':
case 'hana_scale_out':
return (
<HanaClusterDetails
clusterID={clusterID}
clusterName={getClusterName(cluster)}
selectedChecks={cluster.selected_checks}
hasSelectedChecks={hasSelectedChecks}
hosts={clusterHosts}
clusterType={cluster.type}
cibLastWritten={cluster.cib_last_written}
sid={cluster.sid}
provider={cluster.provider}
details={cluster.details}
lastExecution={lastExecution}
onStartExecution={(_, hostList, checks, navigateFunction) =>
dispatch(
executionRequested(clusterID, hostList, checks, navigateFunction)
)
}
navigate={navigate}
/>
);
case 'ascs_ers':
return <div>ASCS/ERS</div>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simple placeholder by now. I will have the AscsErsClusterDetails component next.
I just put some text that will be in the details view to have the tests working fine

default:
return <div>Unknown cluster type</div>;
}
}
41 changes: 41 additions & 0 deletions assets/js/components/ClusterDetails/ClusterDetailsPage.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';

import { screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { withState, renderWithRouterMatch } from '@lib/test-utils';

import { clusterFactory } from '@lib/test-utils/factories';

import { ClusterDetailsPage } from './ClusterDetailsPage';

describe('ClusterDetails ClusterDetailsPage component', () => {
it.each([
{ type: 'hana_scale_up', label: 'HANA scale-up' },
{ type: 'ascs_ers', label: 'ASCS/ERS' },
{ type: 'unknwon', label: 'Unknown cluster type' },
])(
'should display the $type details based on cluster type',
({ type, label }) => {
const cluster = clusterFactory.build({ type });
const initialState = {
clustersList: { clusters: [cluster] },
hostsList: { hosts: [] },
lastExecutions: {
[cluster.id]: { data: null, loading: false, error: null },
},
};

const [statefulClusterDetailsPage, _] = withState(
<ClusterDetailsPage />,
initialState
);

renderWithRouterMatch(statefulClusterDetailsPage, {
path: 'clusters/:clusterID',
route: `/clusters/${cluster.id}`,
});

expect(screen.getByText(label)).toBeInTheDocument();
}
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const siteDetailsConfig = {
],
};

function ClusterDetails({
function HanaClusterDetails({
clusterID,
clusterName,
selectedChecks,
Expand All @@ -64,12 +64,16 @@ function ClusterDetails({
cibLastWritten,
sid,
provider,
clusterNodes,
details,
lastExecution,
onStartExecution,
navigate,
}) {
const enrichedNodes = details?.nodes?.map((node) => ({
...node,
...hosts.find(({ hostname }) => hostname === node.name),
}));

return (
<div>
<BackButton url="/clusters">Back to Clusters</BackButton>
Expand Down Expand Up @@ -106,7 +110,7 @@ function ClusterDetails({
cssClasses="flex rounded relative ml-0.5 disabled:bg-slate-50 disabled:text-slate-500 disabled:border-gray-400"
clusterId={clusterID}
disabled={!hasSelectedChecks}
hosts={hosts}
hosts={hosts.map(({ id }) => id)}
checks={selectedChecks}
onStartExecution={onStartExecution}
>
Expand Down Expand Up @@ -197,7 +201,7 @@ function ClusterDetails({
<Table
className="tn-site-table"
config={siteDetailsConfig}
data={clusterNodes.filter(({ site }) => site === siteName)}
data={enrichedNodes.filter(({ site }) => site === siteName)}
/>
</div>
))}
Expand All @@ -207,4 +211,4 @@ function ClusterDetails({
);
}

export default ClusterDetails;
export default HanaClusterDetails;
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
hostFactory,
} from '@lib/test-utils/factories';

import ClusterDetails from './ClusterDetails';
import HanaClusterDetails from './HanaClusterDetails';

const {
id: clusterID,
Expand All @@ -34,14 +34,9 @@ const hosts = [
hostFactory.build({ hostname: details.nodes[1].name }),
];

const clusterNodes = details.nodes.map((node) => ({
...node,
...hosts.find(({ hostname }) => hostname === node.name),
}));

export default {
title: 'ClusterDetails',
components: ClusterDetails,
title: 'HanaClusterDetails',
components: HanaClusterDetails,
decorators: [
(Story) => (
<MemoryRouter>
Expand All @@ -63,20 +58,19 @@ export const Hana = {
clusterName,
selectedChecks,
hasSelectedChecks: true,
hosts: hosts.map(({ id: hostID }) => hostID),
hosts,
clusterType,
cibLastWritten,
sid,
provider,
clusterNodes,
details,
lastExecution,
onStartExecution: () => {},
navigate: () => {},
},
render: (args) => (
<ContainerWrapper>
<ClusterDetails {...args} />
<HanaClusterDetails {...args} />
</ContainerWrapper>
),
};
4 changes: 3 additions & 1 deletion assets/js/components/ClusterLink.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import React from 'react';
import { Link } from 'react-router-dom';
import classNames from 'classnames';

import { CLUSTER_TYPES } from '@lib/model';

export const getClusterName = (cluster) => cluster?.name || cluster?.id;

function ClusterLink({ cluster }) {
Expand All @@ -11,7 +13,7 @@ function ClusterLink({ cluster }) {
'truncate w-32 inline-block align-middle'
);

if (cluster?.type === 'hana_scale_up' || cluster?.type === 'hana_scale_out') {
if (CLUSTER_TYPES.includes(cluster?.type)) {
return (
<Link
className="text-jungle-green-500 hover:opacity-75"
Expand Down
42 changes: 42 additions & 0 deletions assets/js/components/ClusterLink.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';

import { screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { renderWithRouter } from '@lib/test-utils';

import { clusterFactory } from '@lib/test-utils/factories';

import ClusterLink from './ClusterLink';

describe('ClusterLink component', () => {
it.each(['hana_scale_up', 'hana_scale_out', 'ascs_ers'])(
'should render a link if the cluster type is %s',
(type) => {
const cluster = clusterFactory.build({ type });

renderWithRouter(<ClusterLink cluster={cluster} />);

const link = screen.getByRole('link');

expect(link).toHaveTextContent(cluster.name);
expect(link).toHaveAttribute('href', `/clusters/${cluster.id}`);
}
);

it('should show a simple text if the cluster type is unknown', () => {
const cluster = clusterFactory.build({ type: 'unknown' });

renderWithRouter(<ClusterLink cluster={cluster} />);

expect(screen.queryByRole('link')).not.toBeInTheDocument();
expect(screen.getByText(cluster.name)).toBeInTheDocument();
});

it('should show the cluster ID if the name is not available', () => {
const cluster = clusterFactory.build({ name: '' });

renderWithRouter(<ClusterLink cluster={cluster} />);

expect(screen.getByText(cluster.id)).toBeInTheDocument();
});
});
2 changes: 2 additions & 0 deletions assets/js/lib/model/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ export const isValidTargetType = (targetType) =>

export const UNKNOWN_PROVIDER = 'unknown';
export const VMWARE_PROVIDER = 'vmware';

export const CLUSTER_TYPES = ['hana_scale_up', 'hana_scale_out', 'ascs_ers'];
32 changes: 17 additions & 15 deletions assets/js/lib/test-utils/factories/clusters.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ const clusterTypeEnum = () =>

const hanaStatus = () => faker.helpers.arrayElement(['Primary', 'Failed']);

export const sbdDevicesFactory = Factory.define(() => ({
device: faker.system.filePath(),
status: faker.helpers.arrayElement(['healthy', 'unhealthy']),
}));

export const clusterResourceFactory = Factory.define(() => ({
id: faker.datatype.uuid(),
role: faker.animal.bear(),
Expand All @@ -18,7 +23,7 @@ export const clusterResourceFactory = Factory.define(() => ({
fail_count: faker.datatype.number(),
}));

export const clusterDetailsNodesFactory = Factory.define(() => ({
export const hanaClusterDetailsNodesFactory = Factory.define(() => ({
name: faker.animal.dog(),
site: faker.address.city(),
virtual_ip: faker.internet.ip(),
Expand All @@ -33,12 +38,18 @@ export const clusterDetailsNodesFactory = Factory.define(() => ({
resources: clusterResourceFactory.buildList(5),
}));

export const sbdDevicesFactory = Factory.define(() => ({
device: faker.system.filePath(),
status: faker.helpers.arrayElement(['healthy', 'unhealthy']),
export const hanaClusterDetailsFactory = Factory.define(() => ({
fencing_type: 'external/sbd',
nodes: hanaClusterDetailsNodesFactory.buildList(2),
sbd_devices: sbdDevicesFactory.buildList(3),
secondary_sync_state: 'SOK',
sr_health_state: '4',
stopped_resources: clusterResourceFactory.buildList(2),
system_replication_mode: 'sync',
system_replication_operation_mode: 'logreplay',
}));

export const clusterFactory = Factory.define(({ sequence }) => ({
export const clusterFactory = Factory.define(({ sequence, params }) => ({
id: faker.datatype.uuid(),
name: `${faker.name.firstName()}_${sequence}`,
sid: faker.random.alphaNumeric(3, { casing: 'upper' }),
Expand All @@ -49,14 +60,5 @@ export const clusterFactory = Factory.define(({ sequence }) => ({
selected_checks: [],
provider: cloudProviderEnum(),
cib_last_written: day(faker.date.recent()).format(),
details: {
fencing_type: 'external/sbd',
nodes: clusterDetailsNodesFactory.buildList(2),
sbd_devices: sbdDevicesFactory.buildList(3),
secondary_sync_state: 'SOK',
sr_health_state: '4',
stopped_resources: clusterResourceFactory.buildList(2),
system_replication_mode: 'sync',
system_replication_operation_mode: 'logreplay',
},
details: hanaClusterDetailsFactory.build(params.details),
}));