Skip to content
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
17 changes: 4 additions & 13 deletions client/src/app/components/ReadOnlyButton.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import React from "react";

import { Button, type ButtonProps, Tooltip } from "@patternfly/react-core";
import { Button, type ButtonProps } from "@patternfly/react-core";

import { READ_ONLY_TOOLTIP, ReadOnlyContext } from "./ReadOnlyContext";
import { ReadOnlyContext } from "./ReadOnlyContext";

/** Button that is automatically aria-disabled with a tooltip when the instance is in read-only mode. */
export const ReadOnlyButton: React.FC<ButtonProps> = (props) => {
const { isReadOnly } = React.useContext(ReadOnlyContext);
const { areMutationsDisabled } = React.useContext(ReadOnlyContext);

if (isReadOnly) {
return (
<Tooltip content={READ_ONLY_TOOLTIP}>
<Button {...props} isAriaDisabled />
</Tooltip>
);
}

return <Button {...props} />;
return <Button {...props} isDisabled={areMutationsDisabled} />;
};
30 changes: 16 additions & 14 deletions client/src/app/components/ReadOnlyContext/ReadOnlyContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ const mockedUseFetchTrustifyInfo =
>;

const ReadOnlyConsumer: React.FC = () => {
const { isReadOnly, isLoading } = React.useContext(ReadOnlyContext);
const { isLoading, areMutationsDisabled } = React.useContext(ReadOnlyContext);
return (
<div>
<span data-testid="read-only">{String(isReadOnly)}</span>
<span data-testid="loading">{String(isLoading)}</span>
<span data-testid="mutations-disabled">
{String(areMutationsDisabled)}
</span>
</div>
);
};
Expand All @@ -38,53 +40,53 @@ const renderWithProvider = () => {
};

describe("ReadOnlyContext", () => {
it("provides isReadOnly=true when the endpoint returns readOnly: true", () => {
it("disables mutations when the endpoint returns readOnly: true", () => {
mockedUseFetchTrustifyInfo.mockReturnValue({
trustifyInfo: { version: "0.5.0", readOnly: true },
data: { version: "0.5.0", readOnly: true },
isLoading: false,
error: null,
});

renderWithProvider();

expect(screen.getByTestId("read-only")).toHaveTextContent("true");
expect(screen.getByTestId("loading")).toHaveTextContent("false");
expect(screen.getByTestId("mutations-disabled")).toHaveTextContent("true");
});

it("provides isReadOnly=false when the endpoint returns readOnly: false", () => {
it("allows mutations when the endpoint returns readOnly: false", () => {
mockedUseFetchTrustifyInfo.mockReturnValue({
trustifyInfo: { version: "0.5.0", readOnly: false },
data: { version: "0.5.0", readOnly: false },
isLoading: false,
error: null,
});

renderWithProvider();

expect(screen.getByTestId("read-only")).toHaveTextContent("false");
expect(screen.getByTestId("mutations-disabled")).toHaveTextContent("false");
});

it("treats isReadOnly as true while loading", () => {
it("disables mutations while loading", () => {
mockedUseFetchTrustifyInfo.mockReturnValue({
trustifyInfo: undefined,
data: undefined,
isLoading: true,
error: null,
});

renderWithProvider();

expect(screen.getByTestId("read-only")).toHaveTextContent("true");
expect(screen.getByTestId("loading")).toHaveTextContent("true");
expect(screen.getByTestId("mutations-disabled")).toHaveTextContent("true");
});

it("defaults to isReadOnly=false when the fetch errors", () => {
it("allows mutations when the fetch errors", () => {
mockedUseFetchTrustifyInfo.mockReturnValue({
trustifyInfo: undefined,
data: undefined,
isLoading: false,
error: new Error("network error"),
});

renderWithProvider();

expect(screen.getByTestId("read-only")).toHaveTextContent("false");
expect(screen.getByTestId("mutations-disabled")).toHaveTextContent("false");
});
});
4 changes: 2 additions & 2 deletions client/src/app/components/ReadOnlyContext/ReadOnlyContext.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from "react";

interface IReadOnlyContext {
isReadOnly: boolean;
isLoading: boolean;
areMutationsDisabled: boolean;
}

export const ReadOnlyContext = React.createContext<IReadOnlyContext>({
isReadOnly: false,
isLoading: true,
areMutationsDisabled: true,
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ interface IReadOnlyProvider {
export const ReadOnlyProvider: React.FunctionComponent<IReadOnlyProvider> = ({
children,
}) => {
const { trustifyInfo, isLoading } = useFetchTrustifyInfo();
const isReadOnly = isLoading || (trustifyInfo?.readOnly ?? false);
const { data: trustifyInfo, isLoading } = useFetchTrustifyInfo();
const isReadOnly = trustifyInfo?.readOnly ?? false;
const areMutationsDisabled = isLoading || isReadOnly;

return (
<ReadOnlyContext.Provider value={{ isReadOnly, isLoading }}>
<ReadOnlyContext.Provider value={{ isLoading, areMutationsDisabled }}>
{children}
</ReadOnlyContext.Provider>
);
Expand Down
1 change: 0 additions & 1 deletion client/src/app/components/ReadOnlyContext/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from "./ReadOnlyContext";
export * from "./ReadOnlyProvider";
export * from "./utils";
11 changes: 0 additions & 11 deletions client/src/app/components/ReadOnlyContext/utils.ts

This file was deleted.

26 changes: 20 additions & 6 deletions client/src/app/layout/default-layout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,15 @@ vi.mock("@patternfly/react-core", async () => {
};
});

const renderLayout = (isReadOnly: boolean) => {
const renderLayout = ({
isLoading = false,
areMutationsDisabled = false,
}: {
isLoading?: boolean;
areMutationsDisabled?: boolean;
}) => {
return render(
<ReadOnlyContext.Provider value={{ isReadOnly, isLoading: false }}>
<ReadOnlyContext.Provider value={{ isLoading, areMutationsDisabled }}>
<DefaultLayout>
<div data-testid="page-content">Page content</div>
</DefaultLayout>
Expand All @@ -44,19 +50,27 @@ const renderLayout = (isReadOnly: boolean) => {
};

describe("DefaultLayout", () => {
it("shows a read-only banner when isReadOnly is true", () => {
renderLayout(true);
it("shows a read-only banner when mutations are disabled", () => {
renderLayout({ areMutationsDisabled: true });

expect(screen.getByText(/running in read-only mode/i)).toBeInTheDocument();
expect(screen.getByTestId("page-content")).toBeInTheDocument();
});

it("does not show a banner when isReadOnly is false", () => {
renderLayout(false);
it("does not show a banner when mutations are allowed", () => {
renderLayout({ areMutationsDisabled: false });

expect(
screen.queryByText(/running in read-only mode/i),
).not.toBeInTheDocument();
expect(screen.getByTestId("page-content")).toBeInTheDocument();
});

it("does not show a banner while trustify info is loading", () => {
renderLayout({ isLoading: true, areMutationsDisabled: true });

expect(
screen.queryByText(/running in read-only mode/i),
).not.toBeInTheDocument();
});
});
54 changes: 33 additions & 21 deletions client/src/app/layout/default-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import { ReadOnlyContext } from "@app/components/ReadOnlyContext";

import { HeaderApp } from "./header";
import { SidebarApp } from "./sidebar";
import { LoadingWrapper } from "@app/components/LoadingWrapper";
import { getAxiosErrorMessage } from "@app/utils/utils";

interface DefaultLayoutProps {
children?: React.ReactNode;
}

export const DefaultLayout: React.FC<DefaultLayoutProps> = ({ children }) => {
const { isReadOnly } = React.useContext(ReadOnlyContext);
const { isLoading, areMutationsDisabled } = React.useContext(ReadOnlyContext);
const pageId = "main-content-page-layout-horizontal-nav";
const PageSkipToContent = (
<SkipToContent href={`#${pageId}`}>Skip to content</SkipToContent>
Expand All @@ -35,27 +37,37 @@ export const DefaultLayout: React.FC<DefaultLayoutProps> = ({ children }) => {
skipToContent={PageSkipToContent}
mainContainerId={pageId}
>
{isReadOnly && (
<Banner
isSticky
status="info"
screenReaderText="Info banner: application is in read-only mode"
>
<Flex
justifyContent={{ default: "justifyContentCenter" }}
alignItems={{ default: "alignItemsCenter" }}
gap={{ default: "gapSm" }}
<LoadingWrapper
isFetching={isLoading}
isFetchingState={<></>}
fetchErrorState={(error) => (
<Banner isSticky status="danger">
{getAxiosErrorMessage(error)}
</Banner>
)}
>
{areMutationsDisabled && (
<Banner
isSticky
status="info"
screenReaderText="Info banner: application is in read-only mode"
>
<FlexItem>
<InfoCircleIcon />
</FlexItem>
<FlexItem>
This instance is running in read-only mode. Uploads, imports, and
other modifications are disabled.
</FlexItem>
</Flex>
</Banner>
)}
<Flex
justifyContent={{ default: "justifyContentCenter" }}
alignItems={{ default: "alignItemsCenter" }}
gap={{ default: "gapSm" }}
>
<FlexItem>
<InfoCircleIcon />
</FlexItem>
<FlexItem>
This instance is running in read-only mode. Uploads, imports,
and other modifications are disabled.
</FlexItem>
</Flex>
</Banner>
)}
</LoadingWrapper>
<PageContentWithDrawerProvider>
{children}
<Notifications />
Expand Down
11 changes: 4 additions & 7 deletions client/src/app/pages/advisory-list/advisory-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ import type { AdvisorySummary } from "@app/client";
import { ConfirmDialog } from "@app/components/ConfirmDialog.tsx";
import { LabelsAsList } from "@app/components/LabelsAsList.tsx";
import { NotificationsContext } from "@app/components/NotificationsContext";
import {
readOnlyActionProps,
ReadOnlyContext,
} from "@app/components/ReadOnlyContext";
import { ReadOnlyContext } from "@app/components/ReadOnlyContext";
import { SimplePagination } from "@app/components/SimplePagination";
import {
ConditionalTableBody,
Expand All @@ -50,7 +47,7 @@ import { advisoryDeleteDialogProps } from "@app/Constants";

export const AdvisoryTable: React.FC = () => {
const { pushNotification } = React.useContext(NotificationsContext);
const { isReadOnly } = React.useContext(ReadOnlyContext);
const { areMutationsDisabled } = React.useContext(ReadOnlyContext);

const { isFetching, fetchError, tableControls } = React.useContext(
AdvisorySearchContext,
Expand Down Expand Up @@ -224,7 +221,7 @@ export const AdvisoryTable: React.FC = () => {
onClick: () => {
setEditLabelsModalState(item);
},
...readOnlyActionProps(isReadOnly),
isDisabled: areMutationsDisabled,
},
{
title: "Download",
Expand All @@ -241,7 +238,7 @@ export const AdvisoryTable: React.FC = () => {
onClick: () => {
setAdvisoryToDelete(item);
},
...readOnlyActionProps(isReadOnly),
isDisabled: areMutationsDisabled,
},
]}
/>
Expand Down
13 changes: 6 additions & 7 deletions client/src/app/pages/importer-list/importer-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const getImporterStatus = (importer: Importer): ImporterStatus => {

export const ImporterList: React.FC = () => {
const { pushNotification } = React.useContext(NotificationsContext);
const { isReadOnly } = React.useContext(ReadOnlyContext);
const { areMutationsDisabled } = React.useContext(ReadOnlyContext);

// Actions that each row can trigger
type RowAction = "enable" | "disable" | "run";
Expand Down Expand Up @@ -397,7 +397,7 @@ export const ImporterList: React.FC = () => {
onClick: () => {
prepareActionOnRow("enable", item);
},
...readOnlyActionProps(isReadOnly),
isDisabled: areMutationsDisabled,
},
]
: [
Expand All @@ -406,17 +406,16 @@ export const ImporterList: React.FC = () => {
onClick: () => {
prepareActionOnRow("run", item);
},
isAriaDisabled:
isReadOnly ||
importerStatus === "running",
...readOnlyActionProps(isReadOnly),
isDisabled:
importerStatus === "running" ||
areMutationsDisabled,
},
{
title: "Disable",
onClick: () => {
prepareActionOnRow("disable", item);
},
...readOnlyActionProps(isReadOnly),
isDisabled: areMutationsDisabled,
},
]),
]}
Expand Down
12 changes: 4 additions & 8 deletions client/src/app/pages/sbom-groups/sbom-groups-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ import type { Group } from "@app/client";
import { ConfirmDialog } from "@app/components/ConfirmDialog.tsx";
import { LoadingWrapper } from "@app/components/LoadingWrapper";
import { NotificationsContext } from "@app/components/NotificationsContext";
import {
readOnlyActionProps,
ReadOnlyContext,
} from "@app/components/ReadOnlyContext";
import { ReadOnlyContext } from "@app/components/ReadOnlyContext";
import { SimplePagination } from "@app/components/SimplePagination";
import { TableCellError } from "@app/components/TableCellError";
import { ConditionalTableBody } from "@app/components/TableControls";
Expand Down Expand Up @@ -155,19 +152,18 @@ const SbomGroupRow: React.FC<{
isExpanded || hasBeenExpanded.current,
);

const { isReadOnly } = React.useContext(ReadOnlyContext);
const { areMutationsDisabled } = React.useContext(ReadOnlyContext);

const actions: IAction[] = [
{
title: "Edit",
onClick: () => onEdit(node),
...readOnlyActionProps(isReadOnly),
isDisabled: areMutationsDisabled,
},
{
title: "Delete",
onClick: () => onDelete(node),
isDisabled: !isReadOnly && !!node.number_of_groups,
...readOnlyActionProps(isReadOnly),
isDisabled: areMutationsDisabled || !!node.number_of_groups,
},
];

Expand Down
Loading
Loading