Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
Signed-off-by: David Edler <[email protected]>
  • Loading branch information
edlerd committed Jan 14, 2025
1 parent 1d683b8 commit 54c1afa
Show file tree
Hide file tree
Showing 19 changed files with 904 additions and 215 deletions.
8 changes: 8 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,14 @@ const App: FC = () => {
/>
}
/>
<Route
path="/ui/project/:project/member/:member/network/:name"
element={
<ProtectedRoute
outlet={<ProjectLoader outlet={<NetworkDetail />} />}
/>
}
/>
<Route
path="/ui/project/:project/network/:name/:activeTab"
element={
Expand Down
178 changes: 155 additions & 23 deletions src/api/networks.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { handleEtagResponse, handleResponse } from "util/helpers";
import { LxdNetwork, LxdNetworkState } from "types/network";
import { LxdApiResponse } from "types/apiResponse";
import { LxdClusterMember } from "types/cluster";
import { areNetworksEqual } from "util/networks";
import { ClusterSpecificSelectField } from "components/ClusterSpecificSelect";
import { LxdClusterMember } from "types/cluster";

export const fetchNetworks = (project: string): Promise<LxdNetwork[]> => {
export const fetchNetworks = (
project: string,
target?: string,
): Promise<LxdNetwork[]> => {
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<LxdNetwork[]>) => {
const filteredNetworks = data.metadata.filter(
Expand All @@ -20,24 +25,105 @@ export const fetchNetworks = (project: string): Promise<LxdNetwork[]> => {
});
};

export type LXDClusterMemberNetworks = {
memberName: string;
memberNetworks: LxdNetwork[];
}[];

export const fetchClusterMemberNetworks = async (
project: string,
clusterMembers: LxdClusterMember[],
): Promise<LXDClusterMemberNetworks> => {
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<LxdNetwork[]>;
result.push({ memberName, memberNetworks: networks.value });
}

resolve(result);
})
.catch(reject);
});
};

export const fetchNetwork = (
name: string,
project: string,
target?: string,
): Promise<LxdNetwork> => {
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<LXDClusterMemberNetwork> => {
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<LxdNetwork>;
result.push({ memberName: name, network: promise.value });
}

resolve(result);
})
.catch(reject);
});
};

export const fetchNetworkState = (
name: string,
project: string,
target?: string,
): Promise<LxdNetworkState> => {
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<LxdNetworkState>) => resolve(data.metadata))
.catch(reject);
Expand All @@ -47,21 +133,23 @@ export const fetchNetworkState = (
export const createClusterNetwork = (
network: Partial<LxdNetwork>,
project: string,
clusterMembers: LxdClusterMember[],
parentsPerClusterMember?: ClusterSpecificSelectField,
): Promise<void> => {
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) => {
Expand Down Expand Up @@ -110,15 +198,20 @@ export const createNetwork = (
export const updateNetwork = (
network: Partial<LxdNetwork> & Required<Pick<LxdNetwork, "config">>,
project: string,
target?: string,
): Promise<void> => {
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) => {
Expand All @@ -135,6 +228,45 @@ export const updateNetwork = (
});
};

export const updateClusterNetwork = (
network: Partial<LxdNetwork> & Required<Pick<LxdNetwork, "config">>,
project: string,
parentsPerClusterMember?: ClusterSpecificSelectField,
networkByMembers?: LXDClusterMemberNetwork,
): Promise<void> => {
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,
Expand Down
97 changes: 97 additions & 0 deletions src/components/ClusterSpecificSelect.tsx
Original file line number Diff line number Diff line change
@@ -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<string, string>;

interface Props {
readOnly: boolean;
options: {
memberName: string;
values: string[];
}[];
valueSuffix?: ReactNode;
values?: ClusterSpecificSelectField;
onChange: (value: ClusterSpecificSelectField) => void;
}

const ClusterSpecificSelect: FC<Props> = ({
readOnly,
options,
valueSuffix,
values,
onChange,
}) => {
const [isExpanded, setExpanded] = useState(false);

return isExpanded ? (
<>
<Button onClick={() => setExpanded(false)} hasIcon appearance="base">
<Icon name="collapse" />
</Button>{" "}
<MainTable
style={{ width: "auto", display: "inline" }}
headers={[{ content: "Cluster member" }, { content: "Value" }]}
rows={options.map((item) => {
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: (
<ResourceLabel
type="cluster-member"
value={item.memberName}
/>
),
role: "cell",
"aria-label": "Cluster member",
},
{
content: readOnly ? (
<>
{activeValue}
{valueSuffix}
</>
) : (
<Select
className="u-no-margin--bottom"
options={options}
onChange={(e) => {
onChange({
...values,
[item.memberName]: e.target.value,
});
}}
value={activeValue}
/>
),
role: "cell",
"aria-label": "Value",
},
],
};
})}
/>
</>
) : (
<div>
<Button onClick={() => setExpanded(true)} hasIcon appearance="base">
<Icon name="expand" />
<span>specific values per cluster member</span>
</Button>
</div>
);
};

export default ClusterSpecificSelect;
10 changes: 9 additions & 1 deletion src/components/RenameHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface RenameHeaderValues {

interface Props {
name: string;
relatedChip?: ReactNode;
titleClassName?: string;
parentItems: ReactNode[];
centerControls?: ReactNode;
Expand All @@ -26,6 +27,7 @@ interface Props {

const RenameHeader: FC<Props> = ({
name,
relatedChip,
titleClassName,
parentItems,
centerControls,
Expand Down Expand Up @@ -56,7 +58,10 @@ const RenameHeader: FC<Props> = ({
className="p-breadcrumbs p-breadcrumbs--large"
aria-label="Breadcrumbs"
>
<ol className="p-breadcrumbs__items">
<ol
className="p-breadcrumbs__items"
style={{ display: "inline-block" }}
>
{parentItems.map((item, key) => (
<li
className="p-heading--4 u-no-margin--bottom continuous-breadcrumb"
Expand Down Expand Up @@ -112,6 +117,9 @@ const RenameHeader: FC<Props> = ({
</li>
)}
</ol>
{relatedChip && (
<span style={{ marginLeft: "-1rem" }}>{relatedChip}</span>
)}
</nav>
{!formik?.values.isRenaming && centerControls}
</div>
Expand Down
Loading

0 comments on commit 54c1afa

Please sign in to comment.