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

Issue/iso7 banner #6017

Open
wants to merge 3 commits into
base: iso7
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions changelogs/unreleased/5942-expert-button-banner.yml
Original file line number Diff line number Diff line change
@@ -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}}"
12 changes: 2 additions & 10 deletions cypress/e2e/scenario-2.4-expert-mode.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Expand Down
1 change: 1 addition & 0 deletions src/Data/Managers/V2/POST/UpdateEnvConfig/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./useUpdateEnvConfig";
71 changes: 71 additions & 0 deletions src/Data/Managers/V2/POST/UpdateEnvConfig/useUpdateEnvConfig.ts
Original file line number Diff line number Diff line change
@@ -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<void, Error, ConfigUpdate, unknown>}- The mutation object from `useMutation` hook.
*/
export const useUpdateEnvConfig = (
environment: string,
): UseMutationResult<void, Error, ConfigUpdate, unknown> => {
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<void>} - 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<void> => {
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"));
},
});
};
134 changes: 134 additions & 0 deletions src/UI/Components/ExpertBanner/ExpertBanner.test.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<MemoryRouter initialEntries={[{ search: "?env=aaa" }]}>
<DependencyProvider
dependencies={{
...dependencies,
}}
>
<QueryClientProvider client={client}>
<StoreProvider store={store}>
<ExpertBanner environmentId="aaa" />
</StoreProvider>
</QueryClientProvider>
</DependencyProvider>
</MemoryRouter>
);
};

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();
});
});
66 changes: 60 additions & 6 deletions src/UI/Components/ExpertBanner/ExpertBanner.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> | null} The rendered banner if the expert mode is enabled, otherwise null.
*/
export const ExpertBanner: React.FC<Props> = ({ environmentId }) => {
const [errorMessage, setMessage] = useState<string | undefined>(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() ? (
<React.Fragment>
<>
{isError && errorMessage && (
<ToastAlert
data-testid="ToastAlert"
title={words("error")}
message={errorMessage}
setMessage={setMessage}
/>
)}
<Banner
isSticky
variant="red"
id="expert-mode-banner"
aria-label="expertModeActive"
>
LSM expert mode is enabled, proceed with caution.
<Flex
justifyContent={{ default: "justifyContentCenter" }}
gap={{ default: "gapXs" }}
>
{words("banner.expertMode")}
<Button
variant="link"
isInline
onClick={() => {
setIsLoading(true);
mutate({ id: "enable_lsm_expert_mode", value: false });
}}
>
{words("banner.disableExpertMode")}
</Button>
{isLoading && <StyledSpinner size="sm" />}
</Flex>
</Banner>
</React.Fragment>
</>
) : null;
};

const StyledSpinner = styled(Spinner)`
margin-left: 0.5rem;
--pf-v5-c-spinner--Color: white;
`;
6 changes: 4 additions & 2 deletions src/UI/Components/LicenseBanner/LicenseBanner.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -16,7 +16,9 @@ export const LicenseBanner: React.FC = () => {

return expirationMessage ? (
<Banner isSticky variant="red" aria-label="licenceExpired">
{expirationMessage}
<Flex justifyContent={{ default: "justifyContentCenter" }}>
{expirationMessage}
</Flex>
</Banner>
) : null;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface Props {
isOptional: boolean;
isDisabled?: boolean;
handleInputChange: (value) => void;
alreadySelected: string[];
alreadySelected: string[] | null;
multi?: boolean;
}

Expand Down
9 changes: 5 additions & 4 deletions src/UI/Components/UpdateBanner/UpdateBanner.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -25,9 +26,9 @@ export const UpdateBanner: React.FunctionComponent<Props> = (props) => {
const banner = (
<React.Fragment>
<Banner isSticky variant="gold" aria-label="newVersionAvailable">
You are running {currentVersion}, a new version is available! Please
hard-reload (Ctrl+F5 | Cmd + Shift + R) your page to load the new
version.
<Flex justifyContent={{ default: "justifyContentCenter" }}>
{words("banner.updateBanner")(currentVersion)}
</Flex>
</Banner>
</React.Fragment>
);
Expand Down
2 changes: 1 addition & 1 deletion src/UI/Root/Components/PageFrame/PageFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const PageFrame: React.FC<React.PropsWithChildren<Props>> = ({
return (
<>
<div role="alert" aria-label="bannerNotifications">
<ExpertBanner />
{environmentId && <ExpertBanner environmentId={environmentId} />}
<LicenseBanner />
</div>
<div className="pf-m-grow" style={{ minHeight: "0%" }}>
Expand Down
4 changes: 4 additions & 0 deletions src/UI/words.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down