diff --git a/.env b/.env index 423168a628..07b6eec919 100644 --- a/.env +++ b/.env @@ -7,4 +7,7 @@ VITE_APP_BASENAME=${BASENAME} VITE_APP_VITE_BASENAME=${VITE_BASENAME} VITE_APP_WEBSOCKET_DEBUG=false VITE_APP_USABILLA_ID=fd6cf482fbbb + +# Feature flags VITE_APP_STATIC_IPS_ENABLED=false +VITE_APP_KERNEL_CRASH_DUMPS_ENABLED=false \ No newline at end of file diff --git a/src/app/settings/views/Configuration/KernelParametersForm/KernelParametersForm.test.tsx b/src/app/settings/views/Configuration/KernelParametersForm/KernelParametersForm.test.tsx index 246edd3b87..2c423e1a05 100644 --- a/src/app/settings/views/Configuration/KernelParametersForm/KernelParametersForm.test.tsx +++ b/src/app/settings/views/Configuration/KernelParametersForm/KernelParametersForm.test.tsx @@ -1,6 +1,7 @@ import { Provider } from "react-redux"; import { MemoryRouter } from "react-router-dom"; import configureStore from "redux-mock-store"; +import { test } from "vitest"; import KernelParametersForm, { Labels as FormLabels, @@ -9,7 +10,16 @@ import KernelParametersForm, { import { ConfigNames } from "@/app/store/config/types"; import type { RootState } from "@/app/store/root/types"; import * as factory from "@/testing/factories"; -import { screen, render } from "@/testing/utils"; +import { + screen, + render, + renderWithBrowserRouter, + userEvent, + waitFor, +} from "@/testing/utils"; + +const kernelCrashDumpEnabled = + process.env.VITE_APP_KERNEL_CRASH_DUMP_ENABLED === "true"; const mockStore = configureStore(); @@ -46,4 +56,75 @@ describe("KernelParametersForm", () => { }) ).toHaveValue("foo"); }); + + it("dispatches an action to update the kernel_opts value", async () => { + const state = { ...initialState }; + const store = mockStore(state); + + renderWithBrowserRouter(, { store }); + + await userEvent.clear( + screen.getByRole("textbox", { name: FormLabels.GlobalBootParams }) + ); + await userEvent.type( + screen.getByRole("textbox", { name: FormLabels.GlobalBootParams }), + "bar" + ); + + await userEvent.click(screen.getByRole("button", { name: "Save" })); + expect( + store.getActions().find((action) => action.type === "config/update") + ).toStrictEqual({ + meta: { + method: "bulk_update", + model: "config", + }, + type: "config/update", + payload: { + params: { + items: { + kernel_opts: "bar", + }, + }, + }, + }); + }); + + test.runIf(kernelCrashDumpEnabled)( + "shows a tooltip for minimum OS requirements", + async () => { + renderWithBrowserRouter(, { + state: { ...initialState }, + }); + + await userEvent.hover( + screen.getAllByRole("button", { name: "help-mid-dark" })[1] + ); + + await waitFor(() => { + expect(screen.getByRole("tooltip")).toHaveTextContent( + "Ubuntu 24.04 LTS or higher." + ); + }); + } + ); + + test.runIf(kernelCrashDumpEnabled)( + "shows a tooltip for minimum hardware requirements", + async () => { + renderWithBrowserRouter(, { + state: { ...initialState }, + }); + + await userEvent.hover( + screen.getAllByRole("button", { name: "help-mid-dark" })[0] + ); + + await waitFor(() => { + expect(screen.getByRole("tooltip")).toHaveTextContent( + ">= 4 CPU threads, >= 6GB RAM, Reserve >5x RAM size as free disk space in /var." + ); + }); + } + ); }); diff --git a/src/app/settings/views/Configuration/KernelParametersForm/KernelParametersForm.tsx b/src/app/settings/views/Configuration/KernelParametersForm/KernelParametersForm.tsx index de25597ec8..0fd8cd5ca2 100644 --- a/src/app/settings/views/Configuration/KernelParametersForm/KernelParametersForm.tsx +++ b/src/app/settings/views/Configuration/KernelParametersForm/KernelParametersForm.tsx @@ -1,8 +1,10 @@ +import { ExternalLink } from "@canonical/maas-react-components"; import { useDispatch, useSelector } from "react-redux"; import * as Yup from "yup"; import FormikField from "@/app/base/components/FormikField"; import FormikForm from "@/app/base/components/FormikForm"; +import TooltipButton from "@/app/base/components/TooltipButton"; import { configActions } from "@/app/store/config"; import configSelectors from "@/app/store/config/selectors"; @@ -13,6 +15,7 @@ type KernelParametersValues = { export enum Labels { FormLabel = "Configuration - Kernel parameters", GlobalBootParams = "Global boot parameters always passed to the kernel", + KernelCrashDump = "Try to enable kernel crash dump by default", } const KernelParametersSchema = Yup.object() @@ -45,18 +48,54 @@ const KernelParametersForm = (): JSX.Element => { label: "Kernel parameters form", }} onSubmit={(values, { resetForm }) => { - dispatch(updateConfig(values)); + dispatch(updateConfig({ kernel_opts: values.kernel_opts })); resetForm({ values }); }} saved={saved} saving={saving} validationSchema={KernelParametersSchema} > + General + {import.meta.env.VITE_APP_KERNEL_CRASH_DUMP_ENABLED === "true" && ( + <> + Kernel crash dump + + To enable kernel crash dump, the hardware{" "} + + {" "} + >= 4 CPU threads,
>= 6GB RAM,
+ Reserve >5x RAM size as free disk space in /var. + + } + />{" "} + and OS{" "} + {" "} + must meet the minimum requirements and secure boot must be + disabled. Check crash dump status in machine details.{" "} + + More about kernel crash dump + + + } + label={Labels.KernelCrashDump} + name="kernel_crash_dump" + type="checkbox" + /> + + )} ); }; diff --git a/src/scss/_patterns_icons.scss b/src/scss/_patterns_icons.scss index 60c878ce92..dcaf87bbec 100644 --- a/src/scss/_patterns_icons.scss +++ b/src/scss/_patterns_icons.scss @@ -171,6 +171,11 @@ @include maas-icon-sidebar-collapse(); } + .p-icon--help-mid-dark { + @extend %icon; + @include vf-icon-help($color-mid-dark); + } + [class*="p-circle--"].is-inline, [class*="p-icon--"].is-inline { margin-right: $sph--small;