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

adds VM (domain) title and description to UI #1001

Closed
wants to merge 6 commits into from
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
26 changes: 26 additions & 0 deletions po/de.po
Original file line number Diff line number Diff line change
Expand Up @@ -3454,3 +3454,29 @@ msgstr "Ja"

#~ msgid "show more"
#~ msgstr "Zeig mehr"

#: src/components/vm/overview/descriptionModal.jsx

#~ msgid "Description could not be changed"
#~ msgstr "Beschreibung konnte nicht geändert werden"

#~ msgid "New description"
#~ msgstr "Neue Beschreibung"

#~ msgid "Set description of $0"
#~ msgstr "Beschreibung von $0 festlegen"

#: src/components/vm/overview/titleModal.jsx

#~ msgid "Title could not be changed"
#~ msgstr "Titel konnte nicht geändert werden"

#~ msgid "New title"
#~ msgstr "Neuer Titel"

#~ msgid "Set title of $0"
#~ msgstr "Titel von $0 festlegen"

#: src/components/vm/overview/vmOverviewCard.jsx:256
#~ msgid "Title"
#~ msgstr "Titel"
64 changes: 64 additions & 0 deletions src/components/vm/overview/descriptionModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { useState } from 'react';
import cockpit from 'cockpit';
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
import { useDialogs } from 'dialogs.jsx';

import { ModalError } from 'cockpit-components-inline-notification.jsx';
import { domainSetDescription } from '../../../libvirtApi/domain.js';

const _ = cockpit.gettext;

export const DescriptionModal = ({ vm }) => {
const Dialogs = useDialogs();
const [error, setError] = useState({});
const [newDescription, setNewDescription] = useState(vm.description);
const [isLoading, setIsLoading] = useState(false);

function save() {
setIsLoading(true);
domainSetDescription({
name: vm.name,
id: vm.id,
connectionName: vm.connectionName,
description: newDescription
}).then(Dialogs.close, exc => {
setIsLoading(false);
setError({ dialogError: _("Description could not be changed"), dialogErrorDetail: exc.message });
});
}

const defaultBody = (
<Form isHorizontal>
<FormGroup id="description-modal-select-group" label={_("New description")}
fieldId="set-description-dialog"
>
<TextInput id='set-description-dialog'
value={newDescription}
onChange={setNewDescription} />
</FormGroup>
</Form>
);

return (
<Modal position="top" variant="small" isOpen onClose={Dialogs.close}
title={cockpit.format(_("Set description of $0"), vm.name)}
footer={
<>
<Button variant='primary' isDisabled={isLoading} isLoading={isLoading} id="set-description-dialog-apply" onClick={save}>
{_("Save")}
</Button>
<Button variant='link' onClick={Dialogs.close}>
{_("Cancel")}
</Button>
</>
}>
<>
{error && error.dialogError && <ModalError dialogError={error.dialogError} dialogErrorDetail={error.dialogErrorDetail} />}
{defaultBody}
</>
</Modal>
);
};
64 changes: 64 additions & 0 deletions src/components/vm/overview/titleModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { useState } from 'react';
import cockpit from 'cockpit';
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
import { useDialogs } from 'dialogs.jsx';

import { ModalError } from 'cockpit-components-inline-notification.jsx';
import { domainSetTitle } from '../../../libvirtApi/domain.js';

const _ = cockpit.gettext;

export const TitleModal = ({ vm }) => {
const Dialogs = useDialogs();
const [error, setError] = useState({});
const [newTitle, setNewTitle] = useState(vm.title);
const [isLoading, setIsLoading] = useState(false);

function save() {
setIsLoading(true);
domainSetTitle({
name: vm.name,
id: vm.id,
connectionName: vm.connectionName,
title: newTitle
}).then(Dialogs.close, exc => {
setIsLoading(false);
setError({ dialogError: _("Title could not be changed"), dialogErrorDetail: exc.message });
});
}

const defaultBody = (
<Form isHorizontal>
<FormGroup id="title-modal-select-group" label={_("New title")}
fieldId="set-title-dialog"
>
<TextInput id='set-title-dialog'
value={newTitle}
onChange={setNewTitle} />
</FormGroup>
</Form>
);

return (
<Modal position="top" variant="small" isOpen onClose={Dialogs.close}
title={cockpit.format(_("Set title of $0"), vm.name)}
footer={
<>
<Button variant='primary' isDisabled={isLoading} isLoading={isLoading} id="set-title-dialog-apply" onClick={save}>
{_("Save")}
</Button>
<Button variant='link' onClick={Dialogs.close}>
{_("Cancel")}
</Button>
</>
}>
<>
{error && error.dialogError && <ModalError dialogError={error.dialogError} dialogErrorDetail={error.dialogErrorDetail} />}
{defaultBody}
</>
</Modal>
);
};
94 changes: 91 additions & 3 deletions src/components/vm/overview/vmOverviewCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/ind
import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
import { DialogsContext } from 'dialogs.jsx';

import { RenameDialog } from '../vmRenameDialog.jsx';
import { TitleModal } from './titleModal.jsx';
import { DescriptionModal } from './descriptionModal.jsx';
import { VCPUModal } from './vcpuModal.jsx';
import { CPUTypeModal } from './cpuTypeModal.jsx';
import MemoryModal from './memoryModal.jsx';
Expand Down Expand Up @@ -58,10 +61,14 @@ class VmOverviewCard extends React.Component {
this.state = {
virtXMLAvailable: undefined,
};
this.openName = this.openName.bind(this);
this.openTitle = this.openTitle.bind(this);
this.openDescription = this.openDescription.bind(this);
this.openVcpu = this.openVcpu.bind(this);
this.openCpuType = this.openCpuType.bind(this);
this.openMemory = this.openMemory.bind(this);
this.onAutostartChanged = this.onAutostartChanged.bind(this);
this.guestOffEditButton = this.guestOffEditButton.bind(this);
}

componentDidMount() {
Expand All @@ -81,6 +88,23 @@ class VmOverviewCard extends React.Component {
});
}

openName() {
const Dialogs = this.context;
Dialogs.show(<RenameDialog vmName={this.props.vm.name}
vmId={this.props.vm.id}
connectionName={this.props.vm.connectionName} />);
}

openTitle() {
const Dialogs = this.context;
Dialogs.show(<TitleModal vm={this.props.vm} />);
}

openDescription() {
const Dialogs = this.context;
Dialogs.show(<DescriptionModal vm={this.props.vm} />);
}

openVcpu() {
const Dialogs = this.context;
Dialogs.show(<VCPUModal vm={this.props.vm} maxVcpu={this.props.maxVcpu} />);
Expand All @@ -96,6 +120,15 @@ class VmOverviewCard extends React.Component {
Dialogs.show(<MemoryModal vm={this.props.vm} config={this.props.config} />);
}

guestOffEditButton(vm, action) {
const editButton = (
<Button variant="link" isInline isAriaDisabled={vm.state != 'shut off'} onClick={action}>
{_("edit")}
</Button>
);
return vm.state == 'shut off' ? editButton : (<Tooltip content={_("Only editable when the guest is shut off")}>{editButton}</Tooltip>);
}

render() {
const { vm, nodeDevices, libvirtVersion } = this.props;
const idPrefix = vmId(vm.name);
Expand Down Expand Up @@ -125,6 +158,38 @@ class VmOverviewCard extends React.Component {
label={_("Run when host boots")} />
</DescriptionListDescription>
);

const nameLink = (
<DescriptionListDescription id={`${idPrefix}-name`}>
<Flex spaceItems={{ default: 'spaceItemsSm' }}>
<FlexItem>
{vm.name}
</FlexItem>
{this.guestOffEditButton(vm, this.openName)}
</Flex>
</DescriptionListDescription>
);
const titleLink = (
<DescriptionListDescription id={`${idPrefix}-title`}>
<Flex spaceItems={{ default: 'spaceItemsSm' }}>
<FlexItem>
{vm.title}
</FlexItem>
{this.guestOffEditButton(vm, this.openTitle)}
</Flex>
</DescriptionListDescription>
);
const descriptionLink = (
<DescriptionListDescription id={`${idPrefix}-description`}>
<Flex spaceItems={{ default: 'spaceItemsSm' }}>
<FlexItem>
{vm.description}
</FlexItem>
{this.guestOffEditButton(vm, this.openDescription)}
</Flex>
</DescriptionListDescription>
);

const memory = convertToBestUnit(vm.currentMemory, units.KiB);
const memoryLink = (
<DescriptionListDescription id={`${idPrefix}-memory-count`}>
Expand Down Expand Up @@ -176,10 +241,33 @@ class VmOverviewCard extends React.Component {
);

return (
<Flex className="overview-tab" direction={{ default: "column", "2xl": "row" }}>
<Flex className="overview-tab" direction={{ default: "column" }}>
<FlexItem>
<DescriptionList isHorizontal>
{ // Only show the name, if the title is set
vm.title &&
(
<DescriptionListGroup>
<DescriptionListTerm>{_("Name")}</DescriptionListTerm>
{nameLink}
</DescriptionListGroup>
)
}

<DescriptionListGroup>
<DescriptionListTerm>{_("Title")}</DescriptionListTerm>
{titleLink}
</DescriptionListGroup>

<DescriptionListGroup>
<DescriptionListTerm>{_("Description")}</DescriptionListTerm>
{descriptionLink}
</DescriptionListGroup>
</DescriptionList>
</FlexItem>
<FlexItem>
<DescriptionList isHorizontal>
<Text component={TextVariants.h4}>
<Text component={TextVariants.h3}>
{_("General")}
</Text>

Expand Down Expand Up @@ -240,7 +328,7 @@ class VmOverviewCard extends React.Component {
</FlexItem>
<FlexItem>
<DescriptionList isHorizontal>
<Text component={TextVariants.h4}>
<Text component={TextVariants.h3}>
{_("Hypervisor details")}
</Text>

Expand Down
19 changes: 14 additions & 5 deletions src/components/vm/vmDetailsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { CodeBlock, CodeBlockCode } from "@patternfly/react-core/dist/esm/compon
import { Gallery } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js";
import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
import { Card, CardActions, CardBody, CardFooter, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
import { Page, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page/index.js";
import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
Expand Down Expand Up @@ -90,12 +91,20 @@ export const VmDetailsPage = ({
const vmActionsPageSection = (
<PageSection className="actions-pagesection" variant={PageSectionVariants.light} isWidthLimited>
<div className="vm-top-panel" data-vm-transient={!vm.persistent}>
<h2 className="vm-name">{vm.name}</h2>
<VmActions vm={vm}
config={config}
onAddErrorNotification={onAddErrorNotification}
isDetailsPage />
<Flex spaceItems={{ default: 'spaceItemsMd' }}>
<FlexItem component='h2'>{vm.title ? vm.title : vm.name}</FlexItem>
{vm.title &&
<FlexItem component='h3'>
({vm.name})
</FlexItem>
}
<VmActions vm={vm}
config={config}
onAddErrorNotification={onAddErrorNotification}
isDetailsPage />
</Flex>
</div>
{ vm.description && <h4>{vm.description}</h4> }
</PageSection>
);

Expand Down
2 changes: 1 addition & 1 deletion src/components/vms/hostvmslist.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ const HostVmsList = ({ vms, config, ui, storagePools, actions, networks, onAddEr
isDisabled={vm.isUi && !vm.createInProgress}
component="a"
href={'#' + cockpit.format("vm?name=$0&connection=$1", encodeURIComponent(vm.name), vm.connectionName)}
className="vm-list-item-name">{vm.name}</Button>
className="vm-list-item-name">{vm.title ? vm.title + ' (' + vm.name + ')' : vm.name}</Button>
},
{ title: rephraseUI('connections', vm.connectionName) },
{
Expand Down
4 changes: 4 additions & 0 deletions src/libvirt-xml-parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ export function parseDomainDumpxml(connectionName, domXml, objPath) {
const metadataElem = getSingleOptionalElem(domainElem, "metadata");

const name = domainElem.getElementsByTagName("name")[0].childNodes[0].nodeValue;
const title = domainElem.getElementsByTagName("title")[0]?.childNodes[0]?.nodeValue;
const description = domainElem.getElementsByTagName("description")[0]?.childNodes[0]?.nodeValue;
const id = objPath;
const osType = osTypeElem.nodeValue;
const osBoot = parseDumpxmlForOsBoot(osBootElems);
Expand Down Expand Up @@ -267,6 +269,8 @@ export function parseDomainDumpxml(connectionName, domXml, objPath) {
return {
connectionName,
name,
title,
description,
id,
osType,
osBoot,
Expand Down
Loading