Skip to content

Commit

Permalink
feat: [WD-19015] CMS Server Config for maas.machine (#1097)
Browse files Browse the repository at this point in the history
## Done

- Introduced MaasMachineSettingFormInput.tsx

Fixes [list issues/bugs if needed]

- Maas Machine Setting.
- Storage.*volume settings.
- .*_address settings.

## 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:
    - [PENDING]

## Screenshots


![image](https://github.com/user-attachments/assets/1d457b7b-5b90-4a56-8104-e5252a1615e5)

![image](https://github.com/user-attachments/assets/2f686dc1-e87a-4d07-b531-1b04a400c202)
  • Loading branch information
Kxiru authored Feb 20, 2025
2 parents 2b6fe55 + 8f2f73c commit ea54c7e
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 26 deletions.
80 changes: 74 additions & 6 deletions src/api/server.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,62 @@
import { handleResponse, handleTextResponse } from "util/helpers";
import type { LxdSettings } from "types/server";
import {
constructMemberError,
handleResponse,
handleTextResponse,
} from "util/helpers";
import type { LXDSettingOnClusterMember, LxdSettings } from "types/server";
import type { LxdApiResponse } from "types/apiResponse";
import type { LxdMetadata, LxdConfigPair } from "types/config";
import type { LxdResources } from "types/resources";
import { LxdClusterMember } from "types/cluster";
import { ClusterSpecificValues } from "components/ClusterSpecificSelect";

export const fetchSettings = (): Promise<LxdSettings> => {
export const fetchSettings = (target?: string): Promise<LxdSettings> => {
return new Promise((resolve, reject) => {
fetch("/1.0")
const targetQueryParam = target ? `?target=${target}` : "";
fetch(`/1.0${targetQueryParam}`)
.then(handleResponse)
.then((data: LxdApiResponse<LxdSettings>) => resolve(data.metadata))
.catch(reject);
});
};

export const updateSettings = (config: LxdConfigPair): Promise<void> => {
export const fetchSettingsFromClusterMembers = (
clusterMembers: LxdClusterMember[],
): Promise<LXDSettingOnClusterMember[]> => {
return new Promise((resolve, reject) => {
fetch("/1.0", {
Promise.allSettled(
clusterMembers.map((member) => {
return fetchSettings(member.server_name);
}),
)
.then((results) => {
const settingOnMembers: LXDSettingOnClusterMember[] = [];
for (let i = 0; i < clusterMembers.length; i++) {
const memberName = clusterMembers[i].server_name;
const result = results[i];
if (result.status === "rejected") {
reject(constructMemberError(result, memberName));
}
if (result.status === "fulfilled") {
const promise = results[
i
] as PromiseFulfilledResult<LXDSettingOnClusterMember>;
settingOnMembers.push({ ...promise.value, memberName: memberName });
}
}
resolve(settingOnMembers);
})
.catch(reject);
});
};

export const updateSettings = (
config: LxdConfigPair,
target?: string,
): Promise<void> => {
const targetQueryParam = target ? `?target=${target}` : "";
return new Promise((resolve, reject) => {
fetch(`/1.0${targetQueryParam}`, {
method: "PATCH",
body: JSON.stringify({
config,
Expand All @@ -27,6 +68,33 @@ export const updateSettings = (config: LxdConfigPair): Promise<void> => {
});
};

export const updateClusteredSettings = (
clusterValues: ClusterSpecificValues,
configName: string,
): Promise<void> => {
return new Promise((resolve, reject) => {
Promise.allSettled(
Object.keys(clusterValues).map((memberName) => {
const config = {
[configName]: clusterValues[memberName],
};
return updateSettings(config, memberName);
}),
)
.then((results) => {
const error = results.find((res) => res.status === "rejected")
?.reason as Error | undefined;

if (error) {
reject(error);
return;
}
})
.then(resolve)
.catch(reject);
});
};

export const fetchResources = (): Promise<LxdResources> => {
return new Promise((resolve, reject) => {
fetch("/1.0/resources")
Expand Down
30 changes: 21 additions & 9 deletions src/components/forms/ClusterSpecificInput.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, Fragment, useEffect, useState } from "react";
import { FC, Fragment, ReactNode, useEffect, useState } from "react";
import { CheckboxInput, Input } from "@canonical/react-components";
import ResourceLink from "components/ResourceLink";
import FormEditButton from "components/FormEditButton";
Expand All @@ -8,19 +8,21 @@ interface Props {
id: string;
isReadOnly: boolean;
onChange: (value: ClusterSpecificValues) => void;
toggleReadOnly?: () => void;
memberNames: string[];
toggleReadOnly?: () => void;
values?: ClusterSpecificValues;
disableReason?: string;
canToggleSpecific?: boolean;
isDefaultSpecific?: boolean;
clusterMemberLinkTarget?: (member: string) => string;
disabled?: boolean;
helpText?: string;
helpText?: string | ReactNode;
placeholder?: string;
classname?: string;
}

const ClusterSpecificInput: FC<Props> = ({
disableReason,
values,
id,
isReadOnly,
Expand Down Expand Up @@ -94,12 +96,17 @@ const ClusterSpecificInput: FC<Props> = ({
to={clusterMemberLinkTarget(item)}
/>
</div>
<div className="cluster-specific-value">
<div className="cluster-specific-value-wrapper">
{isReadOnly ? (
<>
{activeValue}
<span className="cluster-specific-value">
{activeValue}
</span>
{!disabled && (
<FormEditButton toggleReadOnly={toggleReadOnly} />
<FormEditButton
disableReason={disableReason}
toggleReadOnly={toggleReadOnly}
/>
)}
</>
) : (
Expand Down Expand Up @@ -127,11 +134,16 @@ const ClusterSpecificInput: FC<Props> = ({
</div>
)}
{!isSpecific && (
<div>
<div className="cluster-specific-value-wrapper">
{isReadOnly ? (
<>
{firstValue}
{!disabled && <FormEditButton toggleReadOnly={toggleReadOnly} />}
<span className="cluster-specific-value">{firstValue}</span>
{!disabled && (
<FormEditButton
disableReason={disableReason}
toggleReadOnly={toggleReadOnly}
/>
)}
</>
) : (
<Input
Expand Down
19 changes: 16 additions & 3 deletions src/context/useSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import { useQuery } from "@tanstack/react-query";
import { queryKeys } from "util/queryKeys";
import { fetchSettings } from "api/server";
import type { LxdSettings } from "types/server";
import { fetchSettingsFromClusterMembers, fetchSettings } from "api/server";
import type { LXDSettingOnClusterMember, LxdSettings } from "types/server";
import { UseQueryResult } from "@tanstack/react-query";
import { useClusterMembers } from "./useClusterMembers";

export const useSettings = (): UseQueryResult<LxdSettings> => {
return useQuery({
queryKey: [queryKeys.settings],
queryFn: fetchSettings,
queryFn: () => fetchSettings(),
});
};

export const useClusteredSettings = (): UseQueryResult<
LXDSettingOnClusterMember[]
> => {
const { data: clusterMembers = [] } = useClusterMembers();

return useQuery({
queryKey: [queryKeys.settings, queryKeys.cluster],
queryFn: () => fetchSettingsFromClusterMembers(clusterMembers),
enabled: clusterMembers.length > 0,
});
};
95 changes: 95 additions & 0 deletions src/pages/settings/ClusteredSettingFormInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { FC, useState } from "react";
import { Button, Form, Icon } from "@canonical/react-components";
import type { ConfigField } from "types/config";
import { getConfigId } from "./SettingForm";
import ConfigFieldDescription from "pages/settings/ConfigFieldDescription";
import ClusterSpecificInput from "components/forms/ClusterSpecificInput";
import { useClusterMembers } from "context/useClusterMembers";
import { ClusterSpecificValues } from "components/ClusterSpecificSelect";

interface Props {
initialValue: ClusterSpecificValues;
configField: ConfigField;
onSubmit: (newValue: ClusterSpecificValues) => void;
onCancel: () => void;
disableReason?: string;
readonly?: boolean;
toggleReadOnly?: () => void;
}

const ClusteredSettingFormInput: FC<Props> = ({
disableReason,
initialValue,
configField,
onSubmit,
onCancel,
readonly = false,
toggleReadOnly,
}) => {
const [value, setValue] = useState<ClusterSpecificValues>(initialValue);

const { data: clusterMembers = [] } = useClusterMembers();
const memberNames = clusterMembers.map((member) => member.server_name);

const canBeReset = Object.values(value).some(
(v) => v !== configField.default,
);

const resetToDefault = () => {
const defaultValues: { [key: string]: string } = {};
memberNames.forEach((name) => {
defaultValues[name] = configField.default;
});
setValue(defaultValues);
};

return (
<Form
onSubmit={(e) => {
e.preventDefault();
onSubmit(value);
}}
>
<ClusterSpecificInput
aria-label={configField.key}
disableReason={disableReason}
id={getConfigId(configField.key)}
values={value}
isReadOnly={readonly}
onChange={(value) => setValue(value)}
memberNames={memberNames}
toggleReadOnly={toggleReadOnly}
helpText={
<ConfigFieldDescription
description={configField.longdesc}
className="p-form-help-text"
/>
}
/>
{!readonly && (
<>
<Button appearance="base" onClick={onCancel}>
Cancel
</Button>
<Button appearance="positive" type="submit">
Save
</Button>
{canBeReset && (
<Button
className="reset-button"
appearance="base"
type="button"
onClick={resetToDefault}
hasIcon
>
<Icon name="restart" className="flip-horizontally" />
<span>Reset to default</span>
</Button>
)}
</>
)}
</Form>
);
};

export default ClusteredSettingFormInput;
Loading

0 comments on commit ea54c7e

Please sign in to comment.