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

Very rough reorganization of the storage UI #1747

Closed
Closed
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: 14 additions & 0 deletions web/src/agama.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,20 @@ agama.ngettext = function ngettext(str1, strN, n) {
return n === 1 ? str1 : strN;
};

/**
* Wrapper around Intl.ListFormat to get a language-specific representation of the given list of
* strings.
*
* @param {string[]} list iterable list of strings to represent
* @param {object} options passed to the Intl.ListFormat constructor
* @return {string} concatenation of the original strings with the correct language-specific
* separators according to the currently selected language for the Agama UI
*/
agama.formatList = function formatList(list, options) {
const formatter = new Intl.ListFormat(agama.language, options);
return formatter.format(list);
};

// register a global object so it can be accessed from a separate po.js script
window.agama = agama;

Expand Down
187 changes: 187 additions & 0 deletions web/src/components/storage/ConfigEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* 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 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 React, {useState} from "react";
import { _ } from "~/i18n";
import { sprintf } from "sprintf-js";
import { useDevices, useConfigDevices } from "~/queries/storage";
import { config as type } from "~/api/storage/types";
import { StorageDevice } from "~/types/storage";
import { deviceSize, SPACE_POLICIES } from "~/components/storage/utils";
import * as driveUI from "~/components/storage/utils/drive";
import { typeDescription, contentDescription } from "~/components/storage/utils/device";
import {
Button,
DescriptionList,
DescriptionListGroup,
DescriptionListTerm,
DescriptionListDescription,
List,
ListItem,
Label,
Stack,
StackItem,
Split,
SplitItem,
MenuToggle,
Dropdown,
DropdownList,
DropdownItem
} from "@patternfly/react-core";
import { generate as generateDevices } from "~/storage/model/config";

type DriveEditorProps = { drive: type.DriveElement, driveDevice: StorageDevice };
type PartitionsProps = { drive: type.DriveElement };

function Partitions({ drive }: PartitionsProps) {
return driveUI.contentDescription(drive);
};

function DriveEditor({ drive, driveDevice }: DriveEditorProps) {
const DriveHeader = () => {
// TRANSLATORS: Header a so-called drive at the storage configuration. %s is the drive identifier
// like 'vdb' or any alias set by the user
const text = sprintf(_("Disk %s"), driveUI.label(drive));

return <h4>{text}</h4>;
};

// FIXME: do this i18n friendly, responsive and all that
const DeviceDescription = () => {
const data = [
driveDevice.name,
deviceSize(driveDevice.size),
typeDescription(driveDevice),
driveDevice.model
];
const usefulData = [...new Set(data)].filter((d) => d && d !== "");

return <span>{usefulData.join(" ")}</span>;
};

const ContentDescription = () => {
const content = contentDescription(driveDevice);

return content && <span>{content}</span>;
// <FilesystemLabel item={driveDevice} />
};

const SpacePolicy = () => {
const currentPolicy = driveUI.spacePolicyEntry(drive);
const [isOpen, setIsOpen] = useState(false);
const onToggleClick = () => {
setIsOpen(!isOpen);
};

const PolicyItem = ({policy}) => {
return (
<DropdownItem
isSelected={policy.id === currentPolicy.id}
description={policy.description}
>
{policy.label}
</DropdownItem>
);
};

return (
<span>
{driveUI.oldContentActionsDescription(drive)}
<Dropdown
shouldFocusToggleOnSelect
isOpen={isOpen}
onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)}
toggle={(toggleRef: React.Ref<MenuToggleElemet>) => (
<MenuToggle
ref={toggleRef}
onClick={onToggleClick}
isExpanded={isOpen}
variant="plain"
>
{_("Change")}
</MenuToggle>
)}
>
<DropdownList>
{SPACE_POLICIES.map((policy) => <PolicyItem policy={policy} />)}
</DropdownList>
</Dropdown>
</span>
);
};

return (
<ListItem>
<Stack>
<StackItem>
<DriveHeader />
</StackItem>
<StackItem>
<DescriptionList isHorizontal isCompact horizontalTermWidthModifier={{ default: '14ch'}}>
<DescriptionListGroup>
<DescriptionListTerm>{_("Device")}</DescriptionListTerm>
<DescriptionListDescription>
<DeviceDescription />
<Button variant="link">Change device</Button>
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{_("Current Content")}</DescriptionListTerm>
<DescriptionListDescription>
<Stack>
<StackItem>
<ContentDescription />
{driveDevice.systems.map((s) => <Label isCompact>{s}</Label>)}
</StackItem>
<StackItem>
<SpacePolicy />
</StackItem>
</Stack>
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>{_("New content")}</DescriptionListTerm>
<DescriptionListDescription>
<Partitions drive={drive} />
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
</StackItem>
</Stack>
</ListItem>
);
};

export default function ConfigEditor() {
const drives = useConfigDevices();
const devices = useDevices("system", { suspense: true });

return (
<List isPlain isBordered>
{drives.map((drive, i) => {
const device = devices.find((d) => d.name === drive.name);

return <DriveEditor key={i} drive={drive} driveDevice={device} />
})}
</List>
);
}
52 changes: 3 additions & 49 deletions web/src/components/storage/DeviceSelectorTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,36 +46,7 @@ const DeviceInfo = ({ item }: { item: PartitionSlot | StorageDevice }) => {
if (!device) return null;

const DeviceType = () => {
let type: string;

switch (device.type) {
case "multipath": {
// TRANSLATORS: multipath device type
type = _("Multipath");
break;
}
case "dasd": {
// TRANSLATORS: %s is replaced by the device bus ID
type = sprintf(_("DASD %s"), device.busId);
break;
}
case "md": {
// TRANSLATORS: software RAID device, %s is replaced by the RAID level, e.g. RAID-1
type = sprintf(_("Software %s"), device.level.toUpperCase());
break;
}
case "disk": {
if (device.sdCard) {
type = _("SD Card");
} else {
const technology = device.transport || device.bus;
type = technology
? // TRANSLATORS: %s is substituted by the type of disk like "iSCSI" or "SATA"
sprintf(_("%s disk"), technology)
: _("Disk");
}
}
}
const type = typeDescription(device);

return type && <div>{type}</div>;
};
Expand Down Expand Up @@ -133,27 +104,10 @@ const DeviceExtendedDetails = ({ item }: { item: PartitionSlot | StorageDevice }

if (!device || ["partition", "lvmLv"].includes(device.type)) return <DeviceDetails item={item} />;

// TODO: there is a lot of room for improvement here, but first we would need
// device.description (comes from YaST) to be way more granular
const Description = () => {
if (device.partitionTable) {
const type = device.partitionTable.type.toUpperCase();
const numPartitions = device.partitionTable.partitions.length;

// TRANSLATORS: disk partition info, %s is replaced by partition table
// type (MS-DOS or GPT), %d is the number of the partitions
return sprintf(_("%s with %d partitions"), type, numPartitions);
}

if (!!device.model && device.model === device.description) {
// TRANSLATORS: status message, no existing content was found on the disk,
// i.e. the disk is completely empty
return _("No content found");
}

return (
<div>
{device.description} <FilesystemLabel item={device} />
{contentDescription(device)} <FilesystemLabel item={device} />
</div>
);
};
Expand All @@ -164,7 +118,7 @@ const DeviceExtendedDetails = ({ item }: { item: PartitionSlot | StorageDevice }
const System = ({ system }) => {
const isWindows = /windows/i.test(system);

if (isWindows) return;
if (isWindows) return <div>{system}</div>;

return (
<div>
Expand Down
49 changes: 23 additions & 26 deletions web/src/components/storage/ProposalPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import React, { useRef } from "react";
import { Grid, GridItem, Stack } from "@patternfly/react-core";
import { Page, Drawer } from "~/components/core/";
import ProposalResultSection from "./ProposalResultSection";
import ConfigEditor from "./ConfigEditor";
import ProposalActionsSummary from "~/components/storage/ProposalActionsSummary";
import { ProposalActionsDialog } from "~/components/storage";
import EncryptionField from "~/components/storage/EncryptionField"
import { _ } from "~/i18n";
import { toValidationError } from "~/utils";
import { useIssues } from "~/queries/issues";
Expand Down Expand Up @@ -86,32 +87,28 @@ export default function ProposalPage() {

<Page.Content>
<Grid hasGutter>
<GridItem sm={12}>
<Drawer
ref={drawerRef}
panelHeader={<h4>{_("Planned Actions")}</h4>}
panelContent={<ProposalActionsDialog actions={actions} />}
<GridItem sm={12} xl={8}>
<Page.Section
title={_("Installation Devices")}
description={_("Structure of the new system, including disks to use and additional devices like LVM volume groups.")}
>
<Stack hasGutter>
<ProposalActionsSummary
system={systemDevices}
staging={stagingDevices}
errors={errors}
actions={actions}
// @ts-expect-error: we do not know how to specify the type of
// drawerRef properly and TS does not find the "open" property
onActionsClick={drawerRef.current?.open}
isLoading={false}
/>
<ProposalResultSection
system={systemDevices}
staging={stagingDevices}
actions={actions}
errors={errors}
isLoading={false}
/>
</Stack>
</Drawer>
<ConfigEditor />
</Page.Section>
</GridItem>
<GridItem sm={12} xl={4}>
<EncryptionField
password={""}
isLoading={false}
/>
</GridItem>
<GridItem sm={12}>
<ProposalResultSection
system={systemDevices}
staging={stagingDevices}
actions={actions}
errors={errors}
isLoading={false}
/>
</GridItem>
</Grid>
</Page.Content>
Expand Down
Loading
Loading