Skip to content

Commit

Permalink
web: WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
joseivanlopez committed Apr 8, 2024
1 parent 7b75c3f commit d26f7de
Show file tree
Hide file tree
Showing 9 changed files with 650 additions and 318 deletions.
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
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
4 changes: 3 additions & 1 deletion web/src/components/storage/ProposalResultSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ const DevicesTreeTable = ({ devicesManager }) => {
};

const renderMountPoint = (item) => item.sid && <em>{item.filesystem?.mountPath}</em>;
const devices = devicesManager.usedDevices();

return (
<TreeTable
Expand All @@ -189,7 +190,8 @@ const DevicesTreeTable = ({ devicesManager }) => {
{ title: _("Details"), content: renderDetails, classNames: "details-column" },
{ title: _("Size"), content: renderSize, classNames: "sizes-column" }
]}
items={devicesManager.usedDevices()}
items={devices}
expandedItems={devices}
itemChildren={d => deviceChildren(d)}
rowClassNames={(item) => {
if (!item.sid) return "dimmed-row";
Expand Down
8 changes: 4 additions & 4 deletions web/src/components/storage/ProposalSettingsSection.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ const BootConfigField = ({
* @component
*
* @param {object} props
* @param {SpacePolicy} props.policy
* @param {SpacePolicy|undefined} props.policy
* @param {SpaceAction[]} props.actions
* @param {StorageDevice[]} props.devices
* @param {boolean} props.isLoading
Expand Down Expand Up @@ -378,7 +378,7 @@ const SpacePolicyField = ({
return sprintf(n_(policy.summaryLabels[0], policy.summaryLabels[1], devices.length), devices.length);
};

if (isLoading) {
if (isLoading || !policy) {
return <Skeleton screenreaderText={_("Waiting for information about space policy")} width="25%" />;
}

Expand Down Expand Up @@ -460,7 +460,7 @@ export default function ProposalSettingsSection({

const lvm = settings.target === "newLvmVg" || settings.target === "reusedLvmVg";
const encryption = settings.encryptionPassword !== undefined && settings.encryptionPassword.length > 0;
const { volumes = [], installationDevices = [] } = settings;
const { volumes = [], installationDevices = [], spaceActions = [] } = settings;
const bootDevice = availableDevices.find(d => d.name === settings.bootDevice);
const defaultBootDevice = availableDevices.find(d => d.name === settings.defaultBootDevice);
const spacePolicy = SPACE_POLICIES.find(p => p.id === settings.spacePolicy);
Expand Down Expand Up @@ -505,7 +505,7 @@ export default function ProposalSettingsSection({
/>
<SpacePolicyField
policy={spacePolicy}
actions={settings.spaceActions}
actions={spaceActions}
devices={installationDevices}
isLoading={isLoading}
onChange={changeSpacePolicy}
Expand Down
41 changes: 41 additions & 0 deletions web/src/components/storage/ProposalSettingsSection.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,44 @@ describe("Encryption field", () => {
});
});
});

describe("Space policy field", () => {
describe("if there is no space policy", () => {
beforeEach(() => {
props.settings = {};
});

it("does not render the space policy field", () => {
plainRender(<ProposalSettingsSection {...props} />);

expect(screen.queryByLabelText("Find space")).toBeNull();
});
});

describe("if there is a space policy", () => {
beforeEach(() => {
props.settings = {
spacePolicy: "delete"
};
});

it("renders the button with a text according to given policy", () => {
const { rerender } = plainRender(<ProposalSettingsSection {...props} />);
screen.getByRole("button", { name: /deleting/ });
rerender(<ProposalSettingsSection settings={{ spacePolicy: "resize" }} />);
screen.getByRole("button", { name: /shrinking/ });
});

it("allows to change the policy", async () => {
const { user } = plainRender(<ProposalSettingsSection {...props} />);
const button = screen.getByRole("button", { name: /deleting all content/ });

await user.click(button);

const popup = await screen.findByRole("dialog");
within(popup).getByText("Find space");
const cancel = within(popup).getByRole("button", { name: "Cancel" });
await user.click(cancel);
});
});
});
184 changes: 184 additions & 0 deletions web/src/components/storage/SpaceActionsTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* Copyright (c) [2024] SUSE LLC
*
* All Rights Reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of version 2 of the GNU General Public License as published
* by the Free Software Foundation.
*
* 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.
*/

// @ts-check

import React from "react";
import { FormSelect, FormSelectOption } from "@patternfly/react-core";

import { _ } from "~/i18n";
import { FilesystemLabel } from "~/components/storage";
import { deviceChildren, deviceSize } from '~/components/storage/utils';
import { If, Tag, TreeTable } from "~/components/core";
import { sprintf } from "sprintf-js";

/**
* @typedef {import ("~/client/storage").SpaceAction} SpaceAction
* @typedef {import ("~/client/storage").StorageDevice} StorageDevice
*/

/**
* Column content.
* @component
*
* @param {object} props
* @param {StorageDevice} props.device
*/
const DeviceName = ({ device }) => {
let name = device.sid && device.name;
// NOTE: returning a fragment here to avoid a weird React complaint when using a PF/Table +
// treeRow props.
if (!name) return <></>;

if (["partition"].includes(device.type))
name = name.split("/").pop();

return (
<span>{name}</span>
);
};

/**
* Column content.
* @component
*
* @param {object} props
* @param {StorageDevice} props.device
*/
const DeviceDetails = ({ device }) => {
if (!device.sid) return _("Unused space");

const renderContent = (device) => {
if (!device.partitionTable && device.systems?.length > 0)
return device.systems.join(", ");

return device.description;
};

const renderPTableType = (device) => {
const type = device.partitionTable?.type;
if (type) return <Tag><b>{type.toUpperCase()}</b></Tag>;
};

return (
<div>{renderContent(device)} <FilesystemLabel device={device} /> {renderPTableType(device)}</div>
);
};

/**
* Column content.
* @component
*
* @param {object} props
* @param {StorageDevice} props.device
*/
const DeviceSizeDetails = ({ device }) => {
if (!device.sid || device.isDrive || device.recoverableSize === 0) return null;

return deviceSize(device.recoverableSize);
};

/**
* Column content with the space action for a device.
* @component
*
* @param {object} props
* @param {StorageDevice} props.device
* @param {string} props.action - Possible values: "force_delete", "resize" or "keep".
* @param {boolean} [props.isDisabled=false]
* @param {(action: SpaceAction) => void} [props.onChange]
*/
const DeviceAction = ({ device, action, isDisabled = false, onChange }) => {
if (!device.sid || device.partitionTable) return null;

const changeAction = (_, action) => onChange({ device: device.name, action });

return (
<FormSelect
value={action}
isDisabled={isDisabled}
onChange={changeAction}
aria-label={
/* TRANSLATORS: %s is replaced by a device name (e.g., /dev/sda) */
sprintf(_("Space action selector for %s"), device.name)
}
>
<FormSelectOption value="force_delete" label={_("Delete")} />
{/* Resize action does not make sense for drives, so it is filtered out. */}
<If
condition={!device.isDrive}
then={<FormSelectOption value="resize" label={_("Allow resize")} />}
/>
<FormSelectOption value="keep" label={_("Do not modify")} />
</FormSelect>
);
};

/**
* Table for selecting the space actions of the given devices.
* @component
*
* @param {object} props
* @param {StorageDevice[]} props.devices
* @param {StorageDevice[]} [props.expandedDevices=[]] - Initially expanded devices.
* @param {boolean} [props.isActionDisabled=false] - Whether the action selector is disabled.
* @param {(device: StorageDevice) => string} props.deviceAction - Gets the action for a device.
* @param {(action: SpaceAction) => void} props.onActionChange
*/
export default function SpaceActionsTable({
devices,
expandedDevices = [],
isActionDisabled = false,
deviceAction,
onActionChange,
}) {
const columns = [
{ title: _("Device"), content: (device) => <DeviceName device={device} /> },
{ title: _("Details"), content: (device) => <DeviceDetails device={device} /> },
{ title: _("Size"), content: (device) => deviceSize(device.size) },
{ title: _("Shrinkable"), content: (device) => <DeviceSizeDetails device={device} /> },
{
title: _("Action"),
content: (device) => (
<DeviceAction
device={device}
action={deviceAction(device)}
isDisabled={isActionDisabled}
onChange={onActionChange}
/>
)
}
];

return (
<TreeTable
columns={columns}
items={devices}
aria-label={_("Actions to find space")}
expandedItems={expandedDevices}
itemChildren={d => deviceChildren(d)}
rowClassNames={(item) => {
if (!item.sid) return "dimmed-row";
}}
className="devices-table"
/>
);
}
Loading

0 comments on commit d26f7de

Please sign in to comment.