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
14 changes: 4 additions & 10 deletions web/src/components/core/ProgressBackdrop.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2025] SUSE LLC
* Copyright (c) [2025-2026] SUSE LLC
*
* All Rights Reserved.
*
Expand Down Expand Up @@ -85,9 +85,7 @@ describe("ProgressBackdrop", () => {
});
});

// Test skipped because rerender fails when using installerRender,
// caused by how InstallerProvider manages context.
it.skip("shows 'Refreshing data...' message temporarily", async () => {
it("shows 'Refreshing data...' message temporarily", async () => {
// Start with active progress
mockProgresses([
{
Expand Down Expand Up @@ -117,9 +115,7 @@ describe("ProgressBackdrop", () => {
expect(mockStartTracking).toHaveBeenCalled();
});

// Test skipped because rerender fails when using installerRender,
// caused by how InstallerProvider manages context.
it.skip("hides backdrop after queries are refetched", async () => {
it("hides backdrop after queries are refetched", async () => {
// Start with active progress
mockProgresses([
{
Expand Down Expand Up @@ -265,9 +261,7 @@ describe("ProgressBackdrop", () => {
);
});

// Test skipped because rerender fails when using installerRender,
// caused by how InstallerProvider manages context.
it.skip("starts tracking when progress finishes", async () => {
it("starts tracking when progress finishes", async () => {
// Start with active progress
mockProgresses([
{
Expand Down
36 changes: 20 additions & 16 deletions web/src/components/overview/OverviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* find current contact information at www.suse.com.
*/

import React, { useState } from "react";
import React, { useDeferredValue, useState } from "react";
import { Navigate } from "react-router";
import {
Button,
Expand Down Expand Up @@ -51,7 +51,7 @@ import { _ } from "~/i18n";
import type { Product } from "~/types/software";

import textStyles from "@patternfly/react-styles/css/utilities/Text/text";
import { useProgress } from "~/hooks/use-progress-tracking";
import { useProgressTracking } from "~/hooks/use-progress-tracking";

type ConfirmationPopupProps = {
product: Product;
Expand Down Expand Up @@ -93,19 +93,15 @@ const ConfirmationPopup = ({
);
};

export default function OverviewPage() {
const product = useProductInfo();
const OverviewPageContent = ({ product }) => {
const issues = useIssues();
const progresses = useProgress();
const { loading } = useProgressTracking();
const isReady = useDeferredValue(!loading);
const { actions } = useDestructiveActions();
const [showConfirmation, setShowConfirmation] = useState(false);
const hasIssues = !isEmpty(issues);
const hasDestructiveActions = actions.length > 0;

if (!product) {
return <Navigate to={PRODUCT.root} />;
}

const [buttonLocationStart, buttonLocationLabel, buttonLocationEnd] = _(
// TRANSLATORS: This hint helps users locate the install button. Text inside
// square brackets [] appears in bold. Keep brackets for proper formatting.
Expand All @@ -123,10 +119,8 @@ export default function OverviewPage() {

const onCancel = () => setShowConfirmation(false);

const isProposalReady = isEmpty(progresses);

const getInstallButtonText = () => {
if (hasIssues || !isProposalReady) return _("Install");
if (hasIssues || !isReady) return _("Install");
if (hasDestructiveActions) return _("Install now with potential data loss");
return _("Install now");
};
Expand Down Expand Up @@ -169,20 +163,20 @@ export default function OverviewPage() {
size="lg"
variant={hasDestructiveActions ? "danger" : "primary"}
onClick={onInstallClick}
isDisabled={hasIssues || !isProposalReady}
isDisabled={hasIssues || !isReady}
>
<Text isBold>{getInstallButtonText()}</Text>
</Button>

{!isProposalReady && (
{!isReady && (
<HelperText>
<HelperTextItem variant="warning">
<HelperTextItem variant="indeterminate">
{_("Wait until current operations are completed.")}
</HelperTextItem>
</HelperText>
)}

{hasIssues && isProposalReady && (
{hasIssues && isReady && (
<HelperText>
<HelperTextItem variant="warning">
{_(
Expand All @@ -204,4 +198,14 @@ export default function OverviewPage() {
)}
</Page>
);
};

export default function OverviewPage() {
const product = useProductInfo();

if (!product) {
return <Navigate to={PRODUCT.root} />;
}

return <OverviewPageContent product={product} />;
}
74 changes: 74 additions & 0 deletions web/src/hooks/model/progress.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright (c) [2026] SUSE LLC
*
* All Rights Reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, contact SUSE LLC.
*
* To contact SUSE LLC about this file by physical or electronic mail, you may
* find current contact information at www.suse.com.
*/

import { renderHook } from "@testing-library/react";
import { mockProgresses } from "~/test-utils";
import type { Progress } from "~/model/status";
import { useProgress } from "./progress";

const fakeSoftwareProgress: Progress = {
scope: "software",
size: 3,
steps: [
"Updating the list of repositories",
"Refreshing metadata from the repositories",
"Calculating the software proposal",
],
step: "Updating the list of repositories",
index: 1,
};

const fakeStorageProgress: Progress = {
scope: "storage",
size: 3,
steps: [],
step: "Activating storage devices",
index: 1,
};

describe("useProgress", () => {
beforeEach(() => {
mockProgresses([fakeStorageProgress, fakeSoftwareProgress]);
});

afterEach(() => {
jest.clearAllMocks();
});

it("returns all progresses when no scope is provided", () => {
const { result } = renderHook(() => useProgress());

expect(result.current).toEqual([fakeStorageProgress, fakeSoftwareProgress]);
});

it("returns the progress matching the given scope", () => {
const { result } = renderHook(() => useProgress("software"));

expect(result.current).toBe(fakeSoftwareProgress);
});

it("returns undefined when the given scope has no progress", () => {
const { result } = renderHook(() => useProgress("network"));

expect(result.current).toBeUndefined();
});
});
54 changes: 54 additions & 0 deletions web/src/hooks/model/progress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright (c) [2026] SUSE LLC
*
* All Rights Reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, contact SUSE LLC.
*
* To contact SUSE LLC about this file by physical or electronic mail, you may
* find current contact information at www.suse.com.
*/

import { isUndefined } from "radashi";
import { useStatus } from "./status";

import type { Progress, Scope } from "~/model/status";

/**
* Convenience hook that returns the currently active progress(es).
*
* - When no `scope` is provided, all active progress objects are returned.
* - When a `scope` is provided, only the progress associated with that scope
* is returned (or `undefined` if none exists).
*
* This hook is a lightweight wrapper around `useStatus`/`useState`. It is
* intentionally defined in a separate file/module to enable proper testing:
* consumers can mock progress data directly without depending on or
* re-implementing the internal logic, which would be fragile and error-prone.
*
* An example of this kind of indirect testing can be found in the
* `useProgressTracking` unit tests, which would not be possible if
* `useStatus` and `useProgress` lived in the same file/module.
*/
export function useProgress(scope?: undefined): Progress[];
export function useProgress(scope: Scope): Progress | undefined;
export function useProgress(scope?: Scope): Progress[] | Progress | undefined {
const { progresses } = useStatus();

if (isUndefined(scope)) {
return progresses;
}

return progresses.find((p) => p.scope === scope);
}
Loading