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

web: Improve find space #1128

Merged
6 changes: 6 additions & 0 deletions web/package/cockpit-agama.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-------------------------------------------------------------------
Mon Apr 8 14:17:45 UTC 2024 - José Iván López González <jlopez@suse.com>

- Improve the representation of the devices in the dialog for
finding space (gh#openSUSE/agama#1128).

-------------------------------------------------------------------
Thu Apr 4 15:59:37 UTC 2024 - Ancor Gonzalez Sosa <ancor@suse.com>

Expand Down
9 changes: 5 additions & 4 deletions web/src/assets/styles/blocks.scss
Original file line number Diff line number Diff line change
Expand Up @@ -526,9 +526,7 @@ table[data-type="agama/tree-table"] {
}
}

// FIXME: use a single selector
table.devices-table,
table.proposal-result {
table.devices-table {
tr.dimmed-row {
background-color: #fff;
opacity: 0.8;
Expand All @@ -538,8 +536,11 @@ table.proposal-result {
color: var(--color-gray-dimmed);
padding-block: 0;
}

}
}

table.proposal-result {
@extend .devices-table;

/**
* Temporary hack because the collapse/expand callback was not given to the
Expand Down
12 changes: 9 additions & 3 deletions web/src/components/core/OptionsPicker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@
* find current contact information at www.suse.com.
*/

// @ts-check

import React from "react";

import { noop } from "~/utils";

/**
* Wrapper for OptionsPicker options
* @component
Expand All @@ -29,12 +33,14 @@ import React from "react";
* @param {string} [props.title] - Text to be used as option title
* @param {string} [props.body] - Text to be used as option body
* @param {boolean} [props.isSelected=false] - Whether the option should be set as select of not
* @param {() => void} [props.onClick=noop]
* @param {object} [props.props] - Other props sent to div#option node
*/
const Option = ({ title, body, isSelected = false, ...props }) => {
const Option = ({ title, body, isSelected = false, onClick = noop, ...props }) => {
return (
<div
{...props}
onClick={onClick}
role="option"
aria-selected={isSelected}
>
Expand All @@ -50,10 +56,10 @@ const Option = ({ title, body, isSelected = false, ...props }) => {
*
* @param {object} props
* @param {string} [props.ariaLabel] - Text to be used as accessible label
* @param {Array<Option>} props.children - A collection of Option
* @param {React.ReactNode} props.children - A collection of Option
* @param {object} [props.props] - Other props sent to div#listbox node
*/
const OptionsPicker = ({ "aria-label": ariaLabel, children, ...props }) => {
const OptionsPicker = ({ ariaLabel, children, ...props }) => {
return (
<div
{...props}
Expand Down
25 changes: 24 additions & 1 deletion web/src/components/core/Page.test.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2023] SUSE LLC
* Copyright (c) [2023-2024] SUSE LLC
*
* All Rights Reserved.
*
Expand All @@ -23,12 +23,35 @@ import React from "react";
import { screen, within } from "@testing-library/react";
import { installerRender, plainRender, mockNavigateFn } from "~/test-utils";
import { Page } from "~/components/core";
import { createClient } from "~/client";

jest.mock("~/client");

const l10nClientMock = {
getUILocale: jest.fn().mockResolvedValue("en_US"),
keymaps: jest.fn().mockResolvedValue([]),
getKeymap: jest.fn().mockResolvedValue(undefined),
timezones: jest.fn().mockResolvedValue([]),
getTimezone: jest.fn().mockResolvedValue(undefined),
onLocalesChange: jest.fn(),
onKeymapChange: jest.fn(),
onTimezoneChange: jest.fn()
};

describe("Page", () => {
beforeAll(() => {
jest.spyOn(console, "error").mockImplementation();
});

beforeEach(() => {
// if defined outside, the mock is cleared automatically
createClient.mockImplementation(() => {
return {
l10n: l10nClientMock
};
});
});

afterAll(() => {
console.error.mockRestore();
});
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/core/PasswordAndConfirmationInput.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { FormGroup } from "@patternfly/react-core";
import { FormValidationError, PasswordInput } from "~/components/core";
import { _ } from "~/i18n";

const PasswordAndConfirmationInput = ({ value, onChange, onValidation, isDisabled }) => {
const PasswordAndConfirmationInput = ({ value, onChange, onValidation, isDisabled = false }) => {
const [confirmation, setConfirmation] = useState(value || "");
const [error, setError] = useState("");

Expand Down
63 changes: 29 additions & 34 deletions web/src/components/core/Popup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,19 @@
* find current contact information at www.suse.com.
*/

// @ts-check

import React from "react";
import { Button, Modal } from "@patternfly/react-core";

import { _ } from "~/i18n";
import { partition } from "~/utils";

/**
* @typedef {import("@patternfly/react-core").ModalProps} ModalProps
* @typedef {import("@patternfly/react-core").ButtonProps} ButtonProps
* @typedef {Omit<ButtonProps, 'variant'>} ButtonWithoutVariantProps
*/

/**
* Wrapper component for holding Popup actions
*
Expand All @@ -33,6 +40,7 @@ import { partition } from "~/utils";
*
* @see Popup examples.
*
* @param {object} props
* @param {React.ReactNode} [props.children] - a collection of Action components
*/
const Actions = ({ children }) => <>{children}</>;
Expand All @@ -42,13 +50,10 @@ const Actions = ({ children }) => <>{children}</>;
*
* Built on top of {@link https://www.patternfly.org/components/button PF/Button}
*
* @see Popup examples.
*
* @param {React.ReactNode} props.children - content of the action
* @param {object} [props] - PF/Button props, see {@link https://www.patternfly.org/components/button#props}
* @param {ButtonProps} props
*/
const Action = ({ children, ...props }) => (
<Button { ...props }>
const Action = ({ children, ...buttonProps }) => (
<Button { ...buttonProps }>
{children}
</Button>
);
Expand All @@ -68,11 +73,10 @@ const Action = ({ children, ...props }) => (
* <Text>Upload</Text>
* </PrimaryAction>
*
* @param {React.ReactNode} props.children - content of the action
* @param {object} [props] - {@link Action} props
* @param {ButtonWithoutVariantProps} props
*/
const PrimaryAction = ({ children, ...props }) => (
<Action { ...props } variant="primary">{ children }</Action>
const PrimaryAction = ({ children, ...actionProps }) => (
<Action { ...actionProps } variant="primary">{ children }</Action>
);

/**
Expand All @@ -84,11 +88,10 @@ const PrimaryAction = ({ children, ...props }) => (
* @example <caption>Using it with a custom text</caption>
* <Confirm onClick={accept}>Accept</Confirm>
*
* @param {React.ReactNode} [props.children="confirm"] - content of the action
* @param {object} [props] - {@link Action} props
* @param {ButtonWithoutVariantProps} props
*/
const Confirm = ({ children = _("Confirm"), ...props }) => (
<PrimaryAction key="confirm" { ...props }>{ children }</PrimaryAction>
const Confirm = ({ children = _("Confirm"), ...actionProps }) => (
<PrimaryAction key="confirm" { ...actionProps }>{ children }</PrimaryAction>
);

/**
Expand All @@ -106,11 +109,10 @@ const Confirm = ({ children = _("Confirm"), ...props }) => (
* <Text>Dismiss</Text>
* </SecondaryAction>
*
* @param {React.ReactNode} props.children - content of the action
* @param {object} [props] - {@link Action} props
* @param {ButtonWithoutVariantProps} props
*/
const SecondaryAction = ({ children, ...props }) => (
<Action { ...props } variant="secondary">{ children }</Action>
const SecondaryAction = ({ children, ...actionProps }) => (
<Action { ...actionProps } variant="secondary">{ children }</Action>
);

/**
Expand All @@ -122,11 +124,10 @@ const SecondaryAction = ({ children, ...props }) => (
* @example <caption>Using it with a custom text</caption>
* <Cancel onClick={dismiss}>Dismiss</Confirm>
*
* @param {React.ReactNode} [props.children="Cancel"] - content of the action
* @param {object} [props] - {@link Action} props
* @param {ButtonWithoutVariantProps} props
*/
const Cancel = ({ children = _("Cancel"), ...props }) => (
<SecondaryAction key="cancel" { ...props }>{ children }</SecondaryAction>
const Cancel = ({ children = _("Cancel"), ...actionProps }) => (
<SecondaryAction key="cancel" { ...actionProps }>{ children }</SecondaryAction>
);

/**
Expand All @@ -144,11 +145,10 @@ const Cancel = ({ children = _("Cancel"), ...props }) => (
* <Text>Do not set</Text>
* </AncillaryAction>
*
* @param {React.ReactNode} props.children - content of the action
* @param {object} [props] - {@link Action} props
* @param {ButtonWithoutVariantProps} props
*/
const AncillaryAction = ({ children, ...props }) => (
<Action { ...props } variant="link">{ children }</Action>
const AncillaryAction = ({ children, ...actionsProps }) => (
<Action { ...actionsProps } variant="link">{ children }</Action>
);

/**
Expand Down Expand Up @@ -187,13 +187,7 @@ const AncillaryAction = ({ children, ...props }) => (
* </Popup.Actions>
* </Popup>
*
* @param {object} props - component props
* @param {boolean} [props.isOpen=false] - whether the popup is displayed or not
* @param {boolean} [props.showClose=false] - whether the popup should include a "X" action for closing it
* @param {string} [props.variant="small"] - the popup size, based on PF/Modal `variant` prop
* @param {React.ReactNode} props.children - the popup content and actions
* @param {object} [pfModalProps] - PF/Modal props, See {@link https://www.patternfly.org/components/modal#props}
*
* @param {ModalProps} props
*/
const Popup = ({
isOpen = false,
Expand All @@ -205,6 +199,7 @@ const Popup = ({
const [actions, content] = partition(React.Children.toArray(children), child => child.type === Actions);

return (
/** @ts-ignore */
<Modal
{ ...pfModalProps }
isOpen={isOpen}
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/core/Section.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import { If, ValidationErrors } from "~/components/core";
* @property {string} [className] - Class name for section html tag.
* @property {string} [id] - Id of the section ("software", "product", "storage", "storage-actions", ...)
* @property {import("~/client/mixins").ValidationError[]} [props.errors] - Validation errors to be shown before the title.
* @property {React.ReactElement} [children] - The section content.
* @property {React.ReactNode} [children] - The section content.
* @property {string} [aria-label] - aria-label attribute, required if title if not given
*
* @param { SectionProps } props
Expand Down
29 changes: 25 additions & 4 deletions web/src/components/core/TreeTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

// @ts-check

import React from "react";
import React, { useEffect, useState } from "react";
import { Table, Thead, Tr, Th, Tbody, Td, TreeRowWrapper } from '@patternfly/react-table';

/**
Expand All @@ -39,6 +39,7 @@ import { Table, Thead, Tr, Th, Tbody, Td, TreeRowWrapper } from '@patternfly/rea
* @typedef {object} TreeTableBaseProps
* @property {TreeTableColumn[]} columns=[]
* @property {object[]} items=[]
* @property {object[]} [expandedItems=[]]
* @property {(any) => array} [itemChildren]
* @property {(any) => string} [rowClassNames]
*/
Expand All @@ -58,9 +59,26 @@ export default function TreeTable({
columns = [],
items = [],
itemChildren = () => [],
expandedItems = [],
rowClassNames = () => "",
...tableProps
}) {
const [expanded, setExpanded] = useState(expandedItems);

useEffect(() => {
setExpanded(expandedItems);
}, [expandedItems, setExpanded]);

const isExpanded = (item) => expanded.includes(item);

const toggle = (item) => {
if (isExpanded(item)) {
setExpanded(expanded.filter(d => d !== item));
} else {
setExpanded([...expanded, item]);
}
};

const renderColumns = (item, treeRow) => {
return columns.map((c, cIdx) => {
const props = {
Expand All @@ -76,17 +94,20 @@ export default function TreeTable({
});
};

const renderRows = (items, level) => {
const renderRows = (items, level, hidden = false) => {
if (items?.length <= 0) return;

return (
items.map((item, itemIdx) => {
const children = itemChildren(item);
const expanded = isExpanded(item);

const treeRow = {
onCollapse: () => toggle(item),
props: {
isExpanded: true,
isExpanded: expanded,
isDetailsExpanded: true,
isHidden: hidden,
"aria-level": level,
"aria-posinset": itemIdx + 1,
"aria-setsize": children?.length || 0
Expand All @@ -101,7 +122,7 @@ export default function TreeTable({
return (
<React.Fragment key={itemIdx}>
<TreeRowWrapper {...rowProps}>{renderColumns(item, treeRow)}</TreeRowWrapper>
{ renderRows(children, level + 1)}
{ renderRows(children, level + 1, !expanded)}
</React.Fragment>
);
})
Expand Down
2 changes: 2 additions & 0 deletions web/src/components/storage/BootSelectionDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { Popup } from "~/components/core";
import { deviceLabel } from "~/components/storage/utils";
import { sprintf } from "sprintf-js";

// @ts-check

/**
* @typedef {import ("~/client/storage").StorageDevice} StorageDevice
*/
Expand Down
4 changes: 3 additions & 1 deletion web/src/components/storage/DeviceSelectionDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
* find current contact information at www.suse.com.
*/

// @ts-check

import React, { useState } from "react";
import { Form } from "@patternfly/react-core";

Expand Down Expand Up @@ -53,7 +55,7 @@ const Html = ({ children, ...props }) => (
* @param {StorageDevice[]} props.targetPVDevices
* @param {StorageDevice[]} props.devices - The actions to perform in the system.
* @param {boolean} [props.isOpen=false] - Whether the dialog is visible or not.
* @param {function} [props.onCancel=noop]
* @param {() => void} [props.onCancel=noop]
* @param {(target: Target) => void} [props.onAccept=noop]
*
* @typedef {object} Target
Expand Down
Loading
Loading