Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: provide UI backward compatibility for older lxd versions [WD-8671] #647

Merged
merged 1 commit into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/api/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ export const fetchResources = (): Promise<LxdResources> => {
});
};

export const fetchConfigOptions = (): Promise<LxdConfigOptions> => {
export const fetchConfigOptions = (
hasMetadataConfiguration: boolean,
): Promise<LxdConfigOptions | null> => {
if (!hasMetadataConfiguration) {
return new Promise((resolve) => resolve(null));
}

return new Promise((resolve, reject) => {
fetch("/1.0/metadata/configuration")
.then(handleResponse)
Expand All @@ -45,7 +51,13 @@ export const fetchConfigOptions = (): Promise<LxdConfigOptions> => {
});
};

export const fetchDocObjects = (): Promise<string[]> => {
export const fetchDocObjects = (
hasDocumentationObject: boolean,
): Promise<string[]> => {
if (!hasDocumentationObject) {
return new Promise((resolve) => resolve([]));
}

return new Promise((resolve, reject) => {
fetch("/documentation/objects.inv.txt")
.then(handleTextResponse)
Expand Down
13 changes: 3 additions & 10 deletions src/context/useDocs.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import { useSettings } from "context/useSettings";
import { useSupportedFeatures } from "./useSupportedFeatures";

export const useDocs = (): string => {
const remoteBase = "https://documentation.ubuntu.com/lxd/en/latest";
const localBase = "/documentation";

const { data: settings } = useSettings();
const serverVersion = settings?.environment?.server_version;
const serverMajor = parseInt(serverVersion?.split(".")[0] ?? "0");
const serverMinor = parseInt(serverVersion?.split(".")[1] ?? "0");
const { hasLocalDocumentation } = useSupportedFeatures();

if (
!serverVersion ||
serverMajor < 5 ||
(serverMajor === 5 && serverMinor < 19)
) {
if (!hasLocalDocumentation) {
return remoteBase;
}

Expand Down
24 changes: 24 additions & 0 deletions src/context/useSupportedFeatures.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useSettings } from "./useSettings";

export const useSupportedFeatures = () => {
const { data: settings, isLoading, error } = useSettings();
const apiExtensions = new Set(settings?.api_extensions);

const serverVersion = settings?.environment?.server_version;
const serverMajor = parseInt(serverVersion?.split(".")[0] ?? "0");
const serverMinor = parseInt(serverVersion?.split(".")[1] ?? "0");

return {
settings,
isSettingsLoading: isLoading,
settingsError: error,
hasCustomVolumeIso: apiExtensions.has("custom_volume_iso"),
hasProjectsNetworksZones: apiExtensions.has("projects_networks_zones"),
hasStorageBuckets: apiExtensions.has("storage_buckets"),
hasMetadataConfiguration: apiExtensions.has("metadata_configuration"),
hasLocalDocumentation:
!!serverVersion && serverMajor >= 5 && serverMinor >= 19,
hasDocumentationObject:
!!serverVersion && serverMajor >= 5 && serverMinor >= 20,
edlerd marked this conversation as resolved.
Show resolved Hide resolved
};
};
4 changes: 3 additions & 1 deletion src/pages/instances/InstanceConsole.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from "../../lib/spice/src/inputs";
import AttachIsoBtn from "pages/instances/actions/AttachIsoBtn";
import NotificationRow from "components/NotificationRow";
import { useSupportedFeatures } from "context/useSupportedFeatures";

interface Props {
instance: LxdInstance;
Expand All @@ -28,6 +29,7 @@ const InstanceConsole: FC<Props> = ({ instance }) => {
const notify = useNotify();
const isVm = instance.type === "virtual-machine";
const [isGraphic, setGraphic] = useState(isVm);
const { hasCustomVolumeIso } = useSupportedFeatures();

const isRunning = instance.status === "Running";

Expand Down Expand Up @@ -76,7 +78,7 @@ const InstanceConsole: FC<Props> = ({ instance }) => {
</div>
{isGraphic && isRunning && (
<div>
<AttachIsoBtn instance={instance} />
{hasCustomVolumeIso && <AttachIsoBtn instance={instance} />}
<Button
className="u-no-margin--bottom"
onClick={() => handleFullScreen()}
Expand Down
7 changes: 6 additions & 1 deletion src/pages/instances/forms/InstanceCreateDetailsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import InstanceLocationSelect from "pages/instances/forms/InstanceLocationSelect
import UseCustomIsoBtn from "pages/images/actions/UseCustomIsoBtn";
import AutoExpandingTextArea from "components/AutoExpandingTextArea";
import ScrollableForm from "components/ScrollableForm";
import { useSupportedFeatures } from "context/useSupportedFeatures";

export interface InstanceDetailsFormValues {
name?: string;
Expand Down Expand Up @@ -67,6 +68,8 @@ const InstanceCreateDetailsForm: FC<Props> = ({
onSelectImage,
project,
}) => {
const { hasCustomVolumeIso } = useSupportedFeatures();

function figureBaseImageName() {
const image = formik.values.image;
return image
Expand Down Expand Up @@ -123,7 +126,9 @@ const InstanceCreateDetailsForm: FC<Props> = ({
) : (
<>
<SelectImageBtn onSelect={onSelectImage} />
<UseCustomIsoBtn onSelect={onSelectImage} />
{hasCustomVolumeIso && (
<UseCustomIsoBtn onSelect={onSelectImage} />
)}
</>
)}
</div>
Expand Down
11 changes: 11 additions & 0 deletions src/pages/projects/CreateProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import BaseLayout from "components/BaseLayout";
import FormFooterLayout from "components/forms/FormFooterLayout";
import { slugify } from "util/slugify";
import { useToastNotification } from "context/toastNotificationProvider";
import { useSupportedFeatures } from "context/useSupportedFeatures";

export type ProjectFormValues = ProjectDetailsFormValues &
ProjectResourceLimitsFormValues &
Expand All @@ -56,6 +57,8 @@ const CreateProject: FC = () => {
const queryClient = useQueryClient();
const controllerState = useState<AbortController | null>(null);
const [section, setSection] = useState(slugify(PROJECT_DETAILS));
const { hasProjectsNetworksZones, hasStorageBuckets } =
useSupportedFeatures();

const ProjectSchema = Yup.object().shape({
name: Yup.string()
Expand Down Expand Up @@ -89,6 +92,14 @@ const CreateProject: FC = () => {
}
: {};

if (!hasProjectsNetworksZones) {
values.features_networks_zones = undefined;
}

if (!hasStorageBuckets) {
values.features_storage_buckets = undefined;
}

createProject(
JSON.stringify({
...projectDetailPayload(values),
Expand Down
11 changes: 11 additions & 0 deletions src/pages/projects/EditProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import FormFooterLayout from "components/forms/FormFooterLayout";
import { useNavigate, useParams } from "react-router-dom";
import { slugify } from "util/slugify";
import { useToastNotification } from "context/toastNotificationProvider";
import { useSupportedFeatures } from "context/useSupportedFeatures";

interface Props {
project: LxdProject;
Expand All @@ -44,6 +45,8 @@ const EditProject: FC<Props> = ({ project }) => {
const toastNotify = useToastNotification();
const queryClient = useQueryClient();
const { section } = useParams<{ section?: string }>();
const { hasProjectsNetworksZones, hasStorageBuckets } =
useSupportedFeatures();

const updateFormHeight = () => {
updateMaxHeight("form-contents", "p-bottom-controls");
Expand All @@ -61,6 +64,14 @@ const EditProject: FC<Props> = ({ project }) => {
initialValues: initialValues,
validationSchema: ProjectSchema,
onSubmit: (values) => {
if (!hasProjectsNetworksZones) {
values.features_networks_zones = undefined;
}

if (!hasStorageBuckets) {
values.features_storage_buckets = undefined;
}

const projectPayload = getPayload(values) as LxdProject;

projectPayload.etag = project.etag;
Expand Down
68 changes: 38 additions & 30 deletions src/pages/projects/forms/ProjectDetailsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { isProjectEmpty } from "util/projects";
import { LxdProject } from "types/project";
import AutoExpandingTextArea from "components/AutoExpandingTextArea";
import ScrollableForm from "components/ScrollableForm";
import { useSupportedFeatures } from "context/useSupportedFeatures";

export interface ProjectDetailsFormValues {
name: string;
Expand Down Expand Up @@ -74,6 +75,9 @@ interface Props {
}

const ProjectDetailsForm: FC<Props> = ({ formik, project, isEdit }) => {
const { hasProjectsNetworksZones, hasStorageBuckets } =
useSupportedFeatures();

const figureFeatures = () => {
if (
formik.values.features_images === undefined &&
Expand Down Expand Up @@ -222,36 +226,40 @@ const ProjectDetailsForm: FC<Props> = ({ formik, project, isEdit }) => {
checked={formik.values.features_networks}
disabled={readOnly || isDefaultProject || isNonEmpty}
/>
<CheckboxInput
id="features_networks_zones"
name="features_networks_zones"
label="Network zones"
onChange={() =>
void formik.setFieldValue(
"features_networks_zones",
!formik.values.features_networks_zones,
)
}
checked={formik.values.features_networks_zones}
disabled={
readOnly ||
isDefaultProject ||
(isNonEmpty && hadFeaturesNetworkZones)
}
/>
<CheckboxInput
id="features_storage_buckets"
name="features_storage_buckets"
label="Storage buckets"
onChange={() =>
void formik.setFieldValue(
"features_storage_buckets",
!formik.values.features_storage_buckets,
)
}
checked={formik.values.features_storage_buckets}
disabled={readOnly || isDefaultProject || isNonEmpty}
/>
{hasProjectsNetworksZones && (
<CheckboxInput
id="features_networks_zones"
name="features_networks_zones"
label="Network zones"
onChange={() =>
void formik.setFieldValue(
"features_networks_zones",
!formik.values.features_networks_zones,
)
}
checked={formik.values.features_networks_zones}
disabled={
readOnly ||
isDefaultProject ||
(isNonEmpty && hadFeaturesNetworkZones)
}
/>
)}
{hasStorageBuckets && (
<CheckboxInput
id="features_storage_buckets"
name="features_storage_buckets"
label="Storage buckets"
onChange={() =>
void formik.setFieldValue(
"features_storage_buckets",
!formik.values.features_storage_buckets,
)
}
checked={formik.values.features_storage_buckets}
disabled={readOnly || isDefaultProject || isNonEmpty}
/>
)}
<CheckboxInput
id="features_storage_volumes"
name="features_storage_volumes"
Expand Down
4 changes: 3 additions & 1 deletion src/pages/settings/ConfigFieldDescription.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useDocs } from "context/useDocs";
import { configDescriptionToHtml } from "util/config";
import { useQuery } from "@tanstack/react-query";
import { fetchDocObjects } from "api/server";
import { useSupportedFeatures } from "context/useSupportedFeatures";

interface Props {
description?: string;
Expand All @@ -11,9 +12,10 @@ interface Props {

const ConfigFieldDescription: FC<Props> = ({ description, className }) => {
const docBaseLink = useDocs();
const { hasDocumentationObject } = useSupportedFeatures();
const objectsInvTxt = useQuery({
queryKey: ["documentation/objects.inv.txt"],
queryFn: fetchDocObjects,
queryFn: () => fetchDocObjects(hasDocumentationObject),
});

return description ? (
Expand Down
28 changes: 21 additions & 7 deletions src/pages/settings/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { FC, useState } from "react";
import {
MainTable,
Notification,
Row,
SearchBox,
useNotify,
} from "@canonical/react-components";
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";
Expand All @@ -20,25 +20,30 @@ import ConfigFieldDescription from "pages/settings/ConfigFieldDescription";
import { toConfigFields } from "util/config";
import CustomLayout from "components/CustomLayout";
import PageHeader from "components/PageHeader";
import { useSupportedFeatures } from "context/useSupportedFeatures";

const Settings: FC = () => {
const docBaseLink = useDocs();
const [query, setQuery] = useState("");
const notify = useNotify();
const {
hasMetadataConfiguration,
settings,
isSettingsLoading,
settingsError,
} = useSupportedFeatures();

const { data: configOptions, isLoading: isConfigOptionsLoading } = useQuery({
queryKey: [queryKeys.configOptions],
queryFn: fetchConfigOptions,
queryFn: () => fetchConfigOptions(hasMetadataConfiguration),
});

const { data: settings, error, isLoading: isSettingsLoading } = useSettings();

if (isConfigOptionsLoading || isSettingsLoading) {
return <Loader />;
}

if (error) {
notify.failure("Loading settings failed", error);
if (settingsError) {
notify.failure("Loading settings failed", settingsError);
}

const getValue = (configField: ConfigField): string | undefined => {
Expand All @@ -62,7 +67,7 @@ const Settings: FC = () => {
{ content: "Value" },
];

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

configFields.push({
key: "user.ui_title",
Expand Down Expand Up @@ -166,6 +171,15 @@ const Settings: FC = () => {
>
<NotificationRow />
<Row>
{!hasMetadataConfiguration && (
<Notification
severity="information"
title="Get more server settings"
titleElement="h2"
>
Update to LXD v5.19.0 or later to access more server settings
</Notification>
)}
<ScrollableTable
dependencies={[notify.notification, rows]}
tableId="settings-table"
Expand Down
Loading
Loading