Skip to content

Commit

Permalink
feat: [WD-19015] CMS Server Config for maas.machine
Browse files Browse the repository at this point in the history
Signed-off-by: Nkeiruka <[email protected]>
  • Loading branch information
Kxiru committed Feb 17, 2025
1 parent fe63069 commit f2b7a0d
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 23 deletions.
79 changes: 73 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,32 @@ export const updateSettings = (config: LxdConfigPair): Promise<void> => {
});
};

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

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

export const fetchResources = (): Promise<LxdResources> => {
return new Promise((resolve, reject) => {
fetch("/1.0/resources")
Expand Down
6 changes: 3 additions & 3 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,14 +8,14 @@ interface Props {
id: string;
isReadOnly: boolean;
onChange: (value: ClusterSpecificValues) => void;
toggleReadOnly?: () => void;
memberNames: string[];
toggleReadOnly?: () => void;
values?: ClusterSpecificValues;
canToggleSpecific?: boolean;
isDefaultSpecific?: boolean;
clusterMemberLinkTarget?: (member: string) => string;
disabled?: boolean;
helpText?: string;
helpText?: string | ReactNode;
placeholder?: string;
classname?: string;
}
Expand Down
17 changes: 14 additions & 3 deletions src/context/useSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
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 { LxdClusterMember } from "types/cluster";

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

export const useClusteredSettings = (
memberNames: LxdClusterMember[],
): UseQueryResult<LXDSettingOnClusterMember[]> => {
return useQuery({
queryKey: [queryKeys.settings, queryKeys.cluster],
queryFn: () => fetchSettingsFromClusterMembers(memberNames),
enabled: memberNames.length > 0,
});
};
84 changes: 84 additions & 0 deletions src/pages/settings/ClusteredSettingFormInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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;
readonly?: boolean;
}

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

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

const canBeReset = String(configField.default) !== String(value);

const resetToDefault = () => {
setValue({});
};

return (
<Form
onSubmit={(e) => {
e.preventDefault();
onSubmit(value);
}}
>
<ClusterSpecificInput
aria-label={configField.key}
classname="input-wrapper"
id={getConfigId(configField.key)}
values={value as ClusterSpecificValues}
isReadOnly={readonly}
onChange={(value) => setValue(value)}
memberNames={memberNames}
disabled={readonly}
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"
onClick={resetToDefault}
hasIcon
>
<Icon name="restart" className="flip-horizontally" />
<span>Reset to default</span>
</Button>
)}
</>
)}
</Form>
);
};

export default ClusteredSettingFormInput;
61 changes: 51 additions & 10 deletions src/pages/settings/SettingForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FC, useEffect, useRef, useState } from "react";
import { Button, Icon, useNotify } from "@canonical/react-components";
import { updateSettings } from "api/server";
import { updateClusteredSettings, updateSettings } from "api/server";
import type { ConfigField } from "types/config";
import { queryKeys } from "util/queryKeys";
import { useQueryClient } from "@tanstack/react-query";
Expand All @@ -11,6 +11,10 @@ import SettingFormPassword from "./SettingFormPassword";
import { useToastNotification } from "context/toastNotificationProvider";
import ResourceLabel from "components/ResourceLabel";
import { useServerEntitlements } from "util/entitlements/server";
import ClusteredSettingFormInput from "./ClusteredSettingFormInput";
import { useSettings } from "context/useSettings";
import { isClusteredServer } from "util/settings";
import { ClusterSpecificValues } from "components/ClusterSpecificSelect";

export const getConfigId = (key: string) => {
return key.replace(".", "___");
Expand All @@ -19,33 +23,50 @@ export const getConfigId = (key: string) => {
interface Props {
configField: ConfigField;
value?: string;
clusteredValue?: ClusterSpecificValues;
isLast?: boolean;
}

const SettingForm: FC<Props> = ({ configField, value, isLast }) => {
const SettingForm: FC<Props> = ({
configField,
value,
clusteredValue,
isLast,
}) => {
const { isRestricted } = useAuth();
const [isEditMode, setEditMode] = useState(false);
const notify = useNotify();
const toastNotify = useToastNotification();
const queryClient = useQueryClient();
const { canEditServerConfiguration } = useServerEntitlements();
const { data: settings } = useSettings();
const isClustered = isClusteredServer(settings);

const editRef = useRef<HTMLDivElement | null>(null);

// Special cases
const isTrustPassword = configField.key === "core.trust_password";
const isLokiAuthPassword = configField.key === "loki.auth.password";
const isMaasMachine = configField.key === "maas.machine";
const isSecret = isTrustPassword || isLokiAuthPassword;
const isClusteredInput = isClustered && isMaasMachine;

const settingLabel = (
<ResourceLabel bold type="setting" value={configField.key} />
);

const onSubmit = (newValue: string | boolean) => {
const config = {
[configField.key]: String(newValue),
};
updateSettings(config)
const onSubmit = (newValue: string | boolean | ClusterSpecificValues) => {
const isNotClustered =
typeof newValue !== "string" && typeof newValue !== "boolean";

const mutationPromise = isNotClustered
? updateSettings({ [configField.key]: String(newValue) })
: updateClusteredSettings(
newValue as unknown as ClusterSpecificValues,
configField.key,
);

mutationPromise
.then(() => {
toastNotify.success(<>Setting {settingLabel} updated.</>);
setEditMode(false);
Expand All @@ -57,6 +78,9 @@ const SettingForm: FC<Props> = ({ configField, value, isLast }) => {
void queryClient.invalidateQueries({
queryKey: [queryKeys.settings],
});
void queryClient.invalidateQueries({
queryKey: [queryKeys.settings, queryKeys.cluster],
});
});
};

Expand Down Expand Up @@ -102,6 +126,13 @@ const SettingForm: FC<Props> = ({ configField, value, isLast }) => {
onSubmit={onSubmit}
onCancel={onCancel}
/>
) : isClusteredInput ? (
<ClusteredSettingFormInput
initialValue={clusteredValue ?? {}}
configField={configField}
onSubmit={onSubmit}
onCancel={onCancel}
/>
) : (
<SettingFormInput
initialValue={value ?? ""}
Expand Down Expand Up @@ -133,9 +164,19 @@ const SettingForm: FC<Props> = ({ configField, value, isLast }) => {
: "You do not have permission to edit server configuration"
}
>
<div className="readmode-value u-truncate">
{getReadModeValue()}
</div>
{isClustered && isMaasMachine ? (
<ClusteredSettingFormInput
initialValue={clusteredValue ?? {}}
configField={configField}
onSubmit={onSubmit}
onCancel={onCancel}
readonly={true}
/>
) : (
<div className="readmode-value u-truncate">
{getReadModeValue()}
</div>
)}
<Icon name="edit" className="edit-icon" />
</Button>
)}
Expand Down
Loading

0 comments on commit f2b7a0d

Please sign in to comment.