Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
77008c8
web: add radashi dependency
dgdavid May 28, 2025
607ff10
refactor(web): use isEmpty and isNullish from radashi
dgdavid May 28, 2025
c47c44b
refactor(web): use `fork` from radashi
dgdavid May 28, 2025
171df13
refactor(web): use `isObject` from radashi
dgdavid May 28, 2025
a552fc5
refactor(web): drop custom isObjectEmpty util
dgdavid May 28, 2025
95bb5d3
refactor(web): use `noop` from radashi
dgdavid May 28, 2025
08ecd75
refactor(web): drop utils/identity
dgdavid May 28, 2025
822c902
refactor(web): use `unique` from radashi
dgdavid May 28, 2025
ef16597
refactor(web): drop utils/classNames
dgdavid May 28, 2025
34ca814
refactor(web): drop utils/slugify
dgdavid May 28, 2025
e7358d8
fix(web): make `hex` util more robust
dgdavid May 31, 2025
9c2582c
fix(util): make `mask` more efficient and robust
dgdavid May 31, 2025
4523c34
fix(web): extract useCancellablePromise hook from utils
dgdavid May 31, 2025
0ac4ae1
fix(web): extract useLocalStorage hook from utils
dgdavid Jun 2, 2025
b04f68e
refactor(web): extract useDebounce hook from utils
dgdavid Jun 2, 2025
7fae384
refactor(web): replace useDebounce hook with Radashi/debounce
dgdavid Jun 2, 2025
0cf119e
fix(web): move breakpoint overrides to Layout component
dgdavid Jun 2, 2025
e14487b
fix(web): remove unused `toValidationError` utility
dgdavid Jun 2, 2025
c4a48f4
fix(web): complete testing for localConnection util
dgdavid Jun 2, 2025
2139c2f
fix(web): add missing tests for `timezoneTime` util
dgdavid Jun 2, 2025
9ad87b4
doc(web): small wording adjustments
dgdavid Jun 2, 2025
65564d9
chore(web): remove unused useNodeSiblings hook
dgdavid Jun 6, 2025
12abfec
fix(web): drop wrong reference to D-Bus
dgdavid Jun 6, 2025
df2caeb
Merge branch 'master' into radashi
dgdavid Jun 6, 2025
0809996
fix(web): allow boolean values in network#Connection options
dgdavid Jun 6, 2025
631515e
doc(web): add entry to the changes file
dgdavid Jun 6, 2025
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
10 changes: 10 additions & 0 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"axios": "^1.9.0",
"fast-sort": "^3.4.1",
"ipaddr.js": "^2.2.0",
"radashi": "^12.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
Expand Down
8 changes: 8 additions & 0 deletions web/package/agama-web-ui.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
-------------------------------------------------------------------
Fri Jun 6 13:44:14 UTC 2025 - David Diaz <dgonzalez@suse.com>

- Replaced custom utility functions with a well-tested,
community-driven third-party dependency, refactored remaining
helpers, and removed unused code to improve maintainability
(gh#agama-project/agama#2412).

-------------------------------------------------------------------
Fri Jun 6 12:32:23 UTC 2025 - Knut Anderssen <kanderssen@suse.com>

Expand Down
2 changes: 1 addition & 1 deletion web/src/components/core/ChangeProductOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import { DropdownItem, DropdownItemProps } from "@patternfly/react-core";
import { useHref, useLocation } from "react-router-dom";
import { useProduct, useRegistration } from "~/queries/software";
import { PRODUCT as PATHS, SIDE_PATHS } from "~/routes/paths";
import { isEmpty } from "radashi";
import { _ } from "~/i18n";
import { isEmpty } from "~/utils";

/**
* DropdownItem Option for navigating to the selection product.
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/core/EmailInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

import React, { useEffect, useState } from "react";
import { InputGroup, TextInput, TextInputProps } from "@patternfly/react-core";
import { isEmpty, noop } from "~/utils";
import { isEmpty, noop } from "radashi";

/**
* Email validation.
Expand Down
14 changes: 8 additions & 6 deletions web/src/components/core/ListSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@

import React, { useState } from "react";
import { SearchInput } from "@patternfly/react-core";
import { debounce, noop } from "radashi";
import { _ } from "~/i18n";
import { noop, useDebounce } from "~/utils";

type ListSearchProps<T> = {
/** Text to display as placeholder for the search input. */
Expand All @@ -44,6 +44,12 @@ function search<T>(elements: T[], term: string): T[] {
return elements.filter(match);
}

function filterList<T>(elements: ListSearchProps<T>["elements"], term: string, action: Function) {
action(search(elements, term));
}

const searchHandler = debounce({ delay: 500 }, filterList);

/**
* Input field for searching in a given list of elements.
* @component
Expand All @@ -61,13 +67,9 @@ export default function ListSearch<T>({
onChangeProp(result);
};

const searchHandler = useDebounce((term: string) => {
updateResult(search(elements, term));
}, 500);

const onChange = (value: string) => {
setValue(value);
searchHandler(value);
searchHandler(elements, value, updateResult);
};

const onClear = () => {
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/core/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import Link, { LinkProps } from "~/components/core/Link";
import textStyles from "@patternfly/react-styles/css/utilities/Text/text";
import flexStyles from "@patternfly/react-styles/css/utilities/Flex/flex";
import { useLocation, useNavigate } from "react-router-dom";
import { isEmpty, isObject } from "~/utils";
import { isEmpty, isObject } from "radashi";
import { SIDE_PATHS } from "~/routes/paths";
import { _ } from "~/i18n";

Expand Down
4 changes: 2 additions & 2 deletions web/src/components/core/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ import {
ModalProps,
} from "@patternfly/react-core";
import { Loading } from "~/components/layout";
import { fork } from "radashi";
import { _ } from "~/i18n";
import { partition } from "~/utils";

type ButtonWithoutVariantProps = Omit<ButtonProps, "variant">;
type PredefinedAction = React.PropsWithChildren<ButtonWithoutVariantProps>;
Expand Down Expand Up @@ -212,7 +212,7 @@ const Popup = ({
children,
...props
}: PopupProps) => {
const [actions, content] = partition(React.Children.toArray(children), (child) =>
const [actions, content] = fork(React.Children.toArray(children), (child) =>
isValidElement(child) ? child.type === Actions : false,
);

Expand Down
6 changes: 3 additions & 3 deletions web/src/components/core/ServerError.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
import React from "react";
import { screen } from "@testing-library/react";
import { installerRender } from "~/test-utils";

import * as utils from "~/utils";
import { ServerError } from "~/components/core";
import { noop } from "radashi";
import * as utils from "~/utils";

jest.mock("~/components/product/ProductRegistrationAlert", () => () => (
<div>ProductRegistrationAlert Mock</div>
Expand Down Expand Up @@ -58,7 +58,7 @@ describe("ServerError", () => {
});

it("calls location.reload when user clicks on 'Reload'", async () => {
jest.spyOn(utils, "locationReload").mockImplementation(utils.noop);
jest.spyOn(utils, "locationReload").mockImplementation(noop);
const { user } = installerRender(<ServerError />);
const reloadButton = await screen.findByRole("button", { name: /Reload/i });
await user.click(reloadButton);
Expand Down
55 changes: 54 additions & 1 deletion web/src/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import Header, { HeaderProps } from "~/components/layout/Header";
import { Loading, Sidebar } from "~/components/layout";
import { IssuesDrawer, SkipTo } from "~/components/core";
import { ROOT } from "~/routes/paths";
import { agamaWidthBreakpoints, getBreakpoint } from "~/utils";

export type LayoutProps = React.PropsWithChildren<{
className?: string;
Expand All @@ -48,6 +47,60 @@ const focusDrawer = (drawer: HTMLElement | null) => {
firstTabbableItem?.focus();
};

/**
* Custom width breakpoints for the Agama layout.
*
* These values override PatternFly's default breakpoints to better fit Agama's
* needs. Each value is specified in pixels and is derived from rem-based
* measurements, multiplied by the standard root font size (16px).
*
* Breakpoints:
* - sm: 36rem / 576px
* - md: 48rem / 768px
* - lg: 64rem / 1024px
* - xl: 75rem / 1200px
* - 2xl: 90rem / 1440px
*/
const agamaWidthBreakpoints = {
sm: parseInt("36rem") * 16,
md: parseInt("48rem") * 16,
lg: parseInt("64rem") * 16,
xl: parseInt("75rem") * 16,
"2xl": parseInt("90rem") * 16,
};

/**
* Maps a viewport width (in pixels) to the appropriate Agama breakpoint.
*
* This function is used to determine the responsive breakpoint level
* based on the current viewport width, aligning with the custom
* `agamaWidthBreakpoints`.
*
* @param width - The current viewport width in pixels.
* @returns A breakpoint string: 'default', 'sm', 'md', 'lg', 'xl', or '2xl'.
*/
const getBreakpoint = (width: number): "default" | "sm" | "md" | "lg" | "xl" | "2xl" => {
if (width === null) {
return null;
}
if (width >= agamaWidthBreakpoints["2xl"]) {
return "2xl";
}
if (width >= agamaWidthBreakpoints.xl) {
return "xl";
}
if (width >= agamaWidthBreakpoints.lg) {
return "lg";
}
if (width >= agamaWidthBreakpoints.md) {
return "md";
}
if (width >= agamaWidthBreakpoints.sm) {
return "sm";
}
return "default";
};

/**
* Component for laying out the application content inside a PF/Page that might
* or might not mount a header and a sidebar depending on the given props.
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/layout/Loading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { PlainLayout } from "~/components/layout";
import { LayoutProps } from "~/components/layout/Layout";
import sizingStyles from "@patternfly/react-styles/css/utilities/Sizing/sizing";
import spacingStyles from "@patternfly/react-styles/css/utilities/Spacing/spacing";
import { isEmpty } from "~/utils";
import { isEmpty } from "radashi";
import { _ } from "~/i18n";

/**
Expand Down
4 changes: 2 additions & 2 deletions web/src/components/network/WifiConnectionForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ import {
import { Page, PasswordInput } from "~/components/core";
import { useAddConnectionMutation, useConnectionMutation, useConnections } from "~/queries/network";
import { Connection, ConnectionState, WifiNetwork, Wireless } from "~/types/network";
import { _ } from "~/i18n";
import { isEmpty } from "radashi";
import { sprintf } from "sprintf-js";
import { isEmpty } from "~/utils";
import { _ } from "~/i18n";

const securityOptions = [
// TRANSLATORS: WiFi authentication mode
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/network/WifiNetworksList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import Icon, { IconProps } from "~/components/layout/Icon";
import { Connection, ConnectionState, WifiNetwork, WifiNetworkStatus } from "~/types/network";
import { useConnections, useNetworkChanges, useWifiNetworks } from "~/queries/network";
import { NETWORK as PATHS } from "~/routes/paths";
import { isEmpty } from "~/utils";
import { isEmpty } from "radashi";
import { formatIp } from "~/utils/network";
import { sprintf } from "sprintf-js";
import { _ } from "~/i18n";
Expand Down
4 changes: 2 additions & 2 deletions web/src/components/overview/SoftwareSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import React from "react";
import { Content, List, ListItem } from "@patternfly/react-core";
import { SelectedBy } from "~/types/software";
import { usePatterns, useProposal, useProposalChanges } from "~/queries/software";
import { isObjectEmpty } from "~/utils";
import { isEmpty } from "radashi";
import { _ } from "~/i18n";

export default function SoftwareSection(): React.ReactNode {
Expand All @@ -33,7 +33,7 @@ export default function SoftwareSection(): React.ReactNode {

useProposalChanges();

if (isObjectEmpty(proposal.patterns)) return;
if (isEmpty(proposal.patterns)) return;

const TextWithoutList = () => {
return (
Expand Down
7 changes: 4 additions & 3 deletions web/src/components/product/ProductRegistrationPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,16 @@ import {
TextInput,
} from "@patternfly/react-core";
import { Link, Page } from "~/components/core";
import RegistrationExtension from "./RegistrationExtension";
import RegistrationCodeInput from "./RegistrationCodeInput";
import { RegistrationInfo } from "~/types/software";
import { HOSTNAME } from "~/routes/paths";
import { useProduct, useRegistration, useRegisterMutation, useAddons } from "~/queries/software";
import { useHostname } from "~/queries/system";
import { isEmpty, mask } from "~/utils";
import { isEmpty } from "radashi";
import { mask } from "~/utils";
import { sprintf } from "sprintf-js";
import { _ } from "~/i18n";
import RegistrationExtension from "./RegistrationExtension";
import RegistrationCodeInput from "./RegistrationCodeInput";

const FORM_ID = "productRegistration";
const EMAIL_LABEL = "Email";
Expand Down
6 changes: 3 additions & 3 deletions web/src/components/product/ProductSelectionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ import { Page } from "~/components/core";
import { useConfigMutation, useProduct, useRegistration } from "~/queries/software";
import pfTextStyles from "@patternfly/react-styles/css/utilities/Text/text";
import pfRadioStyles from "@patternfly/react-styles/css/components/Radio/radio";
import { sprintf } from "sprintf-js";
import { _ } from "~/i18n";
import { PATHS } from "~/router";
import { isEmpty } from "~/utils";
import { Product } from "~/types/software";
import { isEmpty } from "radashi";
import { sprintf } from "sprintf-js";
import { _ } from "~/i18n";
import LicenseDialog from "./LicenseDialog";

const ResponsiveGridItem = ({ children }) => (
Expand Down
4 changes: 2 additions & 2 deletions web/src/components/questions/QuestionActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
*/

import React from "react";
import { partition } from "~/utils";
import { Popup } from "~/components/core";
import { fork } from "radashi";

/**
* Returns given text capitalized
Expand Down Expand Up @@ -56,7 +56,7 @@ export default function QuestionActions({
actionCallback: (action: string) => void;
conditions?: { disable?: { [key: string]: boolean } };
}): React.ReactNode {
let [[primaryAction], secondaryActions] = partition(actions, (a: string) => a === defaultAction);
let [[primaryAction], secondaryActions] = fork(actions, (a: string) => a === defaultAction);

// Ensure there is always a primary action
if (!primaryAction) [primaryAction, ...secondaryActions] = secondaryActions;
Expand Down
4 changes: 2 additions & 2 deletions web/src/components/software/SoftwareConflicts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import { Icon } from "~/components/layout";
import { Page, SubtleContent } from "~/components/core";
import { ConflictSolutionOption } from "~/types/software";
import { useConflicts, useConflictsChanges, useConflictsMutation } from "~/queries/software";
import { isEmpty } from "~/utils";
import { isNullish } from "radashi";
import { sprintf } from "sprintf-js";
import { _ } from "~/i18n";

Expand Down Expand Up @@ -131,7 +131,7 @@ const ConflictsForm = ({ conflict }): React.ReactNode => {
const onSubmit = async (e) => {
e.preventDefault();

if (!isEmpty(chosenSolution)) {
if (!isNullish(chosenSolution)) {
setError(null);
solve({ conflictId: conflict.id, solutionId: chosenSolution });
} else {
Expand Down
7 changes: 4 additions & 3 deletions web/src/components/storage/DevicesManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@

// @ts-check

import { compact, uniq } from "~/utils";
import { unique } from "radashi";
import { compact } from "~/utils";

/**
* @typedef {import ("~/types/storage").Action} Action
Expand Down Expand Up @@ -158,7 +159,7 @@ export default class DevicesManager {
.filter((d) => this.#isUsed(d) || knownNames.includes(d.name))
.map((d) => d.sid);

return compact(uniq(sids).map((sid) => this.stagingDevice(sid)));
return compact(unique(sids).map((sid) => this.stagingDevice(sid)));
}

/**
Expand Down Expand Up @@ -236,7 +237,7 @@ export default class DevicesManager {
* @returns {boolean}
*/
#isUsed(device) {
const sids = uniq(compact(this.actions.map((a) => a.device)));
const sids = unique(compact(this.actions.map((a) => a.device)));

const partitions = device.partitionTable?.partitions || [];
const lvmLvs = device.logicalVolumes || [];
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/storage/EncryptionSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { NestedContent, Page, PasswordAndConfirmationInput } from "~/components/
import { useEncryptionMethods } from "~/queries/storage";
import { useEncryption } from "~/queries/storage/config-model";
import { apiModel } from "~/api/storage/types";
import { isEmpty } from "~/utils";
import { isEmpty } from "radashi";
import { _ } from "~/i18n";

/**
Expand Down
Loading