Skip to content

Everest-1862 | topology diagram view #1088

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

Open
wants to merge 73 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 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
bdf39b8
Merge branch 'main' into EVEREST-1862-topology-diagram-view
recharte Mar 10, 2025
b64caea
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Mar 10, 2025
9fc298a
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Mar 10, 2025
c4df0a9
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Mar 11, 2025
f3a1ccc
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Mar 11, 2025
bbab17a
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Mar 11, 2025
ba16f2a
Merge branch 'main' into EVEREST-1862-topology-diagram-view
fabio-silva Mar 19, 2025
6aa7481
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Mar 19, 2025
28012a6
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Mar 20, 2025
8551574
fix: node height and text overflow
fabio-silva Mar 20, 2025
6bc3c82
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Mar 20, 2025
b568be1
feat: allow to collapse selected nodes
fabio-silva Mar 20, 2025
e4abd59
Merge branch 'EVEREST-1862-topology-diagram-view' of https://github.c…
fabio-silva Mar 20, 2025
703946a
fix: e2e tests
fabio-silva Mar 20, 2025
12b7aa8
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Mar 20, 2025
90120b5
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Mar 21, 2025
e13f4d1
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Mar 21, 2025
2facd47
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Mar 21, 2025
6931cfb
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Mar 21, 2025
54f7901
Merge branch 'main' into EVEREST-1862-topology-diagram-view
Tikdev00 Mar 28, 2025
b5f5401
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Mar 28, 2025
b54c541
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 1, 2025
f377b49
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 2, 2025
55c5e02
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 2, 2025
0c90c4d
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 3, 2025
c0083dc
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 4, 2025
4580e82
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 7, 2025
e0e5c5f
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 8, 2025
f52bd17
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 8, 2025
3618430
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 8, 2025
5cc27b2
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 8, 2025
7c5ddf3
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 9, 2025
c69e21c
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 9, 2025
a570c56
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 9, 2025
9f794e8
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 10, 2025
f2d8071
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 10, 2025
1b3ca53
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 10, 2025
0c3892c
Merge branch 'main' into EVEREST-1862-topology-diagram-view
percona-robot Apr 10, 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
135 changes: 135 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,135 @@
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(
`table[data-testid="${CLUSTER_NAME}-components"] > tbody > tr.MuiTableRow-root:not(.MuiTableRow-head):not(.Mui-TableBodyCell-DetailPanel)`
)
.all();
const allContainers = await page
.locator('tr.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 innerTable = container.locator('table');
const innerTableRows = await innerTable.locator('tr').all();

for (const innerTableRow of innerTableRows) {
const name = await innerTableRow.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(
new RegExp(`component-node-${name}(-selected)?`)
);
const correspondingNodeSelected = page.getByTestId(
`component-node-${name}-selected`
);
const isSelected = await correspondingNodeSelected.isVisible();

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

if (!isSelected) {
await correspondingNode.click();
}
// Wait for the diagram to be updated
await page.waitForTimeout(300);

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-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 @@ -34,11 +34,13 @@
"@percona/utils": "workspace:^",
"@tanstack/react-query": "^5.22.2",
"@tanstack/react-query-devtools": "^5.24.0",
"@xyflow/react": "^12.3.6",
"axios": "^1.8.2",
"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 @@ -60,6 +62,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,116 +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
getRowId={(row) => row.name}
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