From 54c1afa948546c4f9326c6f24b20fea9d4fee523 Mon Sep 17 00:00:00 2001 From: David Edler Date: Fri, 10 Jan 2025 18:01:22 +0100 Subject: [PATCH] wip Signed-off-by: David Edler --- src/App.tsx | 8 + src/api/networks.tsx | 178 +++++++-- src/components/ClusterSpecificSelect.tsx | 97 +++++ src/components/RenameHeader.tsx | 10 +- src/pages/networks/CreateNetwork.tsx | 20 +- src/pages/networks/EditNetwork.tsx | 42 +- src/pages/networks/NetworkDetail.tsx | 14 +- src/pages/networks/NetworkDetailHeader.tsx | 28 +- src/pages/networks/NetworkForwardCount.tsx | 2 +- src/pages/networks/NetworkList.tsx | 372 ++++++++++++------ src/pages/networks/NetworkSearchFilter.tsx | 108 +++++ src/pages/networks/NetworkTopology.tsx | 86 +++- src/pages/networks/forms/NetworkAddresses.tsx | 17 +- src/pages/networks/forms/NetworkForm.tsx | 8 +- src/pages/networks/forms/NetworkFormMain.tsx | 15 +- .../networks/forms/NetworkParentSelector.tsx | 66 +++- .../networks/forms/NetworkStatistics.tsx | 9 +- src/sass/_network_topology.scss | 24 +- src/util/networkForm.tsx | 15 +- 19 files changed, 904 insertions(+), 215 deletions(-) create mode 100644 src/components/ClusterSpecificSelect.tsx create mode 100644 src/pages/networks/NetworkSearchFilter.tsx diff --git a/src/App.tsx b/src/App.tsx index aef94e5db3..07ed53530a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -218,6 +218,14 @@ const App: FC = () => { /> } /> + } />} + /> + } + /> => { +export const fetchNetworks = ( + project: string, + target?: string, +): Promise => { + const targetParam = target ? `&target=${target}` : ""; return new Promise((resolve, reject) => { - fetch(`/1.0/networks?project=${project}&recursion=1`) + fetch(`/1.0/networks?project=${project}&recursion=1${targetParam}`) .then(handleResponse) .then((data: LxdApiResponse) => { const filteredNetworks = data.metadata.filter( @@ -20,24 +25,105 @@ export const fetchNetworks = (project: string): Promise => { }); }; +export type LXDClusterMemberNetworks = { + memberName: string; + memberNetworks: LxdNetwork[]; +}[]; + +export const fetchClusterMemberNetworks = async ( + project: string, + clusterMembers: LxdClusterMember[], +): Promise => { + return new Promise((resolve, reject) => { + void Promise.allSettled( + clusterMembers.map(async (member) => { + return await fetchNetworks(project, member.server_name); + }), + ) + .then((results) => { + const error = results.find((res) => res.status === "rejected") + ?.reason as Error | undefined; + + if (error) { + reject(error); + return; + } + + const result: LXDClusterMemberNetworks = []; + + for (let i = 0; i < clusterMembers.length; i++) { + const memberName = clusterMembers[i].server_name; + const networks = results[i] as PromiseFulfilledResult; + result.push({ memberName, memberNetworks: networks.value }); + } + + resolve(result); + }) + .catch(reject); + }); +}; + export const fetchNetwork = ( name: string, project: string, + target?: string, ): Promise => { + const targetParam = target ? `&target=${target}` : ""; return new Promise((resolve, reject) => { - fetch(`/1.0/networks/${name}?project=${project}`) + fetch(`/1.0/networks/${name}?project=${project}${targetParam}`) .then(handleEtagResponse) .then((data) => resolve(data as LxdNetwork)) .catch(reject); }); }; +export type LXDClusterMemberNetwork = { + memberName: string; + network: LxdNetwork; +}[]; + +export const fetchClusterMemberNetwork = async ( + name: string, + project: string, + clusterMembers: LxdClusterMember[], +): Promise => { + return new Promise((resolve, reject) => { + void Promise.allSettled( + clusterMembers.map(async (member) => { + return await fetchNetwork(name, project, member.server_name); + }), + ) + .then((results) => { + const error = results.find((res) => res.status === "rejected") + ?.reason as Error | undefined; + + if (error) { + reject(error); + return; + } + + const result: LXDClusterMemberNetwork = []; + + for (let i = 0; i < clusterMembers.length; i++) { + const name = clusterMembers[i].server_name; + const promise = results[i] as PromiseFulfilledResult; + result.push({ memberName: name, network: promise.value }); + } + + resolve(result); + }) + .catch(reject); + }); +}; + export const fetchNetworkState = ( name: string, project: string, + target?: string, ): Promise => { + const targetParam = target ? `&target=${target}` : ""; return new Promise((resolve, reject) => { - fetch(`/1.0/networks/${name}/state?project=${project}`) + fetch(`/1.0/networks/${name}/state?project=${project}${targetParam}`) .then(handleResponse) .then((data: LxdApiResponse) => resolve(data.metadata)) .catch(reject); @@ -47,21 +133,23 @@ export const fetchNetworkState = ( export const createClusterNetwork = ( network: Partial, project: string, - clusterMembers: LxdClusterMember[], + parentsPerClusterMember?: ClusterSpecificSelectField, ): Promise => { - return new Promise((resolve, reject) => { - const memberNetwork = { - name: network.name, - description: network.description, - type: network.type, - config: { - parent: network.config?.parent, - }, - }; + if (!parentsPerClusterMember) { + return createNetwork(network, project); + } + return new Promise((resolve, reject) => { void Promise.allSettled( - clusterMembers.map(async (member) => { - await createNetwork(memberNetwork, project, member.server_name); + Object.keys(parentsPerClusterMember).map(async (memberName) => { + const memberNetwork = { + name: network.name, + type: network.type, + config: { + parent: parentsPerClusterMember[memberName], + }, + }; + await createNetwork(memberNetwork, project, memberName); }), ) .then((results) => { @@ -110,15 +198,20 @@ export const createNetwork = ( export const updateNetwork = ( network: Partial & Required>, project: string, + target?: string, ): Promise => { return new Promise((resolve, reject) => { - fetch(`/1.0/networks/${network.name ?? ""}?project=${project}`, { - method: "PUT", - body: JSON.stringify(network), - headers: { - "If-Match": network.etag ?? "invalid-etag", + const targetParam = target ? `&target=${target}` : ""; + fetch( + `/1.0/networks/${network.name ?? ""}?project=${project}${targetParam}`, + { + method: "PUT", + body: JSON.stringify(network), + headers: { + "If-Match": network.etag ?? "invalid-etag", + }, }, - }) + ) .then(handleResponse) .then(resolve) .catch(async (e: Error) => { @@ -135,6 +228,45 @@ export const updateNetwork = ( }); }; +export const updateClusterNetwork = ( + network: Partial & Required>, + project: string, + parentsPerClusterMember?: ClusterSpecificSelectField, + networkByMembers?: LXDClusterMemberNetwork, +): Promise => { + if (!parentsPerClusterMember) { + return updateNetwork(network, project); + } + + return new Promise((resolve, reject) => { + void Promise.allSettled( + Object.keys(parentsPerClusterMember).map(async (memberName) => { + const memberNetwork = { + name: network.name, + type: network.type, + config: { + parent: parentsPerClusterMember[memberName], + }, + etag: networkByMembers?.find((item) => item.memberName === memberName) + ?.network.etag, + }; + await updateNetwork(memberNetwork, project, memberName); + }), + ) + .then((results) => { + const error = results.find((res) => res.status === "rejected") + ?.reason as Error | undefined; + + if (error) { + reject(error); + return; + } + updateNetwork(network, project).then(resolve).catch(reject); + }) + .catch(reject); + }); +}; + export const renameNetwork = ( oldName: string, newName: string, diff --git a/src/components/ClusterSpecificSelect.tsx b/src/components/ClusterSpecificSelect.tsx new file mode 100644 index 0000000000..0c475e034c --- /dev/null +++ b/src/components/ClusterSpecificSelect.tsx @@ -0,0 +1,97 @@ +import { FC, ReactNode, useState } from "react"; +import { Button, Icon, MainTable, Select } from "@canonical/react-components"; +import ResourceLabel from "components/ResourceLabel"; + +export type ClusterSpecificSelectField = Record; + +interface Props { + readOnly: boolean; + options: { + memberName: string; + values: string[]; + }[]; + valueSuffix?: ReactNode; + values?: ClusterSpecificSelectField; + onChange: (value: ClusterSpecificSelectField) => void; +} + +const ClusterSpecificSelect: FC = ({ + readOnly, + options, + valueSuffix, + values, + onChange, +}) => { + const [isExpanded, setExpanded] = useState(false); + + return isExpanded ? ( + <> + {" "} + { + const activeValue = values?.[item.memberName]; + const options = item.values.map((value) => { + return { + label: value, + value: value, + }; + }); + options.unshift({ + label: options.length === 0 ? "None available" : "Select option", + value: "", + }); + + return { + columns: [ + { + content: ( + + ), + role: "cell", + "aria-label": "Cluster member", + }, + { + content: readOnly ? ( + <> + {activeValue} + {valueSuffix} + + ) : ( +