Skip to content

Commit

Permalink
VTAdmin(web): Add simpler topology tree structure (#17245)
Browse files Browse the repository at this point in the history
Signed-off-by: Noble Mittal <noblemittal@outlook.com>
  • Loading branch information
beingnoble03 authored Dec 16, 2024
1 parent 45192d2 commit 998433c
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 2 deletions.
5 changes: 5 additions & 0 deletions web/vtadmin/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { Transactions } from './routes/Transactions';
import { Transaction } from './routes/transaction/Transaction';
import { CreateReshard } from './routes/createWorkflow/CreateReshard';
import { CreateMaterialize } from './routes/createWorkflow/CreateMaterialize';
import { TopologyTree } from './routes/topologyTree/TopologyTree';
import { SchemaMigrations } from './routes/SchemaMigrations';
import { CreateSchemaMigration } from './routes/createSchemaMigration/CreateSchemaMigration';

Expand Down Expand Up @@ -164,6 +165,10 @@ export const App = () => {
<ClusterTopology />
</Route>

<Route path="/topologytree/:clusterID">
<TopologyTree />
</Route>

<Route path="/topology">
<Topology />
</Route>
Expand Down
11 changes: 10 additions & 1 deletion web/vtadmin/src/components/routes/topology/Topology.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export const Topology = () => {
<DataCell>
<TopologyLink clusterID={cluster.id}>View Topology</TopologyLink>
</DataCell>
<DataCell>
<Link className="font-bold" to={`/topologytree/${cluster.id}`}>
View Topology Tree
</Link>
</DataCell>
</tr>
));

Expand All @@ -65,7 +70,11 @@ export const Topology = () => {
<ContentContainer>
<div className="max-w-screen-sm">
<div className="text-xl font-bold">Clusters</div>
<DataTable columns={['Name', 'Id', 'Topology']} data={rows} renderRows={renderRows} />
<DataTable
columns={['Name', 'Id', 'Topology', 'Topology Tree']}
data={rows}
renderRows={renderRows}
/>
</div>
</ContentContainer>
</div>
Expand Down
74 changes: 74 additions & 0 deletions web/vtadmin/src/components/routes/topologyTree/Node.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Copyright 2024 The Vitess Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect, useState } from 'react';

import { useTopologyPath } from '../../../hooks/api';
import { buildChildNodes, TopologyNode } from './TopologyTree';

interface NodeParams {
topologyNode: TopologyNode;
clusterID: string;
}

export const Node = ({ topologyNode, clusterID }: NodeParams) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [node, setNode] = useState<TopologyNode>(topologyNode);

const childrenPath = `${topologyNode.path}/${topologyNode.name}`;
const { data } = useTopologyPath(
{ clusterID, path: childrenPath },
{
enabled: isOpen,
}
);

useEffect(() => {
if (data) {
setNode((prevNode) => ({
...prevNode,
children: buildChildNodes(data.cell, childrenPath),
data: data.cell?.data,
}));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);

const nodeTitle = `${isOpen ? '▼' : '►'} ${node.name}`;
return (
<div className="border-l border-x-zinc-300 pl-2 mt-4">
<div className="w-fit cursor-pointer font-bold text-blue-500" onClick={() => setIsOpen(!isOpen)}>
{nodeTitle}
</div>

{isOpen && (
<div className="w-fit ml-4">
{node.data ? (
<div className="max-w-[300px] mt-1 bg-gray-100 p-2 text-[10px] text-left font-mono whitespace-normal">
{node.data}
</div>
) : (
<>
{node.children &&
node.children.map((child, idx) => {
return <Node key={idx} clusterID={clusterID} topologyNode={child} />;
})}
</>
)}
</div>
)}
</div>
);
};
103 changes: 103 additions & 0 deletions web/vtadmin/src/components/routes/topologyTree/TopologyTree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Copyright 2024 The Vitess Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect, useState } from 'react';

import { useTopologyPath } from '../../../hooks/api';
import { useDocumentTitle } from '../../../hooks/useDocumentTitle';
import { ContentContainer } from '../../layout/ContentContainer';
import { NavCrumbs } from '../../layout/NavCrumbs';
import { WorkspaceHeader } from '../../layout/WorkspaceHeader';
import { WorkspaceTitle } from '../../layout/WorkspaceTitle';
import { Link, useParams } from 'react-router-dom';

import { Node } from './Node';
import { vtctldata } from '../../../proto/vtadmin';

export interface TopologyNode {
name?: string | null;
data?: string | null;
path: string;
children?: TopologyNode[];
}

export const buildChildNodes = (cell: vtctldata.ITopologyCell | null | undefined, path: string) => {
if (cell) {
const childNodes: TopologyNode[] | undefined = cell.children
? cell.children.map((child) => {
return {
name: child,
path,
};
})
: undefined;
return childNodes;
}
};

export const TopologyTree = () => {
interface RouteParams {
clusterID: string;
}
useDocumentTitle('Cluster Topolgy');
const { clusterID } = useParams<RouteParams>();
const { data } = useTopologyPath({ clusterID, path: '/' });
const [topologyNode, setTopologyNode] = useState<TopologyNode | undefined>();

useEffect(() => {
if (data?.cell) {
const topologyNode: TopologyNode = {
path: data.cell.path || '/',
data: data.cell.data,
children: buildChildNodes(data.cell, ''),
};
setTopologyNode(topologyNode);
}
}, [data]);

if (!data) {
return (
<div>
<WorkspaceHeader>
<NavCrumbs>
<Link to="/topology">Topology</Link>
</NavCrumbs>

<WorkspaceTitle className="font-mono">{clusterID}</WorkspaceTitle>
</WorkspaceHeader>

<ContentContainer>404</ContentContainer>
</div>
);
}

return (
<div>
<WorkspaceHeader>
<NavCrumbs>
<Link to="/topology">Topology</Link>
</NavCrumbs>
<WorkspaceTitle className="font-mono">{clusterID}</WorkspaceTitle>
</WorkspaceHeader>

<ContentContainer className="lg:w-[1400px] lg:h-[1200px] md:w-[900px] md:h-[800px]">
{topologyNode &&
topologyNode.children?.map((child, idx) => (
<Node key={idx} topologyNode={child} clusterID={clusterID} />
))}
</ContentContainer>
</div>
);
};
2 changes: 1 addition & 1 deletion web/vtadmin/src/hooks/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -729,7 +729,7 @@ export const useTopologyPath = (
params: GetTopologyPathParams,
options?: UseQueryOptions<vtctldata.GetTopologyPathResponse, Error> | undefined
) => {
return useQuery(['topology-path', params], () => getTopologyPath(params));
return useQuery(['topology-path', params], () => getTopologyPath(params), options);
};
/**
* useValidate is a mutate hook that validates that all nodes reachable from the global replication graph,
Expand Down

0 comments on commit 998433c

Please sign in to comment.