Skip to content

Commit

Permalink
feat(config) use config api for server settings WD-7389
Browse files Browse the repository at this point in the history
  • Loading branch information
edlerd committed Nov 17, 2023
1 parent 5e4ff68 commit 23f0a20
Show file tree
Hide file tree
Showing 9 changed files with 218 additions and 530 deletions.
471 changes: 0 additions & 471 deletions public/assets/data/config-options.json

This file was deleted.

11 changes: 10 additions & 1 deletion src/api/server.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { handleResponse } from "util/helpers";
import { LxdSettings } from "types/server";
import { LxdApiResponse } from "types/apiResponse";
import { LxdConfigPair } from "types/config";
import { LxdConfigOptions, LxdConfigPair } from "types/config";
import { LxdResources } from "types/resources";

export const fetchSettings = (): Promise<LxdSettings> => {
Expand Down Expand Up @@ -35,3 +35,12 @@ export const fetchResources = (): Promise<LxdResources> => {
.catch(reject);
});
};

export const fetchConfigOptions = (): Promise<LxdConfigOptions> => {
return new Promise((resolve, reject) => {
fetch("/1.0/metadata/configuration")
.then(handleResponse)
.then((data: LxdApiResponse<LxdConfigOptions>) => resolve(data.metadata))
.catch(reject);
});
};
30 changes: 30 additions & 0 deletions src/pages/settings/ConfigFieldDescription.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { FC } from "react";
import { LxdConfigField } from "types/config";

interface Props {
configField: LxdConfigField;
}

const ConfigFieldDescription: FC<Props> = ({ configField }) => {
const description = configField.longdesc
? configField.longdesc
: configField.shortdesc ?? "";

const clearString = (string: string) =>
string?.replace("`", "<code>").replace("`", "</code>") ?? "";

let count = 0;
let stringElement = description;
while (stringElement.includes("`") && count++ < 100) {
stringElement = clearString(stringElement);
}

return (
<p
className="p-text--small u-no-margin u-text--muted"
dangerouslySetInnerHTML={{ __html: stringElement }}
></p>
);
};

export default ConfigFieldDescription;
2 changes: 1 addition & 1 deletion src/pages/settings/SettingFormInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const SettingFormInput: FC<Props> = ({
const canBeReset = String(configField.default) !== String(value);

const resetToDefault = () => {
setValue(configField.default as string);
setValue(configField.default);
};

return (
Expand Down
115 changes: 63 additions & 52 deletions src/pages/settings/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { FC, useEffect, useState } from "react";
import React, { FC, useState } from "react";
import {
Col,
MainTable,
Expand All @@ -7,37 +7,35 @@ import {
useNotify,
} from "@canonical/react-components";
import BaseLayout from "components/BaseLayout";
import { handleResponse } from "util/helpers";
import { LxdConfigField } from "types/config";
import SettingForm from "./SettingForm";
import Loader from "components/Loader";
import { useSettings } from "context/useSettings";
import NotificationRow from "components/NotificationRow";
import ScrollableTable from "components/ScrollableTable";
import HelpLink from "components/HelpLink";
import { useDocs } from "context/useDocs";

const configOptionsUrl = "/ui/assets/data/config-options.json";
import { queryKeys } from "util/queryKeys";
import { fetchConfigOptions } from "api/server";
import { useQuery } from "@tanstack/react-query";
import { LxdConfigField } from "types/config";
import ConfigFieldDescription from "pages/settings/ConfigFieldDescription";
import { toConfigFields } from "util/config";

const Settings: FC = () => {
const docBaseLink = useDocs();
const [configOptions, setConfigOptions] = useState<LxdConfigField[]>([]);
const [query, setQuery] = useState("");
const notify = useNotify();

const loadConfigOptions = () => {
void fetch(configOptionsUrl)
.then(handleResponse)
.then((data: LxdConfigField[]) => {
setConfigOptions(data);
});
};
const { data: configOptions, isLoading: isConfigOptionsLoading } = useQuery({
queryKey: [queryKeys.configOptions],
queryFn: fetchConfigOptions,
});

useEffect(() => {
loadConfigOptions();
}, []);
const { data: settings, error, isLoading: isSettingsLoading } = useSettings();

const { data: settings, error, isLoading } = useSettings();
if (isConfigOptionsLoading || isSettingsLoading) {
return <Loader />;
}

if (error) {
notify.failure("Loading settings failed", error);
Expand All @@ -49,8 +47,8 @@ const Settings: FC = () => {
return value;
}
}
if (typeof configField.default === "boolean") {
return configField.default ? "true" : "false";
if (configField.type === "bool") {
return configField.default === "true" ? "true" : "false";
}
if (configField.default === "-") {
return undefined;
Expand All @@ -64,8 +62,28 @@ const Settings: FC = () => {
{ content: "Value" },
];

const configFields = toConfigFields(configOptions?.configs.server ?? {});

configFields.concat({
key: "user.ui_title",
default: "",
longdesc:
"Custom identifier for the title of the UI when used on this server",
type: "string",
});

configFields.sort((a, b) => {
if (a.key < b.key) {
return -1;
}
if (a.key > b.key) {
return 1;
}
return 0;
});

let lastGroup = "";
const rows = configOptions
const rows = configFields
.filter((configField) => {
if (!query) {
return true;
Expand Down Expand Up @@ -97,10 +115,8 @@ const Settings: FC = () => {
configField.key
) : (
<strong>{configField.key}</strong>
)}{" "}
<p className="p-text--small u-no-margin u-text--muted">
{configField.description}
</p>
)}
<ConfigFieldDescription configField={configField} />
</div>
),
role: "cell",
Expand Down Expand Up @@ -131,39 +147,34 @@ const Settings: FC = () => {
href={`${docBaseLink}/server/`}
title="Learn more about server configuration"
>
Settings
Server settings
</HelpLink>
}
contentClassName="settings"
>
<NotificationRow />
{isLoading && <Loader text="Loading settings..." />}
{!isLoading && (
<>
<Row>
<Col size={8}>
<SearchBox
name="search-setting"
type="text"
onChange={(value) => {
setQuery(value);
}}
placeholder="Search"
value={query}
/>
</Col>
</Row>
<Row>
<ScrollableTable dependencies={[notify.notification, rows]}>
<MainTable
headers={headers}
rows={rows}
emptyStateMsg="No data to display"
/>
</ScrollableTable>
</Row>
</>
)}
<Row>
<Col size={8}>
<SearchBox
name="search-setting"
type="text"
onChange={(value) => {
setQuery(value);
}}
placeholder="Search"
value={query}
/>
</Col>
</Row>
<Row>
<ScrollableTable dependencies={[notify.notification, rows]}>
<MainTable
headers={headers}
rows={rows}
emptyStateMsg="No data to display"
/>
</ScrollableTable>
</Row>
</BaseLayout>
</>
);
Expand Down
32 changes: 27 additions & 5 deletions src/types/config.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
export type LxdConfigPair = Record<string, string>;

export interface LxdConfigField {
default: string | boolean;
description: string;
export type LxdConfigField = LxdConfigOptionValue & {
default: string;
key: string;
scope: "global" | "local";
type: "string" | "integer" | "bool";
};

export interface LxdConfigOptionValue {
defaultdesc?: string;
longdesc?: string;
scope?: "global" | "local";
shortdesc?: string;
type: "bool" | "string" | "integer";
}

export interface LxcConfigOptionCategories {
[category: string]: {
keys: {
[key: string]: LxdConfigOptionValue;
}[];
};
}

export interface LxdConfigOptions {
configs: {
cluster: LxcConfigOptionCategories;
instance: LxcConfigOptionCategories;
project: LxcConfigOptionCategories;
server: LxcConfigOptionCategories;
};
}
68 changes: 68 additions & 0 deletions src/util/config.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { toConfigFields } from "./config";
import { LxcConfigOptionCategories } from "types/config";

const exampleConfig: LxcConfigOptionCategories = {
acme: {
keys: [
{
"acme.agree_tos": {
defaultdesc: "`false`",
shortdesc: "Agree to ACME terms of service",
type: "bool",
},
},
{
"acme.ca_url": {
defaultdesc: "`https://acme-v02.api.letsencrypt.org/`",
shortdesc: "",
type: "string",
},
},
],
},
cluster: {
keys: [
{
"cluster.healing_threshold": {
defaultdesc: "`0`",
shortdesc: "Threshold when to evacuate",
type: "integer",
},
},
{
"cluster.https_address": {
shortdesc: "Address to use for clustering traffic",
type: "string",
},
},
],
},
};

describe("toConfigFields", () => {
it("translates config categories to flat array of fields", () => {
const fields = toConfigFields(exampleConfig);

expect(fields.length).toBe(4);

expect(fields[0].key).toBe("acme.agree_tos");
expect(fields[0].shortdesc).toBe("Agree to ACME terms of service");
expect(fields[0].type).toBe("bool");
expect(fields[0].default).toBe("false");

expect(fields[1].key).toBe("acme.ca_url");
expect(fields[1].shortdesc).toBe("");
expect(fields[1].type).toBe("string");
expect(fields[1].default).toBe("https://acme-v02.api.letsencrypt.org/");

expect(fields[2].key).toBe("cluster.healing_threshold");
expect(fields[2].shortdesc).toBe("Threshold when to evacuate");
expect(fields[2].type).toBe("integer");
expect(fields[2].default).toBe("0");

expect(fields[3].key).toBe("cluster.https_address");
expect(fields[3].shortdesc).toBe("Address to use for clustering traffic");
expect(fields[3].type).toBe("string");
expect(fields[3].default).toBe("");
});
});
18 changes: 18 additions & 0 deletions src/util/config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { LxcConfigOptionCategories, LxdConfigField } from "types/config";

export const toConfigFields = (
categories: LxcConfigOptionCategories,
): LxdConfigField[] => {
return Object.values(categories).flatMap((value) =>
value.keys.flatMap(
(item) =>
Object.entries(item).flatMap(([key, value]) => ({
...value,
default: value.defaultdesc?.startsWith("`")
? value.defaultdesc.substring(1, value.defaultdesc.length - 1)
: "",
key,
}))[0],
),
);
};
1 change: 1 addition & 0 deletions src/util/queryKeys.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const queryKeys = {
certificates: "certificates",
cluster: "cluster",
configOptions: "configOptions",
groups: "groups",
images: "images",
instances: "instances",
Expand Down

0 comments on commit 23f0a20

Please sign in to comment.