diff --git a/web/src/components/storage/ProposalSpacePolicyField.jsx b/web/src/components/storage/ProposalSpacePolicyField.jsx deleted file mode 100644 index 17455afb4b..0000000000 --- a/web/src/components/storage/ProposalSpacePolicyField.jsx +++ /dev/null @@ -1,554 +0,0 @@ -/* - * 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. - */ - -import React, { useEffect, useState } from "react"; -import { Button, Form, FormSelect, FormSelectOption, Skeleton } from "@patternfly/react-core"; - -import { _, N_, n_ } from "~/i18n"; -import { deviceSize } from '~/components/storage/utils'; -import { If, OptionsPicker, Popup, SectionSkeleton } from "~/components/core"; -import { noop } from "~/utils"; -import { sprintf } from "sprintf-js"; -import { Table, Thead, Tr, Th, Tbody, Td, TreeRowWrapper } from '@patternfly/react-table'; - -/** - * @typedef {import ("~/client/storage").ProposalManager.ProposalSettings} ProposalSettings - * @typedef {import ("~/client/storage").ProposalManager.SpaceAction} SpaceAction - * @typedef {import ("~/client/storage").DevicesManager.StorageDevice} StorageDevice - */ - -/** - * @typedef SpacePolicy - * @type {object} - * @property {string} id - * @property {string} label - * @property {string} description - */ - -/** @type {SpacePolicy[]} */ -const SPACE_POLICIES = [ - { - id: "delete", - label: N_("Delete current content"), - description: N_("All partitions will be removed and any data in the disks will be lost."), - summaryLabels: [ - // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence - // would read as "Find space deleting all content[...]" - N_("deleting all content of the installation device"), - // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence - // would read as "Find space deleting all content[...]" - N_("deleting all content of the %d selected disks") - ] - }, - { - id: "resize", - label: N_("Shrink existing partitions"), - description: N_("The data is kept, but the current partitions will be resized as needed."), - summaryLabels: [ - // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence - // would read as "Find space shrinking partitions[...]" - N_("shrinking partitions of the installation device"), - // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence - // would read as "Find space shrinking partitions[...]" - N_("shrinking partitions of the %d selected disks") - ] - }, - { - id: "keep", - label: N_("Use available space"), - description: N_("The data is kept. Only the space not assigned to any partition will be used."), - summaryLabels: [ - // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence - // would read as "Find space without modifying any partition". - N_("without modifying any partition") - ] - }, - { - id: "custom", - label: N_("Custom"), - description: N_("Select what to do with each partition."), - summaryLabels: [ - // TRANSLATORS: This is presented next to the label "Find space", so the whole sentence - // would read as "Find space performing a custom set of actions". - N_("performing a custom set of actions") - ] - } -]; - -// Names of the columns for the policy actions. -const columnNames = { - device: N_("Used device"), - content: N_("Details"), - size: N_("Size"), - details: N_("Size details"), - action: N_("Action") -}; - -/** - * Column content with the description of a device. - * @component - * - * @param {object} props - * @param {StorageDevice} props.device - */ -const DeviceDescriptionColumn = ({ device }) => { - if (device.isDrive || device.type === "lvmVg") return device.name; - - return device.name.split("/").pop(); -}; - -/** - * Column content with details about the device. - * @component - * - * @param {object} props - * @param {StorageDevice} props.device - */ -const DeviceContentColumn = ({ device }) => { - const systems = device.systems; - if (systems.length > 0) return systems.join(", "); - - return device.description; -}; - -/** - * Column content with information about the size of the device. - * @component - * - * @param {object} props - * @param {StorageDevice} props.device - */ -const DeviceSizeColumn = ({ device }) => { - return deviceSize(device.size); -}; - -/** - * Column content with details about the device. - * @component - * - * @param {object} props - * @param {StorageDevice} props.device - */ -const DeviceDetailsColumn = ({ device }) => { - const UnusedSize = () => { - if (device.filesystem) return null; - - const unused = device.partitionTable?.unpartitionedSize || 0; - // TRANSLATORS: %s is replaced by a disk size (e.g., 20 GiB) - return sprintf(_("%s unused"), deviceSize(unused)); - }; - - const RecoverableSize = () => { - const size = device.recoverableSize; - if (size === 0) return null; - - // TRANSLATORS: %s is replaced by a disk size (e.g., 2 GiB) - return sprintf(_("Shrinkable by %s"), deviceSize(device.recoverableSize)); - }; - - return ( - } else={} /> - ); -}; - -/** - * 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 DeviceActionColumn = ({ device, action, isDisabled = false, onChange = noop }) => { - const changeAction = (_, action) => onChange({ device: device.name, action }); - - // For a drive device (e.g., Disk, RAID) it does not make sense to offer the resize action. - // At this moment, the Agama backend generates a resize action for drives if the policy is set to - // 'resize'. In that cases, the action is converted here to 'keep'. - const value = (device.isDrive && action === "resize") ? "keep" : action; - - return ( - - - {/* Resize action does not make sense for drives, so it is filtered out. */} - } - /> - - - ); -}; - -/** - * Row for configuring the space action of a device. - * @component - * - * @param {object} props - * @param {StorageDevice} props.device - * @param {ProposalSettings} props.settings - * @param {number} props.rowIndex - @see {@link https://www.patternfly.org/components/table/#tree-table} - * @param {number} [props.level=1] - @see {@link https://www.patternfly.org/components/table/#tree-table} - * @param {number} [props.setSize=0] - @see {@link https://www.patternfly.org/components/table/#tree-table} - * @param {number} [props.posInSet=0] - @see {@link https://www.patternfly.org/components/table/#tree-table} - * @param {boolean} [props.isExpanded=false] - @see {@link https://www.patternfly.org/components/table/#tree-table} - * @param {boolean} [props.isHidden=false] - @see {@link https://www.patternfly.org/components/table/#tree-table} - * @param {function} [props.onCollapse] - @see {@link https://www.patternfly.org/components/table/#tree-table} - * @param {(action: SpaceAction) => void} [props.onChange] - */ -const DeviceRow = ({ - device, - policy, - actions, - rowIndex, - level = 1, - setSize = 0, - posInSet = 1, - isExpanded = false, - isHidden = false, - onCollapse = noop, - onChange = noop -}) => { - // Generates the action value according to the policy. - const action = () => { - if (policy.id === "custom") - return actions.find(a => a.device === device.name)?.action || "keep"; - - const policyAction = { delete: "force_delete", resize: "resize", keep: "keep" }; - return policyAction[policy.id]; - }; - - const isDisabled = policy.id !== "custom"; - const showAction = !device.partitionTable; - - const treeRow = { - onCollapse, - rowIndex, - props: { - isExpanded, - isDetailsExpanded: true, - isHidden, - 'aria-level': level, - 'aria-posinset': posInSet, - 'aria-setsize': setSize - } - }; - - return ( - - {/* eslint-disable agama-i18n/string-literals */} - - - - - - - - - } - /> - - {/* eslint-enable agama-i18n/string-literals */} - - ); -}; - -/** - * Table for configuring the space actions. - * @component - * - * @param {object} props - * @param {ProposalSettings} props.settings - * @param {(action: SpaceAction) => void} [props.onChange] - */ -const SpaceActionsTable = ({ policy, actions, devices, onChange = noop }) => { - const [expandedDevices, setExpandedDevices] = useState([]); - const [autoExpanded, setAutoExpanded] = useState(false); - - useEffect(() => { - const devNames = devices.map(d => d.name); - let currentExpanded = devNames.filter(d => expandedDevices.includes(d)); - - if (policy.id === "custom" && !autoExpanded) { - currentExpanded = [...devNames]; - setAutoExpanded(true); - } else if (policy.id !== "custom" && autoExpanded) { - setAutoExpanded(false); - } - - if (currentExpanded.sort().toString() !== expandedDevices.sort().toString()) { - setExpandedDevices(currentExpanded); - } - }, [autoExpanded, expandedDevices, setAutoExpanded, setExpandedDevices, policy, devices]); - - const renderRows = () => { - const rows = []; - - devices?.forEach((device, index) => { - const isExpanded = expandedDevices.includes(device.name); - const children = device.partitionTable?.partitions; - - const onCollapse = () => { - const otherExpandedDevices = expandedDevices.filter(name => name !== device.name); - const expanded = isExpanded ? otherExpandedDevices : [...otherExpandedDevices, device.name]; - setExpandedDevices(expanded); - }; - - rows.push( - - ); - - children?.forEach((child, index) => { - rows.push( - - ); - }); - }); - - return rows; - }; - - return ( - - - - - - - - - - - {renderRows()} -
{columnNames.device}{columnNames.content}{columnNames.size}{columnNames.details}{columnNames.action}
- ); -}; - -/** - * Widget to allow user picking desired policy to make space - * @component - * - * @param {object} props - * @param {SpacePolicy} props.currentPolicy - * @param {(policy: string) => void} [props.onChange] - */ -const SpacePolicyPicker = ({ currentPolicy, onChange = noop }) => { - return ( - - {/* eslint-disable agama-i18n/string-literals */} - {SPACE_POLICIES.map((policy) => { - return ( - onChange(policy)} - isSelected={currentPolicy?.id === policy.id} - /> - ); - })} - {/* eslint-enable agama-i18n/string-literals */} - - ); -}; - -/** - * Section for configuring the space policy. - * @component - * - * @param {ProposalSettings} settings - * @param {boolean} [isLoading=false] - * @param {(settings: ProposalSettings) => void} [onChange] - */ -const SpacePolicyForm = ({ - id, - currentPolicy, - currentActions, - devices, - isLoading = false, - onSubmit = noop -}) => { - const [policy, setPolicy] = useState(currentPolicy); - const [actions, setActions] = useState(currentActions); - const [customUsed, setCustomUsed] = useState(false); - - // The selectors for the space action have to be initialized always to the same value - // (e.g., "keep") when the custom policy is selected for first time. The following two useEffect - // ensures that. - - // Stores whether the custom policy has been used. - useEffect(() => { - if (policy.id === "custom" && !customUsed) setCustomUsed(true); - }, [policy, customUsed, setCustomUsed]); - - // Resets actions (i.e., sets everything to "keep") if the custom policy has not been used yet. - useEffect(() => { - if (policy.id !== "custom" && !customUsed) setActions([]); - }, [policy, customUsed, setActions]); - - const changeActions = (spaceAction) => { - const spaceActions = actions.filter(a => a.device !== spaceAction.device); - if (spaceAction.action !== "keep") spaceActions.push(spaceAction); - - setActions(spaceActions); - }; - - const submitForm = (e) => { - e.preventDefault(); - if (policy !== undefined) onSubmit(policy, actions); - }; - - return ( -
- } - else={ - <> - - 0} - then={ - - } - /> - - } - /> - - ); -}; - -const SpacePolicyButton = ({ policy, devices, onClick = noop }) => { - const Text = () => { - // eslint-disable-next-line agama-i18n/string-literals - if (policy.summaryLabels.length === 1) return _(policy.summaryLabels[0]); - - // eslint-disable-next-line agama-i18n/string-literals - return sprintf(n_(policy.summaryLabels[0], policy.summaryLabels[1], devices.length), devices.length); - }; - - return ; -}; - -export default function ProposalSpacePolicyField({ - policy, - actions = [], - devices = [], - isLoading = false, - onChange = noop -}) { - const spacePolicy = SPACE_POLICIES.find(p => p.id === policy) || SPACE_POLICIES[0]; - const [isFormOpen, setIsFormOpen] = useState(false); - - const openForm = () => setIsFormOpen(true); - const closeForm = () => setIsFormOpen(false); - - const acceptForm = (spacePolicy, actions) => { - closeForm(); - onChange(spacePolicy.id, actions); - }; - - if (isLoading) { - return ; - } - - const description = _("Allocating the file systems might need to find free space \ -in the devices listed below. Choose how to do it."); - - return ( -
- {/* TRANSLATORS: To be completed with the rest of a sentence like "deleting all content" */} - {_("Find space")} - - -
- -
- - - {_("Accept")} - - - -
-
- ); -} diff --git a/web/src/components/storage/ProposalSpacePolicyField.test.jsx b/web/src/components/storage/ProposalSpacePolicyField.test.jsx deleted file mode 100644 index a8b5bc9bc8..0000000000 --- a/web/src/components/storage/ProposalSpacePolicyField.test.jsx +++ /dev/null @@ -1,359 +0,0 @@ -/* - * 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. - */ - -import React from "react"; -import { screen, within } from "@testing-library/react"; -import { plainRender, resetLocalStorage } from "~/test-utils"; -import { ProposalSpacePolicyField } from "~/components/storage"; - -const sda = { - sid: "59", - isDrive: true, - type: "disk", - vendor: "Micron", - model: "Micron 1100 SATA", - driver: ["ahci", "mmcblk"], - bus: "IDE", - busId: "", - transport: "usb", - dellBOSS: false, - sdCard: true, - active: true, - name: "/dev/sda", - size: 1024, - recoverableSize: 0, - systems : [], - udevIds: ["ata-Micron_1100_SATA_512GB_12563", "scsi-0ATA_Micron_1100_SATA_512GB"], - udevPaths: ["pci-0000:00-12", "pci-0000:00-12-ata"], -}; - -const sda1 = { - sid: "60", - isDrive: false, - type: "", - active: true, - name: "/dev/sda1", - size: 512, - recoverableSize: 128, - systems : [], - udevIds: [], - udevPaths: [] -}; - -const sda2 = { - sid: "61", - isDrive: false, - type: "", - active: true, - name: "/dev/sda2", - size: 512, - recoverableSize: 0, - systems : [], - udevIds: [], - udevPaths: [] -}; - -sda.partitionTable = { - type: "gpt", - partitions: [sda1, sda2], - unpartitionedSize: 512 -}; - -const sdb = { - sid: "62", - isDrive: true, - type: "disk", - vendor: "Samsung", - model: "Samsung Evo 8 Pro", - driver: ["ahci"], - bus: "IDE", - busId: "", - transport: "", - dellBOSS: false, - sdCard: false, - active: true, - name: "/dev/sdb", - size: 2048, - recoverableSize: 0, - systems : [], - udevIds: [], - udevPaths: ["pci-0000:00-19"] -}; - -let policy; -let devices; -let actions; - -const openPopup = async (props = {}) => { - const allProps = { policy, devices, actions, ...props }; - const { user } = plainRender(); - const button = screen.getByRole("button"); - - await user.click(button); - const dialog = screen.getByRole("dialog", { name: "Find Space" }); - return { user, dialog }; -}; - -const expandRow = async (user, dialog, name) => { - const row = within(dialog).getByRole("row", { name }); - const toggler = within(row).getByRole("button", { name: /expand/i }); - await user.click(toggler); -}; - -const checkSpaceActions = async (deviceActions) => { - deviceActions.forEach(({ name, action }) => { - const row = screen.getByRole("row", { name }); - const selector = within(row).getByRole("combobox", { name }); - within(selector).getByRole("option", { name: action, selected: true }); - }); -}; - -beforeEach(() => { - devices = [sda, sdb]; - policy = "keep"; - actions = [ - { device: "/dev/sda1", action: "force_delete" }, - { device: "/dev/sda2", action: "resize" } - ]; - - resetLocalStorage(); -}); - -describe("ProposalSpacePolicyField", () => { - it("renders a button for opening the space policy dialog", async () => { - const { user } = plainRender(); - const button = screen.getByRole("button"); - - await user.click(button); - - screen.getByRole("dialog", { name: "Find Space" }); - }); - - it("renders the button with a text according to given policy", () => { - const { rerender } = plainRender(); - screen.getByRole("button", { name: /deleting/ }); - rerender(); - screen.getByRole("button", { name: /shrinking/ }); - }); - - describe("within the dialog", () => { - it("renders the space policy picker", async () => { - const { dialog } = await openPopup(); - const picker = within(dialog).getByRole("listbox"); - within(picker).getByRole("option", { name: /delete/i }); - within(picker).getByRole("option", { name: /resize/i }); - within(picker).getByRole("option", { name: /available/i }); - within(picker).getByRole("option", { name: /custom/i }); - }); - - describe("when there are no installation devices", () => { - beforeEach(() => { - devices = []; - }); - - it("does not render the policy actions", async () => { - const { dialog } = await openPopup(); - const actionsTree = within(dialog).queryByRole("treegrid", { name: "Actions to find space" }); - expect(actionsTree).toBeNull(); - }); - }); - - describe("when there are installation devices", () => { - it("renders the policy actions", async () => { - const { dialog } = await openPopup(); - within(dialog).getByRole("treegrid", { name: "Actions to find space" }); - }); - }); - - describe.each([ - { id: 'delete', nameRegexp: /delete/i }, - { id: 'resize', nameRegexp: /shrink/i }, - { id: 'keep', nameRegexp: /the space not assigned/i } - ])("when space policy is '$id'", ({ id, nameRegexp }) => { - beforeEach(() => { - policy = id; - }); - - it("only renders '$id' option as selected", async () => { - const { dialog } = await openPopup(); - const picker = within(dialog).getByRole("listbox"); - within(picker).getByRole("option", { name: nameRegexp, selected: true }); - expect(within(picker).getAllByRole("option", { selected: false }).length).toEqual(3); - }); - - it("does not allow to modify the space actions", async () => { - const { dialog } = await openPopup(); - // NOTE: HTML `disabled` attribute removes the element from the a11y tree. - // That's why the test is using `hidden: true` here to look for disabled actions. - // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled - // https://testing-library.com/docs/queries/byrole/#hidden - // TODO: use a more inclusive way to disable the actions. - // https://css-tricks.com/making-disabled-buttons-more-inclusive/ - const spaceActions = within(dialog).getAllByRole("combobox", { name: /Space action selector/, hidden: true }); - expect(spaceActions.length).toEqual(3); - }); - }); - - describe("when space policy is 'custom'", () => { - beforeEach(() => { - policy = "custom"; - }); - - it("only renders 'custom' option as selected", async () => { - const { dialog } = await openPopup(); - const picker = within(dialog).getByRole("listbox"); - within(picker).getByRole("option", { name: /custom/i, selected: true }); - expect(within(picker).getAllByRole("option", { selected: false }).length).toEqual(3); - }); - - it("allows to modify the space actions", async () => { - const { dialog } = await openPopup(); - const spaceActions = within(dialog).getAllByRole("combobox", { name: /Space action selector/ }); - expect(spaceActions.length).toEqual(3); - }); - }); - - describe("DeviceActionColumn", () => { - it("renders the space actions selector for devices without partition table", async () => { - const { dialog } = await openPopup(); - const sdaRow = within(dialog).getByRole("row", { name: /sda/ }); - const sdaActionsSelector = within(sdaRow).queryByRole("combobox", { name: "Space action selector for /dev/sda" }); - // sda has partition table, the selector shouldn't be found - expect(sdaActionsSelector).toBeNull(); - const sdbRow = screen.getByRole("row", { name: /sdb/ }); - // sdb does not have partition table, selector should be there - within(sdbRow).getByRole("combobox", { name: "Space action selector for /dev/sdb" }); - }); - - it("does not renders the 'resize' option for drives", async () => { - const { dialog } = await openPopup(); - const sdbRow = within(dialog).getByRole("row", { name: /sdb/ }); - const spaceActionsSelector = within(sdbRow).getByRole("combobox", { name: "Space action selector for /dev/sdb" }); - const resizeOption = within(spaceActionsSelector).queryByRole("option", { name: /resize/ }); - expect(resizeOption).toBeNull(); - }); - - it("renders the 'resize' option for devices other than drives", async () => { - const { user, dialog } = await openPopup(); - const sdaRow = within(dialog).getByRole("row", { name: /sda/ }); - const sdaToggler = within(sdaRow).getByRole("button", { name: /expand/i }); - await user.click(sdaToggler); - const sda1Row = screen.getByRole("row", { name: /sda1/ }); - const spaceActionsSelector = within(sda1Row).getByRole("combobox", { name: "Space action selector for /dev/sda1" }); - within(spaceActionsSelector).getByRole("option", { name: /resize/ }); - }); - - describe("when space policy is 'delete'", () => { - beforeEach(() => { - policy = "delete"; - }); - - it("renders as selected the delete option", async () => { - const { user, dialog } = await openPopup(); - await expandRow(user, dialog, /sda/); - await checkSpaceActions([ - { name: /sda1/, action: /delete/i }, - { name: /sda2/, action: /delete/i } - ]); - }); - }); - - describe("when space policy is 'resize'", () => { - beforeEach(() => { - policy = "resize"; - }); - - it("renders as selected the resize option", async () => { - const { user, dialog } = await openPopup(); - await expandRow(user, dialog, /sda/); - await checkSpaceActions([ - { name: /sda1/, action: /resize/i }, - { name: /sda2/, action: /resize/i } - ]); - }); - }); - - describe("when space policy is 'keep'", () => { - beforeEach(() => { - policy = "keep"; - }); - - it("renders as selected the keep option", async () => { - const { user, dialog } = await openPopup(); - await expandRow(user, dialog, /sda/); - await checkSpaceActions([ - { name: /sda1/, action: /not modify/i }, - { name: /sda2/, action: /not modify/i } - ]); - }); - }); - - describe("when space policy is 'custom'", () => { - beforeEach(() => { - policy = "custom"; - }); - - it("renders as selected the option matching the given device space action", async () => { - await openPopup(); - await checkSpaceActions([ - { name: /sda1/, action: /delete/i }, - { name: /sda2/, action: /resize/i } - ]); - }); - }); - }); - }); - - it("triggers the onChange callback when user accepts the dialog", async () => { - const onChangeFn = jest.fn(); - const { user, dialog } = await openPopup({ onChange: onChangeFn }); - - // Select 'custom' - const picker = within(dialog).getByRole("listbox"); - await user.selectOptions( - picker, - within(picker).getByRole("option", { name: /custom/i }) - ); - - // Select custom actions - const sda1Row = within(dialog).getByRole("row", { name: /sda1/ }); - const sda1Select = within(sda1Row).getByRole("combobox", { name: "Space action selector for /dev/sda1" }); - await user.selectOptions( - sda1Select, - within(sda1Select).getByRole("option", { name: /delete/i }) - ); - const sda2Row = within(dialog).getByRole("row", { name: /sda2/ }); - const sda2Select = within(sda2Row).getByRole("combobox", { name: "Space action selector for /dev/sda2" }); - await user.selectOptions( - sda2Select, - within(sda2Select).getByRole("option", { name: /resize/i }) - ); - - // Accept the result - const acceptButton = within(dialog).getByRole("button", { name: "Accept" }); - await user.click(acceptButton); - - expect(onChangeFn).toHaveBeenCalledWith( - "custom", - expect.arrayContaining([{ action: "resize", device: "/dev/sda2" }, { action: "force_delete", device: "/dev/sda1" }]) - ); - }); -}); diff --git a/web/src/components/storage/index.js b/web/src/components/storage/index.js index eda9195002..d0e3db5f5c 100644 --- a/web/src/components/storage/index.js +++ b/web/src/components/storage/index.js @@ -22,7 +22,6 @@ export { default as ProposalPage } from "./ProposalPage"; export { default as ProposalPageMenu } from "./ProposalPageMenu"; export { default as ProposalSettingsSection } from "./ProposalSettingsSection"; -export { default as ProposalSpacePolicyField } from "./ProposalSpacePolicyField"; export { default as ProposalDeviceSection } from "./ProposalDeviceSection"; export { default as ProposalTransactionalInfo } from "./ProposalTransactionalInfo"; export { default as ProposalActionsDialog } from "./ProposalActionsDialog";