Skip to content

Commit

Permalink
feat: [WD-14512] Proxy device configuration (#883)
Browse files Browse the repository at this point in the history
## Done

- Adding Proxy Configuration...

**Draft PR for Proxy device config**
Current issues / todos:
[✔️] ~~Correct implementation of the tiered Listen / Connect sections
given the restrictions of the current components getInheritedDeviceRow
and getConfigurationRowBase.~~
[✔️] Correct / best case implementation of the UI design.
[✔️] Device (From profile) inheritance & display.

Upcoming:
[✔️] Reviewing create/edit instance/profile pipeline with new api
params.
~~[] Explanation tooltips for disabled buttons~~
~~[] NAT boolean logic (If false, connect and listen types must be the
same)~~

## QA

1. Run the LXD-UI:
- On the demo server via the link posted by @webteam-app below. This is
only available for PRs created by collaborators of the repo. Ask
@mas-who or @edlerd for access.
- With a local copy of this branch, [build and run as described in the
docs](../CONTRIBUTING.md#setting-up-for-development).
2. Perform the following QA steps:
- Navigate to the Create Instance page to see Proxy device config upon
creating a new instance, or also access from the Create Profile / Edit
Instance / Edit Profile pages.

## Screenshots

Pending...
  • Loading branch information
Kxiru authored Sep 11, 2024
2 parents 94a7510 + 18342c3 commit 0d3b75e
Show file tree
Hide file tree
Showing 15 changed files with 481 additions and 3 deletions.
3 changes: 2 additions & 1 deletion src/components/forms/GPUDeviceForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC } from "react";
import { FC } from "react";
import {
Button,
Icon,
Expand Down Expand Up @@ -262,6 +262,7 @@ const GPUDevicesForm: FC<Props> = ({ formik, project }) => {
<ConfigurationTable rows={customRows} />
</div>
)}

<AttachGPUBtn
onSelect={(card) => {
ensureEditMode(formik);
Expand Down
16 changes: 16 additions & 0 deletions src/components/forms/NewProxyBtn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { FC } from "react";
import { Button, Icon } from "@canonical/react-components";

interface Props {
onSelect: () => void;
}

const NewProxyBtn: FC<Props> = ({ onSelect }) => {
return (
<Button onClick={onSelect} type="button" hasIcon>
<Icon name="plus" />
<span>New Proxy Device</span>
</Button>
);
};
export default NewProxyBtn;
360 changes: 360 additions & 0 deletions src/components/forms/ProxyDeviceForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,360 @@
import { FC } from "react";
import {
Button,
Icon,
Input,
Label,
Select,
useNotify,
} from "@canonical/react-components";
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { LxdProxyDevice } from "types/device";
import { InstanceAndProfileFormikProps } from "./instanceAndProfileFormValues";
import { fetchProfiles } from "api/profiles";
import { getInheritedProxies } from "util/configInheritance";
import Loader from "components/Loader";
import ScrollableForm from "components/ScrollableForm";
import RenameDeviceInput from "components/forms/RenameDeviceInput";
import ConfigurationTable from "components/ConfigurationTable";
import { MainTableRow } from "@canonical/react-components/dist/components/MainTable/MainTable";
import { getConfigurationRowBase } from "components/ConfigurationRow";
import classnames from "classnames";
import {
addNoneDevice,
deduplicateName,
findNoneDeviceIndex,
removeDevice,
} from "util/formDevices";
import { getInheritedDeviceRow } from "components/forms/InheritedDeviceRow";
import { deviceKeyToLabel } from "util/devices";
import { ensureEditMode } from "util/instanceEdit";
import NewProxyBtn from "components/forms/NewProxyBtn";
import ConfigFieldDescription from "pages/settings/ConfigFieldDescription";
import { optionEnabledDisabled, optionYesNo } from "util/instanceOptions";

interface Props {
formik: InstanceAndProfileFormikProps;
project: string;
}

const ProxyDeviceForm: FC<Props> = ({ formik, project }) => {
const notify = useNotify();

const {
data: profiles = [],
isLoading: isProfileLoading,
error: profileError,
} = useQuery({
queryKey: [queryKeys.profiles],
queryFn: () => fetchProfiles(project),
});

if (profileError) {
notify.failure("Loading profiles failed", profileError);
}

const inheritedProxies = getInheritedProxies(formik.values, profiles);

const existingDeviceNames: string[] = [];
existingDeviceNames.push(...inheritedProxies.map((item) => item.key));
existingDeviceNames.push(...formik.values.devices.map((item) => item.name));

const addProxy = () => {
const copy = [...formik.values.devices];
copy.push({
type: "proxy",
name: deduplicateName("proxy", 1, existingDeviceNames),
});
void formik.setFieldValue("devices", copy);
};

const hasCustomProxy = formik.values.devices.some(
(item) => item.type === "proxy",
);

const getProxyDeviceFormRows = (
label: string,
fieldName: string,
index: number,
options: {
label: string;
value: string;
disabled?: boolean;
}[],
value?: string,
help?: string,
) => {
const key = `devices.${index}.${fieldName}`;

return getConfigurationRowBase({
className: "no-border-top inherited-with-form",
configuration: <Label forId={key}>{label}</Label>,
inherited: (
<Select
name={key}
id={key}
key={key}
onBlur={formik.handleBlur}
onChange={(e) => {
ensureEditMode(formik);
void formik.setFieldValue(key, e.target.value);
}}
value={value ?? ""}
options={options}
help={<ConfigFieldDescription description={help} />}
className="u-no-margin--bottom"
/>
),
override: "",
});
};

const inheritedRows: MainTableRow[] = [];
inheritedProxies.forEach((item) => {
const noneDeviceId = findNoneDeviceIndex(item.key, formik);
const isNoneDevice = noneDeviceId !== -1;

inheritedRows.push(
getConfigurationRowBase({
className: "no-border-top override-with-form",
configuration: (
<div
className={classnames("device-name", {
"u-text--muted": isNoneDevice,
})}
>
<b>{item.key}</b>
</div>
),
inherited: (
<div className="p-text--small u-text--muted u-no-margin--bottom">
From: {item.source}
</div>
),
override: isNoneDevice ? (
<Button
appearance="base"
type="button"
title="Reattach volume"
onClick={() => {
ensureEditMode(formik);
removeDevice(noneDeviceId, formik);
}}
className="has-icon u-no-margin--bottom"
>
<Icon name="connected"></Icon>
<span>Reattach</span>
</Button>
) : (
<Button
appearance="base"
type="button"
onClick={() => {
ensureEditMode(formik);
addNoneDevice(item.key, formik);
}}
className="has-icon u-no-margin--bottom"
dense
>
<Icon name="disconnect"></Icon>
<span>Detach</span>
</Button>
),
}),
);

Object.keys(item.proxy).forEach((key) => {
if (key === "name" || key === "type") {
return null;
}

inheritedRows.push(
getInheritedDeviceRow({
label: deviceKeyToLabel(key),
inheritValue: item.proxy[key as keyof typeof item.proxy],
readOnly: false,
isDeactivated: isNoneDevice,
}),
);
});
});

const customRows: MainTableRow[] = [];
formik.values.devices.forEach((formDevice, index) => {
if (formDevice.type !== "proxy") {
return;
}
const device = formik.values.devices[index] as LxdProxyDevice;

customRows.push(
getConfigurationRowBase({
className: "no-border-top custom-device-name",
configuration: (
<RenameDeviceInput
name={device.name}
index={index}
setName={(name) => {
ensureEditMode(formik);
void formik.setFieldValue(`devices.${index}.name`, name);
}}
/>
),
inherited: "",
override: (
<Button
className="u-no-margin--top u-no-margin--bottom"
onClick={() => {
ensureEditMode(formik);
removeDevice(index, formik);
}}
type="button"
appearance="base"
hasIcon
dense
title="Detach Proxy"
>
<Icon name="disconnect" />
<span>Detach</span>
</Button>
),
}),
);

customRows.push(
getProxyDeviceFormRows(
"Bind",
"bind",
index,
[
{ label: "Select option", value: "", disabled: true },
{ label: "Host", value: "host" },
{ label: "Instance", value: "instance" },
],
device.bind,
"Whether to bind the listen address to the instance or host",
),
);

customRows.push(
getProxyDeviceFormRows(
"NAT mode",
"nat",
index,
optionEnabledDisabled,
device.nat,
),
);

customRows.push(
getProxyDeviceFormRows(
"Use HAProxy Protocol",
"proxy_protocol",
index,
optionYesNo,
device.proxy_protocol,
),
);

customRows.push(
getConfigurationRowBase({
className: "no-border-top inherited-with-form",
configuration: <Label>Listen</Label>,
inherited: (
<Input
name={`devices.${index}.listen`}
id={`devices.${index}.listen`}
key={`devices.${index}.listen`}
onBlur={formik.handleBlur}
onChange={(e) => {
ensureEditMode(formik);
void formik.setFieldValue(
`devices.${index}.listen`,
e.target.value,
);
}}
value={device.listen}
type="text"
help={
<ConfigFieldDescription
description={
"Use the following format to specify the address and port: <type>:<addr>:<port>[-<port>][,<port>]"
}
/>
}
className="u-no-margin--bottom"
/>
),
override: "",
}),
);

customRows.push(
getConfigurationRowBase({
className: "no-border-top inherited-with-form",
configuration: <Label>Connect</Label>,
inherited: (
<Input
name={`devices.${index}.connect`}
id={`devices.${index}.connect`}
key={`devices.${index}.connect`}
onBlur={formik.handleBlur}
onChange={(e) => {
ensureEditMode(formik);
void formik.setFieldValue(
`devices.${index}.connect`,
e.target.value,
);
}}
value={device.connect}
type="text"
help={
<ConfigFieldDescription
description={
"Use the following format to specify the address and port: <type>:<addr>:<port>[-<port>][,<port>]"
}
/>
}
className="u-no-margin--bottom"
/>
),
override: "",
}),
);
});

if (isProfileLoading) {
return <Loader />;
}

return (
<ScrollableForm className="device-form">
{/* hidden submit to enable enter key in inputs */}
<Input type="submit" hidden value="Hidden input" />

{inheritedRows.length > 0 && (
<div className="inherited-devices">
<h2 className="p-heading--4">Inherited Proxy devices</h2>
<ConfigurationTable rows={inheritedRows} />
</div>
)}

{hasCustomProxy && (
<div className="custom-devices">
<h2 className="p-heading--4 custom-devices-heading">
Custom Proxy devices
</h2>
<ConfigurationTable rows={customRows} />
</div>
)}

<NewProxyBtn
onSelect={() => {
ensureEditMode(formik);
addProxy();
}}
/>
</ScrollableForm>
);
};
export default ProxyDeviceForm;
Loading

0 comments on commit 0d3b75e

Please sign in to comment.