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

Everest-1862 | topology diagram view #1088

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
aba945a
chore: add react-flow to deps
fabio-silva Jan 14, 2025
e311ebd
feat: showing edges to nodes
fabio-silva Jan 14, 2025
3ea3fba
feat: selectable nodes
fabio-silva Jan 14, 2025
2bd4c10
fix: real db cluster components on flow
fabio-silva Jan 14, 2025
3cd3ab8
feat: show cluster data
fabio-silva Jan 15, 2025
c949df4
Merge branch 'main' into EVEREST-1823-react-flow-components
fabio-silva Jan 15, 2025
3b1d2f5
Merge branch 'EVEREST-1823-react-flow-components' into EVEREST-1862-t…
fabio-silva Feb 4, 2025
161b8f6
feat: switch between tree/diagram view
fabio-silva Feb 4, 2025
f89e307
feat: use dagre to layout nodes
fabio-silva Feb 4, 2025
3209f50
chore: remove diagram controls
fabio-silva Feb 4, 2025
37d5b11
chore: hide handles
fabio-silva Feb 4, 2025
3691ad7
chore: improve styling
fabio-silva Feb 4, 2025
f06a6fb
fix: missing started time
fabio-silva Feb 5, 2025
9dc06d9
fix: calculate layout after node is selected
fabio-silva Feb 5, 2025
5993179
fix: ignore clicks on container nodes
fabio-silva Feb 5, 2025
77b7161
chore: ComponentStatus
fabio-silva Feb 5, 2025
96bc8cc
chore: ComponentAge
fabio-silva Feb 5, 2025
a48e759
chore: change react-flow attribution style
fabio-silva Feb 5, 2025
e2223c4
chore: new utils functions
fabio-silva Feb 5, 2025
9a74906
chore: DiagramNode
fabio-silva Feb 6, 2025
bde919a
chore: remove "one child" layout calculation
fabio-silva Feb 6, 2025
222c89d
Merge branch 'main' into EVEREST-1862-topology-diagram-view
fabio-silva Feb 6, 2025
0867859
Merge branch 'main' into EVEREST-1862-topology-diagram-view
fabio-silva Feb 7, 2025
66b3f56
chore: add zoom controls to diagram
fabio-silva Feb 7, 2025
c398020
test: components e2e
fabio-silva Feb 7, 2025
9e8ef5e
Merge branch 'main' into EVEREST-1862-topology-diagram-view
fabio-silva Feb 10, 2025
ae8f3ad
fix: diagram control colors
fabio-silva Feb 11, 2025
5a56065
feat: set pointer cursos to component nodes
fabio-silva Feb 11, 2025
d39a65c
Merge branch 'main' into EVEREST-1862-topology-diagram-view
fabio-silva Feb 17, 2025
c2c9c74
fix: use regex to find containers on e2e tests
fabio-silva Feb 18, 2025
d687579
fix: e2e components table selectors
fabio-silva Feb 18, 2025
b417159
Merge branch 'main' into EVEREST-1862-topology-diagram-view
fabio-silva Feb 18, 2025
bae488e
Merge branch 'main' into EVEREST-1862-topology-diagram-view
fabio-silva Feb 19, 2025
52e4c36
Merge branch 'main' into EVEREST-1862-topology-diagram-view
fabio-silva Feb 24, 2025
b849766
Merge branch 'main' into EVEREST-1862-topology-diagram-view
fabio-silva Feb 26, 2025
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
119 changes: 119 additions & 0 deletions ui/apps/everest/.e2e/pr/db-cluster-details/components.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { createDbClusterFn, deleteDbClusterFn } from '@e2e/utils/db-cluster';
import { findDbAndClickRow } from '@e2e/utils/db-clusters-list';
import { expect, test } from '@playwright/test';
import { DBClusterDetailsTabs } from '../../../src/pages/db-cluster-details/db-cluster-details.types';
import { waitForInitializingState } from '@e2e/utils/table';

const CLUSTER_NAME = 'components-mysql';

test.describe('Cluster components', async () => {
test.beforeAll(async ({ request }) => {
await createDbClusterFn(request, {
dbName: CLUSTER_NAME,
dbType: 'mysql',
numberOfNodes: '3',
numberOfProxies: '2',
});
});

test.beforeEach(async ({ page }) => {
await page.goto('/databases');
await waitForInitializingState(page, CLUSTER_NAME);
});

test.afterAll(async ({ request }) => {
await deleteDbClusterFn(request, CLUSTER_NAME);
});

test('Same components on table and diagram', async ({ page }) => {
const componentsList: Array<{
name: string;
type: string;
containers: Array<{
name: string;
}>;
}> = [];

await findDbAndClickRow(page, CLUSTER_NAME);
await page.getByTestId(DBClusterDetailsTabs.components).click();

const switchInput = page
.getByTestId('switch-input-table-view')
.getByRole('checkbox');
await switchInput.check();
await expect(page.getByRole('table')).toBeVisible();
expect(
await page
.locator(
'.MuiTableRow-root:not(.MuiTableRow-head):not(.Mui-TableBodyCell-DetailPanel)'
)
.count()
).not.toBe(0);
// Waiting for some containers to be available
await page.waitForTimeout(5000);
await page.getByLabel('Expand all').getByRole('button').click();
await expect(page.getByLabel('Collapse all')).toBeVisible();
const allComponents = await page
.locator(
'.MuiTableRow-root:not(.MuiTableRow-head):not(.Mui-TableBodyCell-DetailPanel)'
)
.all();
const allContainers = await page
.locator('.Mui-TableBodyCell-DetailPanel')
.all();

for (const component of allComponents) {
// We just read static data, as the other data might change while we check things around
const name = await component.locator('td').nth(2).innerText();
const type = await component.locator('td').nth(3).innerText();
componentsList.push({ name, type, containers: [] });
}

for (const [index, container] of allContainers.entries()) {
const innerText = await container.innerText();

if (innerText !== 'No containers') {
const name = await container.locator('td').nth(2).innerText();
componentsList[index].containers.push({ name });
}
}

await switchInput.uncheck();
await expect(page.getByTitle('zoom in')).toBeVisible();

for (const component of componentsList) {
const { name, type, containers } = component;
const correspondingNode = page.getByTestId(`component-node-${name}`);

await expect(correspondingNode).toBeVisible();
expect(
await correspondingNode.getByTestId('component-node-name').innerText()
).toBe(name);
expect(
await correspondingNode.getByTestId('component-node-type').innerText()
).toBe(type);
await correspondingNode.click();

if (containers.length) {
const correspondingContainers = await page
.getByTestId('container-node')
.all();
expect(correspondingContainers.length).toBe(containers.length);

for (const container of containers) {
const { name } = container;
const correspondingContainer = page.getByTestId(
`container-node-${name}`
);

await expect(correspondingContainer).toBeVisible();
expect(
await correspondingContainer
.getByTestId('container-node-name')
.innerText()
).toBe(name);
}
}
}
});
});
3 changes: 3 additions & 0 deletions ui/apps/everest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@
"@percona/utils": "workspace:^",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query-devtools": "^5.24.0",
"@xyflow/react": "^12.3.6",
"axios": "^1.7.7",
"casbin.js": "^0.5.1",
"cron-parser": "^4.9.0",
"cron-time-generator": "^2.0.1",
"css-mediaquery": "^0.1.2",
"dagre": "^0.8.5",
"date-fns": "^3.6.0",
"date-fns-tz": "^3.1.3",
"dotenv": "^16.3.1",
Expand All @@ -58,6 +60,7 @@
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.0.0",
"@types/css-mediaquery": "^0.1.3",
"@types/dagre": "^0.7.52",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@types/semver": "^7.5.8",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Tooltip, Typography, TypographyProps } from '@mui/material';
import { format, formatDistanceToNowStrict, isValid } from 'date-fns';
import { DATE_FORMAT } from 'consts';

export type ComponentAgeProps = {
date: string;
render?: (date?: string) => React.ReactNode;
typographyProps?: TypographyProps;
};

const ComponentAge = ({ date, render, typographyProps }: ComponentAgeProps) => {
const dateObj = new Date(date);

const formattedDate = isValid(dateObj)
? formatDistanceToNowStrict(dateObj)
: '';

return (
<Tooltip
title={isValid(dateObj) ? `Started at ${format(date, DATE_FORMAT)}` : ''}
placement="right"
arrow
>
<Typography variant="caption" color="text.secondary" {...typographyProps}>
{render ? render(formattedDate) : formattedDate}
</Typography>
</Tooltip>
);
};

export default ComponentAge;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './component-age';
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { capitalize, Typography, TypographyProps } from '@mui/material';
import StatusField from 'components/status-field';
import { COMPONENT_STATUS, CONTAINER_STATUS } from '../components.constants';
import {
BaseStatus,
StatusFieldProps,
} from 'components/status-field/status-field.types';

const ComponentStatus = <T extends COMPONENT_STATUS | CONTAINER_STATUS>({
status,
statusMap,
typographyProps,
}: {
status: T;
statusMap: Record<T, BaseStatus>;
typographyProps?: TypographyProps;
} & StatusFieldProps<T>) => (
<StatusField status={status} statusMap={statusMap}>
<Typography variant="body2" fontWeight="bold" {...typographyProps}>
{capitalize(status)}
</Typography>
</StatusField>
);

export default ComponentStatus;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './component-status';
Original file line number Diff line number Diff line change
Expand Up @@ -13,115 +13,52 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { useState } from 'react';
import { Box, FormControlLabel, Stack, Switch } from '@mui/material';
import ComponentsDiagramView from './diagram-view/components-diagram-view';
import { ReactFlowProvider } from '@xyflow/react';
import { useParams } from 'react-router-dom';
import { useDbClusterComponents } from 'hooks/api/db-cluster/useDbClusterComponents';
import { useMemo } from 'react';
import { capitalize, Tooltip } from '@mui/material';
import { Table } from '@percona/ui-lib';
import { MRT_ColumnDef } from 'material-react-table';
import { DBClusterComponent } from 'shared-types/components.types';
import StatusField from 'components/status-field';
import { format, formatDistanceToNowStrict, isValid } from 'date-fns';
import {
COMPONENT_STATUS,
COMPONENT_STATUS_WEIGHT,
componentStatusToBaseStatus,
} from './components.constants';
import ExpandedRow from './expanded-row';
import { DATE_FORMAT } from 'consts';
import { useDbClusterComponents } from 'hooks';
import ComponentsTableView from './table-view';

const Components = () => {
const { dbClusterName, namespace = '' } = useParams();

const [tableView, setTableView] = useState(false);
const { dbClusterName = '', namespace = '' } = useParams();
const { data: components = [], isLoading } = useDbClusterComponents(
namespace,
dbClusterName!
);

const columns = useMemo<MRT_ColumnDef<DBClusterComponent>[]>(() => {
return [
{
header: 'Status',
accessorKey: 'status',
Cell: ({ cell, row }) => (
<StatusField
status={cell.getValue<COMPONENT_STATUS>()}
statusMap={componentStatusToBaseStatus(row?.original?.ready)}
>
{capitalize(cell?.row?.original?.status)}
</StatusField>
),
sortingFn: (rowA, rowB) => {
return (
COMPONENT_STATUS_WEIGHT[rowA?.original?.status] -
COMPONENT_STATUS_WEIGHT[rowB?.original?.status]
);
},
},
{
header: 'Ready',
accessorKey: 'ready',
},
{
header: 'Name',
accessorKey: 'name',
},
{
header: 'Type',
accessorKey: 'type',
},
{
header: 'Age',
accessorKey: 'started',
Cell: ({ cell }) => {
const date = new Date(cell.getValue<string>());

return isValid(date) ? (
<Tooltip
title={`Started at ${format(date, DATE_FORMAT)}`}
placement="right"
arrow
>
<div>{formatDistanceToNowStrict(date)}</div>
</Tooltip>
) : (
''
);
},
},
{
header: 'Restarts',
accessorKey: 'restarts',
},
];
}, []);

return (
<Table
initialState={{
sorting: [
{
id: 'status',
desc: true,
},
],
}}
state={{ isLoading }}
tableName={`${dbClusterName}-components`}
columns={columns}
data={components}
noDataMessage={'No components'}
renderDetailPanel={({ row }) => <ExpandedRow row={row} />}
muiTableDetailPanelProps={{
sx: {
padding: 0,
width: '100%',
'.MuiCollapse-root': {
width: '100%',
},
},
}}
/>
<Stack>
<FormControlLabel
sx={{ ml: 'auto' }}
control={
<Switch
data-testid="switch-input-table-view"
value={tableView}
onChange={(_, checked) => setTableView(checked)}
/>
}
label="Table view"
/>
{tableView ? (
<ComponentsTableView
components={components}
isLoading={isLoading}
dbClusterName={dbClusterName}
/>
) : (
<Box
height="500px"
sx={{ backgroundColor: 'surfaces.elevation0', borderRadius: 2 }}
>
<ReactFlowProvider>
<ComponentsDiagramView components={components} />
</ReactFlowProvider>
</Box>
)}
</Stack>
);
};

Expand Down
Loading
Loading