diff --git a/changelogs/unreleased/5942-expert-button-banner.yml b/changelogs/unreleased/5942-expert-button-banner.yml new file mode 100644 index 000000000..ae266437e --- /dev/null +++ b/changelogs/unreleased/5942-expert-button-banner.yml @@ -0,0 +1,6 @@ +description: Add button to disable expert mode from the banner +issue-nr: 5942 +change-type: minor +destination-branches: [iso7] +sections: + minor-improvement: "{{description}}" diff --git a/cypress/e2e/scenario-2.4-expert-mode.cy.js b/cypress/e2e/scenario-2.4-expert-mode.cy.js index 0f6ce48db..a0132b575 100644 --- a/cypress/e2e/scenario-2.4-expert-mode.cy.js +++ b/cypress/e2e/scenario-2.4-expert-mode.cy.js @@ -347,16 +347,8 @@ if (Cypress.env("edition") === "iso") { cy.get("button").contains("Yes").click(); cy.get('[aria-label="ServiceInventory-Empty"').should("to.be.visible"); - // At the end go back to settings and turn expert mode off - cy.get(".pf-v5-c-nav__item").contains("Settings").click(); - cy.get("button").contains("Configuration").click(); - cy.get('[aria-label="Row-enable_lsm_expert_mode"]') - .find(".pf-v5-c-switch") - .click(); - cy.get('[data-testid="Warning"]').should("exist"); - cy.get('[aria-label="Row-enable_lsm_expert_mode"]') - .find('[aria-label="SaveAction"]') - .click(); + // At the end turn expert mode off through the banner + cy.get("button").contains("Disable").click(); cy.get('[data-testid="Warning"]').should("not.exist"); cy.get("[id='expert-mode-banner']").should("not.exist"); }); diff --git a/src/Data/Managers/V2/POST/UpdateEnvConfig/index.ts b/src/Data/Managers/V2/POST/UpdateEnvConfig/index.ts new file mode 100644 index 000000000..068295aa9 --- /dev/null +++ b/src/Data/Managers/V2/POST/UpdateEnvConfig/index.ts @@ -0,0 +1 @@ +export * from "./useUpdateEnvConfig"; diff --git a/src/Data/Managers/V2/POST/UpdateEnvConfig/useUpdateEnvConfig.ts b/src/Data/Managers/V2/POST/UpdateEnvConfig/useUpdateEnvConfig.ts new file mode 100644 index 000000000..035eba88f --- /dev/null +++ b/src/Data/Managers/V2/POST/UpdateEnvConfig/useUpdateEnvConfig.ts @@ -0,0 +1,71 @@ +import { + UseMutationResult, + useMutation, + useQueryClient, +} from "@tanstack/react-query"; +import { ParsedNumber } from "@/Core"; +import { PrimaryBaseUrlManager } from "@/UI"; +import { Dict } from "@/UI/Components"; +import { useFetchHelpers } from "../../helpers"; + +interface ConfigUpdate { + id: string; + value: string | boolean | ParsedNumber | Dict; +} + +/** + * React Query hook for updating environment configuration settings. + * + * @param {string} environment - The environment to use for creating headers. + * @returns {UseMutationResult}- The mutation object from `useMutation` hook. + */ +export const useUpdateEnvConfig = ( + environment: string, +): UseMutationResult => { + const client = useQueryClient(); + + const baseUrlManager = new PrimaryBaseUrlManager( + globalThis.location.origin, + globalThis.location.pathname, + ); + const { createHeaders, handleErrors } = useFetchHelpers(); + const headers = createHeaders(environment); + const baseUrl = baseUrlManager.getBaseUrl(process.env.API_BASEURL); + + /** + * Update the environment configuration setting. + * + * @param {ConfigUpdate} configUpdate - The info about the config setting to update + * + * @returns {Promise} - The promise object of the fetch request. + * @throws {Error} If the response is not successful, an error with the error message is thrown. + */ + const updateConfig = async (configUpdate: ConfigUpdate): Promise => { + const { id, value } = configUpdate; + + const response = await fetch( + baseUrl + `/api/v2/environment_settings/${id}`, + { + method: "POST", + body: JSON.stringify({ value }), + headers, + }, + ); + + await handleErrors(response); + }; + + return useMutation({ + mutationFn: updateConfig, + mutationKey: ["update_env_config"], + onSuccess: () => { + client.invalidateQueries({ + queryKey: ["get_env_config"], //for the future rework of the env getter + }); + client.invalidateQueries({ + queryKey: ["get_env_details"], //for the future rework of the env getter + }); + document.dispatchEvent(new Event("settings-update")); + }, + }); +}; diff --git a/src/UI/Components/ExpertBanner/ExpertBanner.test.tsx b/src/UI/Components/ExpertBanner/ExpertBanner.test.tsx new file mode 100644 index 000000000..21e6f42ef --- /dev/null +++ b/src/UI/Components/ExpertBanner/ExpertBanner.test.tsx @@ -0,0 +1,134 @@ +import React, { act } from "react"; +import { MemoryRouter } from "react-router-dom"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen, waitFor } from "@testing-library/react"; +import { userEvent } from "@testing-library/user-event"; +import { StoreProvider } from "easy-peasy"; +import { HttpResponse, http } from "msw"; +import { setupServer } from "msw/node"; +import { getStoreInstance } from "@/Data"; +import * as useUpdateEnvConfig from "@/Data/Managers/V2/POST/UpdateEnvConfig/useUpdateEnvConfig"; //import with that exact path is required for mock to work correctly +import { dependencies } from "@/Test"; +import { DependencyProvider } from "@/UI/Dependency"; +import { ExpertBanner } from "./ExpertBanner"; + +const setup = (flag: boolean) => { + const client = new QueryClient(); + + dependencies.environmentModifier.useIsExpertModeEnabled = jest.fn(() => flag); + const store = getStoreInstance(); + + return ( + + + + + + + + + + ); +}; + +describe("Given ExpertBanner", () => { + it("When expert_mode is set to true Then should render,", () => { + render(setup(true)); + + expect( + screen.getByText("LSM expert mode is enabled, proceed with caution."), + ).toBeVisible(); + + expect(screen.getByText("Disable expert mode")).toBeVisible(); + }); + + it("When expert_mode is set to true AND user clicks to disable expert mode it Then should fire mutation function", async () => { + const mutateSpy = jest.fn(); + const spy = jest + .spyOn(useUpdateEnvConfig, "useUpdateEnvConfig") + .mockReturnValue({ + data: undefined, + error: null, + failureCount: 0, + isError: false, + isIdle: false, + isSuccess: true, + isPending: false, + reset: jest.fn(), + isPaused: false, + context: undefined, + variables: { + id: "", + value: "", + }, + failureReason: null, + submittedAt: 0, + mutateAsync: jest.fn(), + status: "success", + mutate: mutateSpy, + }); + + render(setup(true)); + + await act(async () => { + await userEvent.click(screen.getByText("Disable expert mode")); + }); + + expect(mutateSpy).toHaveBeenCalledWith({ + id: "enable_lsm_expert_mode", + value: false, + }); + spy.mockRestore(); + }); + + it("When expert_mode is set to true AND user clicks to disable expert mode it AND something was wrong with the request Then AlertToast with error message should open", async () => { + const server = setupServer( + http.post( + "/api/v2/environment_settings/enable_lsm_expert_mode", + async () => { + return HttpResponse.json( + { + message: "Request or referenced resource does not exist", + }, + { + status: 404, + }, + ); + }, + ), + ); + + server.listen(); + render(setup(true)); + + await act(async () => { + await userEvent.click(screen.getByText("Disable expert mode")); + }); + + await waitFor(() => { + expect(screen.getByText("Something went wrong")).toBeVisible(); + }); + expect( + screen.getByText("Request or referenced resource does not exist"), + ).toBeVisible(); + + expect( + screen.getByText("LSM expert mode is enabled, proceed with caution."), + ).toBeVisible(); + expect(screen.getByText("Disable expert mode")).toBeVisible(); + + server.close(); + }); + + it("When expert_mode is set to false Then should not render,", () => { + render(setup(false)); + + expect( + screen.queryByText("LSM expert mode is enabled, proceed with caution."), + ).not.toBeInTheDocument(); + }); +}); diff --git a/src/UI/Components/ExpertBanner/ExpertBanner.tsx b/src/UI/Components/ExpertBanner/ExpertBanner.tsx index 95fb41262..693048781 100644 --- a/src/UI/Components/ExpertBanner/ExpertBanner.tsx +++ b/src/UI/Components/ExpertBanner/ExpertBanner.tsx @@ -1,20 +1,74 @@ -import React, { useContext } from "react"; -import { Banner } from "@patternfly/react-core"; +import React, { useContext, useEffect, useState } from "react"; +import { Banner, Button, Flex, Spinner } from "@patternfly/react-core"; +import styled from "styled-components"; +import { useUpdateEnvConfig } from "@/Data/Managers/V2/POST/UpdateEnvConfig"; import { DependencyContext } from "@/UI/Dependency"; +import { words } from "@/UI/words"; +import { ToastAlert } from "../ToastAlert"; -export const ExpertBanner = () => { +interface Props { + environmentId: string; +} + +/** + * A React component that displays a banner when the expert mode is enabled. + * + * @props {object} props - The properties passed to the component. + * @prop {string} environmentId -The ID of the environment. + * @returns { React.FC | null} The rendered banner if the expert mode is enabled, otherwise null. + */ +export const ExpertBanner: React.FC = ({ environmentId }) => { + const [errorMessage, setMessage] = useState(undefined); const { environmentModifier } = useContext(DependencyContext); + const { mutate, isError, error } = useUpdateEnvConfig(environmentId); + const [isLoading, setIsLoading] = useState(false); // isLoading is to indicate the asynchronous operation is in progress, as we need to wait until setting will be updated, getters are still in the V1 - task https://github.com/inmanta/web-console/issues/5999 + + useEffect(() => { + if (isError) { + setMessage(error.message); + setIsLoading(false); + } + }, [isError, error]); return environmentModifier.useIsExpertModeEnabled() ? ( - + <> + {isError && errorMessage && ( + + )} - LSM expert mode is enabled, proceed with caution. + + {words("banner.expertMode")} + + {isLoading && } + - + ) : null; }; + +const StyledSpinner = styled(Spinner)` + margin-left: 0.5rem; + --pf-v5-c-spinner--Color: white; +`; diff --git a/src/UI/Components/LicenseBanner/LicenseBanner.tsx b/src/UI/Components/LicenseBanner/LicenseBanner.tsx index e674e8162..202c49728 100644 --- a/src/UI/Components/LicenseBanner/LicenseBanner.tsx +++ b/src/UI/Components/LicenseBanner/LicenseBanner.tsx @@ -1,5 +1,5 @@ import React, { useContext } from "react"; -import { Banner } from "@patternfly/react-core"; +import { Banner, Flex } from "@patternfly/react-core"; import { DependencyContext } from "@/UI/Dependency"; import { words } from "@/UI/words"; @@ -16,7 +16,9 @@ export const LicenseBanner: React.FC = () => { return expirationMessage ? ( - {expirationMessage} + + {expirationMessage} + ) : null; }; diff --git a/src/UI/Components/ServiceInstanceForm/Components/AutoCompleteInputProvider.tsx b/src/UI/Components/ServiceInstanceForm/Components/AutoCompleteInputProvider.tsx index ae6586927..bc83ff9b5 100644 --- a/src/UI/Components/ServiceInstanceForm/Components/AutoCompleteInputProvider.tsx +++ b/src/UI/Components/ServiceInstanceForm/Components/AutoCompleteInputProvider.tsx @@ -12,7 +12,7 @@ interface Props { isOptional: boolean; isDisabled?: boolean; handleInputChange: (value) => void; - alreadySelected: string[]; + alreadySelected: string[] | null; multi?: boolean; } diff --git a/src/UI/Components/UpdateBanner/UpdateBanner.tsx b/src/UI/Components/UpdateBanner/UpdateBanner.tsx index 8d836b155..babe3a08e 100644 --- a/src/UI/Components/UpdateBanner/UpdateBanner.tsx +++ b/src/UI/Components/UpdateBanner/UpdateBanner.tsx @@ -1,8 +1,9 @@ import React, { useContext, useState } from "react"; -import { Banner } from "@patternfly/react-core"; +import { Banner, Flex } from "@patternfly/react-core"; import { ApiHelper } from "@/Core"; import { GetVersionFileQueryManager } from "@/Data/Managers/GetVersionFile/OnteTimeQueryManager"; import { DependencyContext } from "@/UI/Dependency"; +import { words } from "@/UI/words"; interface Props { apiHelper: ApiHelper; @@ -25,9 +26,9 @@ export const UpdateBanner: React.FunctionComponent = (props) => { const banner = ( - You are running {currentVersion}, a new version is available! Please - hard-reload (Ctrl+F5 | Cmd + Shift + R) your page to load the new - version. + + {words("banner.updateBanner")(currentVersion)} + ); diff --git a/src/UI/Root/Components/PageFrame/PageFrame.tsx b/src/UI/Root/Components/PageFrame/PageFrame.tsx index 0f5cfd5b3..066f994e7 100644 --- a/src/UI/Root/Components/PageFrame/PageFrame.tsx +++ b/src/UI/Root/Components/PageFrame/PageFrame.tsx @@ -26,7 +26,7 @@ export const PageFrame: React.FC> = ({ return ( <>
- + {environmentId && }
diff --git a/src/UI/words.tsx b/src/UI/words.tsx index e8e735227..06bce50f1 100644 --- a/src/UI/words.tsx +++ b/src/UI/words.tsx @@ -718,12 +718,16 @@ const dict = { /** * Banners */ + "banner.expertMode": "LSM expert mode is enabled, proceed with caution. ", + "banner.updateBanner": (currentVersion: string) => + `You are running ${currentVersion}, a new version is available! Please hard-reload (Ctrl+F5 | Cmd + Shift + R) your page to load the new version.`, "banner.entitlement.expired": (days: number) => `Your license has expired ${days} days ago!`, "banner.certificate.expired": (days: number) => `Your license has expired ${days} days ago!`, "banner.certificate.will.expire": (days: number) => `Your license will expire in ${days} days.`, + "banner.disableExpertMode": "Disable expert mode", /** * Common