diff --git a/src/changeRemoteModal.jsx b/src/changeRemoteModal.jsx
deleted file mode 100644
index 0e5a37b9..00000000
--- a/src/changeRemoteModal.jsx
+++ /dev/null
@@ -1,265 +0,0 @@
-/*
- * This file is part of Cockpit.
- *
- * Copyright (C) 2020 Red Hat, Inc.
- *
- * Cockpit is free software; you can redistribute it and/or modify it
- * under the terms of the GNU Lesser General Public License as published by
- * the Free Software Foundation; either version 2.1 of the License, or
- * (at your option) any later version.
- *
- * Cockpit 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with Cockpit; If not, see .
- */
-
-import React, { useState } from 'react';
-import PropTypes from "prop-types";
-
-import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/esm/components/Alert";
-import { Button } from "@patternfly/react-core/dist/esm/components/Button";
-import { ActionGroup, Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form";
-import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox";
-import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
-import { SimpleList, SimpleListItem } from "@patternfly/react-core/dist/esm/components/SimpleList";
-import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea";
-import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput";
-import { Title } from "@patternfly/react-core/dist/esm/components/Title";
-
-import { PencilAltIcon, AddCircleOIcon } from '@patternfly/react-icons';
-
-import { FormHelper } from 'cockpit-components-form-helper.jsx';
-import cockpit from 'cockpit';
-
-import * as remotes from './remotes';
-
-const _ = cockpit.gettext;
-
-export const ChangeRemoteModal = ({ setIsModalOpen, isModalOpen, remotesList, currentRemote, refreshRemotes, onChangeRemoteOrigin }) => {
- const [addNewRepoDialogOpen, setAddNewRepoDialogOpen] = useState(false);
- const [editRepoDialogOpen, setEditRepoDialogOpen] = useState(false);
- const [selectedRemote, setSelectedRemote] = useState(currentRemote);
- const [error, setError] = useState("");
-
- // Disable 'Change Repository' button when the 'Edit' form is open or when the previously selected remote does not exit any more (got deleted)
- const footer = (
- <>
- {
- onChangeRemoteOrigin(selectedRemote).then(() => setIsModalOpen(false), ex => setError(ex.message));
- }}>
- {_("Change repository")}
-
- setIsModalOpen(false)}>
- {_("Cancel")}
-
- >
- );
-
- return (
- setIsModalOpen(false)}
- footer={footer}>
- <>
- {error && }
- setSelectedRemote(currentItemProps.id)}>
- {(remotesList || []).map(remote => {
- return (
- (!editRepoDialogOpen || editRepoDialogOpen.name !== remote)
- ? {
- ev.stopPropagation();
- ev.preventDefault();
- }}
- isActive={remote === selectedRemote}>
- {remote}
- {
- remotes.loadRemoteSettings(remote)
- .then(remoteSettings => setEditRepoDialogOpen(Object.assign(remoteSettings, { name: remote })));
- }}
- className="edit-remote"
- variant="secondary">
-
-
-
- :
-
-
- );
- }).concat([
- !addNewRepoDialogOpen
- ? {
- ev.stopPropagation();
- ev.preventDefault();
- }}
- key="add-new">
- {
- ev.stopPropagation();
- ev.preventDefault();
- setAddNewRepoDialogOpen(true);
- }}
- variant="link"
- icon={ }
- id="add-new-remote-btn"
- iconPosition="left">{_("Add new repository")}
-
- :
- ])}
-
- >
-
- );
-};
-
-ChangeRemoteModal.propTypes = {
- remotesList: PropTypes.array.isRequired,
- currentRemote: PropTypes.string,
- isModalOpen: PropTypes.bool.isRequired,
- setIsModalOpen: PropTypes.func.isRequired,
- refreshRemotes: PropTypes.func.isRequired,
- onChangeRemoteOrigin: PropTypes.func.isRequired,
-};
-
-const AddNewRepoForm = ({ setAddNewRepoDialogOpen, refreshRemotes }) => {
- const [newRepoName, setNewRepoName] = useState("");
- const [newRepoURL, setNewRepoURL] = useState("");
- const [newRepoTrusted, setNewRepoTrusted] = useState(false);
-
- const [hasValidation, setHasValidation] = useState(false);
- const [addNewRepoError, setAddNewRepoError] = useState(undefined);
-
- const onAddRemote = () => {
- if (!(newRepoURL.trim().length && newRepoName.trim().length)) {
- setHasValidation(true);
- return;
- }
- return remotes.addRemote(newRepoName, newRepoURL, newRepoTrusted)
- .then(() => refreshRemotes())
- .then(() => setAddNewRepoDialogOpen(false),
- ex => setAddNewRepoError(ex.message));
- };
-
- return (
-
- );
-};
-AddNewRepoForm.propTypes = {
- refreshRemotes: PropTypes.func.isRequired,
- setAddNewRepoDialogOpen: PropTypes.func.isRequired,
-};
-
-const EditRemoteForm = ({ remoteSettings, setEditRepoDialogOpen, refreshRemotes }) => {
- const [addAnotherKey, setAddAnotherKey] = useState(false);
- const [key, setKey] = useState('');
- const [isTrusted, setIsTrusted] = useState(remoteSettings['gpg-verify'] !== 'false');
- const [error, setError] = useState('');
-
- const onUpdate = () => {
- const promises = [];
- if (key)
- promises.push(remotes.importGPGKey(remoteSettings.name, key));
- promises.push(remotes.updateRemoteSettings(remoteSettings.name, { "gpg-verify": isTrusted }));
-
- Promise.all(promises).then(() => setEditRepoDialogOpen(false), ex => setError(ex.message));
- };
- const onDelete = () => {
- remotes.deleteRemote(remoteSettings.name)
- .then(() => refreshRemotes())
- .then(setEditRepoDialogOpen(false), ex => setError(ex.message));
- };
-
- return (
-
- );
-};
-EditRemoteForm.propTypes = {
- refreshRemotes: PropTypes.func.isRequired,
- setEditRepoDialogOpen: PropTypes.func.isRequired,
- remoteSettings: PropTypes.object.isRequired,
-};
diff --git a/src/client.js b/src/client.js
index daf04601..48f23d36 100644
--- a/src/client.js
+++ b/src/client.js
@@ -369,6 +369,9 @@ class RPMOSTreeDBusClient {
if (deployment.id && deployment.osname?.v !== os_name)
continue;
+ // required for pinning deployments
+ deployment.index = i;
+
// always show the default deployment,
// skip showing the upgrade if it is the
// same as the default.
diff --git a/src/deploymentModals.jsx b/src/deploymentModals.jsx
new file mode 100644
index 00000000..599e658f
--- /dev/null
+++ b/src/deploymentModals.jsx
@@ -0,0 +1,213 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see .
+ */
+
+import React, { useState } from 'react';
+
+import cockpit from 'cockpit';
+import client from './client';
+
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Checkbox } from '@patternfly/react-core/dist/esm/components/Checkbox';
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack";
+import { Text } from "@patternfly/react-core/dist/esm/components/Text";
+
+import { useDialogs } from "dialogs.jsx";
+
+const _ = cockpit.gettext;
+
+export const CleanUpModal = ({ os }) => {
+ const Dialogs = useDialogs();
+
+ const [deleteTemporaryFiles, setDeleteTemporaryFiles] = useState(true);
+ const [deleteRPMmetadata, setDeleteRPMmetadata] = useState(true);
+ const [deletePendingDeployments, setDeletePendingDeployments] = useState(false);
+ const [deleteRollbackDeployments, setDeleteRollbackDeployments] = useState(false);
+ const [buttonLoading, setButtonLoading] = useState(false);
+ const [error, setError] = useState("");
+
+ const doCleanup = () => {
+ setButtonLoading(true);
+ setError("");
+
+ const cleanupFlags = [];
+ if (deleteTemporaryFiles) {
+ cleanupFlags.push("base");
+ }
+ if (deleteRPMmetadata) {
+ cleanupFlags.push("repomd");
+ }
+ if (deletePendingDeployments) {
+ cleanupFlags.push("pending-deploy");
+ }
+ if (deleteRollbackDeployments) {
+ cleanupFlags.push("rollback-deploy");
+ }
+
+ return client.run_transaction("Cleanup", [cleanupFlags], os)
+ .then(Dialogs.close)
+ .catch(ex => {
+ console.warn(ex);
+ setError(ex.message);
+ setButtonLoading(false);
+ });
+ };
+
+ const actions = [
+ doCleanup()}
+ >
+ {_("Clean up")}
+ ,
+
+ {_("Cancel")}
+
+ ];
+
+ return (
+
+ {error &&
+
+ }
+
+ setDeleteTemporaryFiles(isChecked)}
+ isChecked={deleteTemporaryFiles}
+ />
+ setDeleteRPMmetadata(isChecked)}
+ isChecked={deleteRPMmetadata}
+ />
+ setDeletePendingDeployments(isChecked)}
+ isChecked={deletePendingDeployments}
+ />
+ setDeleteRollbackDeployments(isChecked)}
+ isChecked={deleteRollbackDeployments}
+ />
+
+
+ );
+};
+
+export const ResetModal = ({ os }) => {
+ const Dialogs = useDialogs();
+
+ const [removeOverlays, setRemoveOverlays] = useState(false);
+ const [removeOverrides, setRemoveOverrides] = useState(false);
+ const [error, setError] = useState("");
+ const [buttonLoading, setButtonLoading] = useState(false);
+
+ const doReset = () => {
+ setButtonLoading(true);
+ setError("");
+
+ const resetFlags = {};
+ if (removeOverlays) {
+ // remove all overlayed packages
+ resetFlags["no-layering"] = { t: "b", v: true };
+ }
+ if (removeOverrides) {
+ // remove all overrides
+ resetFlags["no-overrides"] = { t: "b", v: true };
+ }
+
+ return client.run_transaction("UpdateDeployment", [{}, resetFlags], os)
+ .then(Dialogs.close)
+ .catch(ex => {
+ console.warn(ex);
+ setError(ex.message);
+ setButtonLoading(false);
+ });
+ };
+
+ const actions = [
+ doReset()}>
+ {_("Reset to original state")}
+ ,
+
+ {_("Cancel")}
+
+ ];
+
+ return (
+
+ {error &&
+
+ }
+
+ {_("Remove package additions or substitutions to return the current deployment to its original state.")}
+
+
+ setRemoveOverlays(isChecked)}
+ isChecked={removeOverlays}
+ description={_("Packages which have been added to the system")}
+ />
+ setRemoveOverrides(isChecked)}
+ isChecked={removeOverrides}
+ description={_("Substitutions of packages normally included in an OS build")}
+ />
+
+
+ );
+};
diff --git a/src/ostree.jsx b/src/ostree.jsx
index ddd558e4..b2e31b26 100644
--- a/src/ostree.jsx
+++ b/src/ostree.jsx
@@ -30,22 +30,22 @@ import { Alert } from "@patternfly/react-core/dist/esm/components/Alert";
import { Button } from "@patternfly/react-core/dist/esm/components/Button";
import { Card, CardHeader, CardTitle, CardBody } from "@patternfly/react-core/dist/esm/components/Card";
import { EmptyState, EmptyStateIcon, EmptyStateBody, EmptyStateHeader, EmptyStateFooter, EmptyStateVariant } from "@patternfly/react-core/dist/esm/components/EmptyState";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
import {
DescriptionList, DescriptionListGroup, DescriptionListTerm, DescriptionListDescription
} from "@patternfly/react-core/dist/esm/components/DescriptionList";
-import { Label } from "@patternfly/react-core/dist/esm/components/Label";
-import {
- OverflowMenu, OverflowMenuContent, OverflowMenuGroup, OverflowMenuItem, OverflowMenuControl, OverflowMenuDropdownItem
-} from "@patternfly/react-core/dist/esm/components/OverflowMenu";
-import { Page, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page";
+import { Gallery, } from "@patternfly/react-core/dist/esm/layouts/Gallery/index.js";
+import { Label, } from "@patternfly/react-core/dist/esm/components/Label";
+import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import { Page, PageSection, } from "@patternfly/react-core/dist/esm/components/Page";
import { Popover } from "@patternfly/react-core/dist/esm/components/Popover";
import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner";
-import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core/dist/esm/components/Toolbar";
+import { Text } from "@patternfly/react-core/dist/esm/components/Text";
-import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select";
-import { Dropdown, KebabToggle } from "@patternfly/react-core/dist/esm/deprecated/components/Dropdown";
+import { Dropdown, DropdownItem, DropdownSeparator, KebabToggle, } from "@patternfly/react-core/dist/esm/deprecated/components/Dropdown";
-import { ExclamationCircleIcon, PendingIcon, ErrorCircleOIcon } from '@patternfly/react-icons';
+import { BugIcon, CheckIcon, ExclamationCircleIcon, ExclamationTriangleIcon, PendingIcon, ErrorCircleOIcon, CheckCircleIcon, SyncAltIcon } from '@patternfly/react-icons';
import cockpit from 'cockpit';
@@ -56,9 +56,11 @@ import { ListingPanel } from 'cockpit-components-listing-panel.jsx';
import client from './client';
import * as remotes from './remotes';
-import { ChangeRemoteModal } from './changeRemoteModal.jsx';
+import { AddRepositoryModal, EditRepositoryModal, RebaseRepositoryModal, RemoveRepositoryModal } from './repositoryModals.jsx';
import './ostree.scss';
+import { CleanUpModal, ResetModal } from './deploymentModals';
+import { WithDialogs, DialogsContext, useDialogs } from "dialogs.jsx";
const _ = cockpit.gettext;
@@ -156,56 +158,6 @@ Curtain.propTypes = {
reconnect: PropTypes.bool,
};
-const OriginSelector = ({ os, remotes, branches, branchLoadError, currentRemote, currentBranch, setChangeRemoteModal, onChangeBranch }) => {
- const [branchSelectExpanded, setBranchSelectExpanded] = useState(false);
-
- if (!os)
- return null;
-
- const origin = client.get_default_origin(os);
-
- if (!origin || !remotes || remotes.length === 0)
- return ;
-
- return (
- <>
-
-
- { _("Repository") }
- setChangeRemoteModal(true)}>{currentRemote}
-
- { _("Branch") }
-
- setBranchSelectExpanded(exp) }
- onSelect={(_event, branch) => { setBranchSelectExpanded(false); onChangeBranch(branch) } }>
- { branchLoadError
- ? [ ]
- : (branches || []).map(branch => )
- }
-
-
-
-
- {branchLoadError && }
- >
- );
-};
-
-OriginSelector.propTypes = {
- os: PropTypes.string,
- remotes: PropTypes.arrayOf(PropTypes.string),
- branches: PropTypes.arrayOf(PropTypes.string),
- branchLoadError: PropTypes.string,
- currentRemote: PropTypes.string,
- currentBranch: PropTypes.string,
- setChangeRemoteModal: PropTypes.func.isRequired,
- onChangeBranch: PropTypes.func.isRequired,
-};
-
/**
* Render a single deployment in the table
*/
@@ -303,18 +255,14 @@ const SignaturesDetails = ({ signatures }) => {
};
const Deployments = ({ versions }) => {
+ const Dialogs = useDialogs();
const [inProgress, setInProgress] = useState({});
- const [openedKebab, _setOpenedKebab] = useState({});
const [error, _setError] = useState({});
const setError = (id, err) => {
_setError({ ...error, [id]: err });
};
- const setOpenedKebab = (id, state) => {
- _setOpenedKebab({ ...openedKebab, [id]: state });
- };
-
const doRollback = (key, osname) => {
const args = {
reboot: cockpit.variant("b", true)
@@ -346,50 +294,115 @@ const Deployments = ({ versions }) => {
.finally(() => setInProgress({ ...inProgress, [key]: false }));
};
+ const columns = [
+ {
+ title: _("Version"),
+ props: { width: 15, },
+ },
+ {
+ title: _("Status"),
+ props: { width: 15, },
+ },
+ {
+ title: _("Time"),
+ props: { width: 15, },
+ },
+ {
+ title: _("Branch"),
+ },
+ {
+ title: "",
+ props: { className: "pf-v5-c-table__action" }
+ },
+ ];
+
const items = versions.map(item => {
const key = track_id(item);
const packages = client.packages(item);
- return DeploymentDetails(key, item, packages, doRollback, doUpgrade, doRebase, inProgress[key], setError, error[key], setOpenedKebab, openedKebab[key]);
+ return DeploymentDetails(key, item, packages, doRollback, doUpgrade, doRebase, inProgress[key], setError, error[key], Dialogs);
});
+
return (
);
};
-const DeploymentDetails = (akey, info, packages, doRollback, doUpgrade, doRebase, inProgress, setError, error, setOpenedKebab, openedKebab) => {
- let name = null;
- if (info && info.osname) {
- name = info.osname.v;
- if (info.version)
- name += " " + info.version.v;
- }
+const isUpdate = (info) => {
+ return client.item_matches(info, 'CachedUpdate') && !client.item_matches(info, 'DefaultDeployment');
+};
- const isUpdate = () => {
- return client.item_matches(info, 'CachedUpdate') && !client.item_matches(info, 'DefaultDeployment');
- };
+const isRollback = (info) => {
+ return !client.item_matches(info, 'CachedUpdate') && client.item_matches(info, 'RollbackDeployment');
+};
- const isRollback = () => {
- return !client.item_matches(info, 'CachedUpdate') && client.item_matches(info, 'RollbackDeployment');
- };
+const isRebase = (info) => {
+ return !info.id && !client.item_matches(info, 'BootedDeployment', 'origin') && !client.item_matches(info, 'RollbackDeployment') &&
+ !client.item_matches(info, "DefaultDeployment");
+};
- const isRebase = () => {
- return !info.id && !client.item_matches(info, 'BootedDeployment', 'origin') && !client.item_matches(info, 'RollbackDeployment') &&
- !client.item_matches(info, "DefaultDeployment");
- };
+const ConfirmDeploymentChange = ({ actionName, bodyText, onConfirmAction }) => {
+ const Dialogs = useDialogs();
+
+ const actions = [
+ { onConfirmAction(); Dialogs.close() }}
+ >
+ {cockpit.format(_("$0 and reboot"), actionName)}
+ ,
+
+ {_("Cancel")}
+
+ ];
+
+ const titleContent = (
+
+
+
+
+
+ {`${actionName}?`}
+
+
+ );
- let state;
+ return (
+
+ {bodyText}
+
+ );
+};
+
+const DeploymentDetails = (akey, info, packages, doRollback, doUpgrade, doRebase, inProgress, setError, error, Dialogs) => {
+ const version = info.version ? info.version.v : null;
+
+ const labels = [];
if (inProgress)
- state = }>{_("Updating")};
- else if (info.booted && info.booted.v)
- state = {_("Running")} ;
- else if (error)
- state = (
-
+ labels.push( } key={"updating" + version}>{_("Updating")});
+ if (info.booted && info.booted.v)
+ labels.push( }>{_("Current")});
+ if (info?.pinned?.v)
+ labels.push({_("Pinned")} );
+ if (error)
+ labels.push(
+
}
className="deployment-error"
@@ -402,59 +415,82 @@ const DeploymentDetails = (akey, info, packages, doRollback, doUpgrade, doRebase
);
- else
- state = {_("Available")} ;
+ if (isUpdate(info) || isRebase(info))
+ labels.push({_("New")} );
let action_name = null;
let action = null;
-
- if (isUpdate()) {
- action_name = _("Update and reboot");
- action = () => doUpgrade(akey, info.osname.v, info.checksum.v);
- } else if (isRollback()) {
- action_name = _("Roll back and reboot");
- action = () => doRollback(akey, info.osname.v);
- } else if (isRebase()) {
- action_name = _("Rebase and reboot");
- action = () => doRebase(akey, info.osname.v, info.origin.v, info.checksum.v);
+ const releaseTime = timeformat.distanceToNow(info.timestamp.v * 1000, true);
+
+ if (isUpdate(info)) {
+ action_name = "update";
+ action = () => Dialogs.show( doUpgrade(akey, info.osname.v, info.checksum.v)}
+ />);
+ } else if (isRollback(info)) {
+ action_name = "rollback";
+ action = () => Dialogs.show( doRollback(akey, info.osname.v)}
+ />);
+ } else if (isRebase(info)) {
+ action_name = "rebase";
+ action = () => Dialogs.show( doRebase(akey, info.osname.v, info.origin.v, info.checksum.v)}
+ />);
}
+ const action_button_text = {
+ update: _("Update"),
+ rollback: _("Roll back"),
+ rebase: _("Rebase"),
+ };
const columns = [
- { title: name, props: { className: "deployment-name" } },
- { title: state }
+ { title: version, props: { className: "deployment-name" } },
+ {
+ title: (
+
+ {labels}
+
+ ),
+ }
];
+ columns.push({ title: releaseTime });
+
+ columns.push({ title: info.origin?.v });
+
if (action_name) {
columns.push({
- title:
-
-
-
-
- {action_name}
-
-
-
-
-
- setOpenedKebab(akey, !openedKebab)}
- toggle={
- setOpenedKebab(akey, open)} />
- }
- isOpen={openedKebab}
- isPlain
- dropdownItems={[
- {action_name}
- ]}
- />
-
-
+ title: (
+
+
+ {action_button_text[action_name]}
+
+
+ ),
});
} else {
columns.push({ title: "" });
}
+ if (info.index !== undefined) {
+ columns.push({
+ title: (
+
+ ),
+ props: { className: "pf-v5-c-table__action" }
+ });
+ }
+
let signatures = [];
if (info.signatures && info.signatures.v.length > 0)
signatures = info.signatures.v.map((raw, index) => client.signature_obj(raw));
@@ -484,10 +520,213 @@ const DeploymentDetails = (akey, info, packages, doRollback, doUpgrade, doRebase
});
};
+const DeploymentActions = ({ deploymentIndex, deploymentIsPinned, isCurrent, isStaged }) => {
+ const [isKebabOpen, setKebabOpen] = useState(false);
+
+ const togglePin = () => {
+ const pinFlags = [];
+ if (deploymentIsPinned) {
+ pinFlags.push("--unpin");
+ }
+
+ cockpit.spawn(["ostree", "admin", "pin", ...pinFlags, deploymentIndex], { superuser: "try" })
+ .then(() => setKebabOpen(false));
+ };
+
+ const deleteDeployment = () => {
+ cockpit.spawn(["ostree", "admin", "undeploy", deploymentIndex], { superuser: "try" })
+ .then(() => setKebabOpen(false));
+ };
+
+ const actions = [];
+ if (!isStaged) {
+ actions.push(
+ togglePin()}
+ >
+ {deploymentIsPinned ? _("Unpin") : _("Pin")}
+ ,
+ );
+ }
+
+ if (!isCurrent) {
+ if (actions.length > 0) {
+ actions.push( );
+ }
+ actions.push(
+ deleteDeployment()}
+ >
+ {_("Delete")}
+
+ );
+ }
+
+ return (
+ setKebabOpen(isOpen)} />}
+ isOpen={isKebabOpen}
+ id="deployment-actions"
+ isPlain
+ position="right"
+ dropdownItems={actions} />
+ );
+};
+
+const OStreeStatus = ({ ostreeState, versions }) => {
+ const updates = versions.filter(version => isUpdate(version));
+
+ const statusItems = [];
+ if (updates.length) {
+ statusItems.push({
+ key: "update-available",
+ icon: ,
+ message: _("Update available"),
+ });
+ } else {
+ statusItems.push({
+ key: "up-to-date",
+ icon: ,
+ message: _("System is up to date"),
+ });
+ }
+
+ if (ostreeState.branchLoadError) {
+ const [errorName, errorDetail] = ostreeState.branchLoadError.replace("error: ", "").split(';');
+ statusItems.push({
+ key: "status-error",
+ icon: ,
+ message: (
+ <>
+
+ {errorName}
+
+
+ {errorDetail}
+
+ >
+ ),
+ });
+ }
+
+ return (
+
+
+ {_("Status")}
+
+
+
+ {statusItems.map(item => (
+
+
+ {item.icon}
+ {item.message}
+
+
+ ))}
+
+
+
+ );
+};
+
+OStreeStatus.propTypes = {
+ ostreeState: PropTypes.object.isRequired,
+ versions: PropTypes.array.isRequired,
+};
+
+const OStreeSource = ({ ostreeState, refreshRemotes, onChangeBranch, onChangeRemoteOrigin }) => {
+ const Dialogs = useDialogs();
+ const [isKebabOpen, setKebabOpen] = useState(false);
+
+ const actions = [
+ Dialogs.show(
+
+ )}
+ >
+ {_("Rebase")}
+ ,
+ ,
+ Dialogs.show(
+
+ )}
+ >
+ {_("Add repository")}
+ ,
+ Dialogs.show(
+
+ )}
+ >
+ {_("Edit repository")}
+ ,
+ Dialogs.show(
+
+ )}
+ >
+ {_("Remove repository")}
+ ,
+ ];
+
+ const ostreeSourceActions = (
+ setKebabOpen(isOpen)} />}
+ isPlain
+ isOpen={isKebabOpen}
+ position="right"
+ id="ostree-source-actions"
+ dropdownItems={actions}
+ />
+ );
+
+ return (
+
+
+ {_("OStree source")}
+
+
+
+
+ {_("Repository")}
+ {ostreeState.origin.remote}
+
+
+ {_("Branch")}
+ {ostreeState.origin.branch}
+
+
+
+
+ );
+};
+
+OStreeSource.propTypes = {
+ ostreeState: PropTypes.object.isRequired,
+ refreshRemotes: PropTypes.func.isRequired,
+ onChangeBranch: PropTypes.func.isRequired,
+ onChangeRemoteOrigin: PropTypes.func.isRequired,
+};
+
/**
* Main application
*/
class Application extends React.Component {
+ static contextType = DialogsContext;
+
constructor(props) {
super(props);
this.state = {
@@ -499,7 +738,7 @@ class Application extends React.Component {
origin: { remote: null, branch: null },
curtain: { state: 'silent', failure: false, message: null, final: false },
progressMsg: undefined,
- isChangeRemoteOriginModalOpen: false,
+ isKebabOpen: false,
};
this.onChangeBranch = this.onChangeBranch.bind(this);
@@ -594,7 +833,10 @@ class Application extends React.Component {
}
checkForUpgrades() {
- this.setState({ progressMsg: _("Checking for updates") });
+ this.setState({
+ progressMsg: _("Checking for updates"),
+ error: "",
+ });
return client.check_for_updates(this.state.os, this.state.origin.remote, this.state.origin.branch)
.catch(ex => this.setState({ error: ex }))
@@ -646,6 +888,7 @@ class Application extends React.Component {
}
render() {
+ const Dialogs = this.context;
/* curtain: empty state pattern (connecting, errors) */
const c = this.state.curtain;
if (c.state)
@@ -662,41 +905,54 @@ class Application extends React.Component {
packages.addEventListener("changed", () => this.setState({})); // re-render
});
- const actionButton = (
-
- {_("Check for updates")}
-
+ const kebabActions = [
+ Dialogs.show( )}>
+ {_("Clean up")}
+ ,
+ ,
+ Dialogs.show( )}>
+ {_("Reset")}
+ ,
+ ];
+
+ const cardActions = (
+
+
+
+
+ this.setState({ isKebabOpen: isOpen })} />}
+ isPlain
+ isOpen={this.state.isKebabOpen}
+ position="right"
+ id="deployments-actions"
+ dropdownItems={kebabActions}
+ />
+
);
return (
- this.setState({ isChangeRemoteOriginModalOpen })}
- currentRemote={this.state.origin.remote}
- refreshRemotes={this.refreshRemotes}
- onChangeRemoteOrigin={this.onChangeRemoteOrigin}
- remotesList={this.state.remotes} />
-
- this.setState({ isChangeRemoteOriginModalOpen })} onChangeBranch={this.onChangeBranch} />
-
- {this.state.error && }
-
-
- {_("Deployments and updates")}
-
-
-
-
-
+
+
+
+
+ {this.state.error && }
+
+ {_("Deployments and updates")}
+
+
+
+
+
+
);
@@ -704,5 +960,5 @@ class Application extends React.Component {
}
document.addEventListener("DOMContentLoaded", () => {
- createRoot(document.getElementById("app")).render( );
+ createRoot(document.getElementById("app")).render( );
});
diff --git a/src/ostree.scss b/src/ostree.scss
index 466532c5..13a2e5e2 100644
--- a/src/ostree.scss
+++ b/src/ostree.scss
@@ -22,10 +22,16 @@
@use "page.scss";
@use "ct-card.scss";
@import "patternfly/patternfly-5-overrides.scss";
+@import "global-variables";
+@import "@patternfly/patternfly/utilities/Text/text.scss";
+@import "@patternfly/patternfly/utilities/Spacing/spacing.css";
-#deployments {
- @extend .ct-card;
+/* Style the list cards as ct-cards */
+.pf-v5-c-page__main-section .pf-v5-c-card {
+ @extend .ct-card;
+}
+#deployments {
.deployment-name {
font-weight: 700;
}
diff --git a/src/repositoryModals.jsx b/src/repositoryModals.jsx
new file mode 100644
index 00000000..9c1262c1
--- /dev/null
+++ b/src/repositoryModals.jsx
@@ -0,0 +1,451 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see .
+ */
+
+import React, { useEffect, useState } from 'react';
+import PropTypes from "prop-types";
+
+import cockpit from 'cockpit';
+
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Checkbox } from '@patternfly/react-core/dist/esm/components/Checkbox';
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import { Text } from "@patternfly/react-core/dist/esm/components/Text";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput";
+import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea";
+import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import { ExclamationCircleIcon } from '@patternfly/react-icons';
+
+import { FormHelper } from 'cockpit-components-form-helper.jsx';
+import { useDialogs } from "dialogs.jsx";
+
+import * as remotes from './remotes';
+
+const _ = cockpit.gettext;
+
+export const RemoveRepositoryModal = ({ origin, availableRemotes, refreshRemotes }) => {
+ const Dialogs = useDialogs();
+ const [error, setError] = useState('');
+ const [selectedRemotes, setSelectedRemotes] = useState([]);
+
+ const onDelete = () => {
+ Promise.all(selectedRemotes.map(remote => remotes.deleteRemote(remote)))
+ .then(() => refreshRemotes())
+ .then(Dialogs.close, ex => setError(ex.message));
+ };
+
+ const handleChange = (event, isChecked) => {
+ if (isChecked) {
+ setSelectedRemotes([...selectedRemotes, event.target.id]);
+ } else {
+ setSelectedRemotes(selectedRemotes.filter(remote => remote !== event.target.id));
+ }
+ };
+
+ const actions = [
+ onDelete()}>
+ {_("Remove")}
+ ,
+
+ {_("Cancel")}
+
+ ];
+
+ const repositories = availableRemotes.map(remote => {
+ return (
+
+ );
+ });
+
+ return (
+
+
+ {error && }
+
+ {repositories}
+
+
+
+ );
+};
+
+RemoveRepositoryModal.propTypes = {
+ origin: PropTypes.object.isRequired,
+ availableRemotes: PropTypes.array.isRequired,
+ refreshRemotes: PropTypes.func.isRequired,
+};
+
+export const AddRepositoryModal = ({ refreshRemotes }) => {
+ const Dialogs = useDialogs();
+ const [newRepoName, setNewRepoName] = useState("");
+ const [newRepoURL, setNewRepoURL] = useState("");
+ const [newRepoTrusted, setNewRepoTrusted] = useState(false);
+
+ const [hasValidation, setHasValidation] = useState(false);
+ const [addNewRepoError, setAddNewRepoError] = useState(undefined);
+
+ const onAddRemote = () => {
+ if (!(newRepoURL.trim().length && newRepoName.trim().length)) {
+ setHasValidation(true);
+ return;
+ }
+ return remotes.addRemote(newRepoName, newRepoURL, newRepoTrusted)
+ .then(() => refreshRemotes())
+ .then(Dialogs.close,
+ ex => setAddNewRepoError(ex.message));
+ };
+
+ const actions = [
+ onAddRemote()}>
+ {_("Add repository")}
+ ,
+
+ {_("Cancel")}
+
+ ];
+
+ return (
+
+
+ {addNewRepoError && }
+
+ setNewRepoName(name)}
+ />
+
+
+
+ setNewRepoURL(url)}
+ />
+
+
+
+ setNewRepoTrusted(checked)}
+ />
+
+
+
+ );
+};
+
+AddRepositoryModal.propTypes = {
+ refreshRemotes: PropTypes.func.isRequired,
+};
+
+export const EditRepositoryModal = ({ remote, availableRemotes }) => {
+ const Dialogs = useDialogs();
+ const [addAnotherKey, setAddAnotherKey] = useState(false);
+ const [key, setKey] = useState('');
+ const [error, setError] = useState('');
+ const [isSelectRemoteExpanded, setSelectRemoteExpanded] = useState(false);
+ const [selectedRemote, setSelectedRemote] = useState(remote);
+ const [newURL, setNewURL] = useState('');
+ const [isTrusted, setIsTrusted] = useState(null);
+
+ useEffect(() => {
+ remotes.loadRemoteSettings(selectedRemote)
+ .then(remoteSettings => {
+ setNewURL(remoteSettings.url);
+ setIsTrusted(remoteSettings['gpg-verify'] === "true");
+ });
+ }, [selectedRemote]);
+
+ if (!newURL)
+ return;
+
+ const onUpdate = () => {
+ const promises = [];
+ if (key)
+ promises.push(remotes.importGPGKey(selectedRemote, key));
+
+ const options = {
+ url: newURL,
+ "gpg-verify": isTrusted,
+ };
+
+ promises.push(remotes.updateRemoteSettings(selectedRemote, options));
+
+ Promise.all(promises).then(() => Dialogs.close(), ex => setError(ex.message));
+ };
+
+ const actions = [
+
+ {_("Save")}
+ ,
+
+ {_("Cancel")}
+ ,
+ ];
+
+ return (
+
+
+ {error && }
+
+ setSelectRemoteExpanded(expanded) }
+ onSelect={(_, remote) => { setSelectRemoteExpanded(false); setSelectedRemote(remote) }}
+ menuAppendTo="parent"
+ >
+ {availableRemotes.map(remote => {remote} )}
+
+
+
+ setNewURL(url)}
+ type="text" />
+
+
+ {
+ setIsTrusted(checked);
+ }} />
+
+
+ {!addAnotherKey
+ ? setAddAnotherKey(true)}>{_("Add another key")}
+ : setKey(key)}
+ value={key}
+ aria-label={_("GPG public key")} />}
+
+
+
+ );
+};
+
+EditRepositoryModal.propTypes = {
+ remote: PropTypes.string.isRequired,
+ availableRemotes: PropTypes.array.isRequired,
+};
+
+export const RebaseRepositoryModal = ({ origin, availableRemotes, currentOriginBranches, currentBranchLoadError, onChangeBranch, onChangeRemoteOrigin }) => {
+ const Dialogs = useDialogs();
+ const [isSelectRemoteExpanded, setSelectRemoteExpanded] = useState(false);
+ const [selectedRemote, setSelectedRemote] = useState(origin.remote);
+ const [isSelectBranchExpanded, setSelectBranchExpanded] = useState(false);
+ const [selectedBranch, setSelectedBranch] = useState(origin.branch);
+ const [availableBranches, setAvailableBranches] = useState(currentOriginBranches);
+ const [branchLoadError, setBranchLoadError] = useState(currentBranchLoadError);
+ const [loadingBranches, setLoadingBranches] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handeRemoteSelect = async (remote) => {
+ setSelectedRemote(remote);
+ setLoadingBranches(true);
+ remotes.listBranches(remote)
+ .then(newBranches => {
+ setBranchLoadError(null);
+ setAvailableBranches(newBranches);
+ setLoadingBranches(false);
+
+ if (newBranches.includes(origin.branch)) {
+ setSelectedBranch(origin.branch);
+ } else {
+ setSelectedBranch(newBranches[0]);
+ }
+ })
+ .catch(ex => {
+ setBranchLoadError(ex.message);
+ setAvailableBranches(null);
+ setLoadingBranches(false);
+ });
+ };
+
+ const onRebaseClicked = () => {
+ onChangeRemoteOrigin(selectedRemote)
+ .then(() => onChangeBranch(selectedBranch))
+ .then(Dialogs.close())
+ .catch(ex => setError(ex.message));
+ };
+
+ const actions = [
+
+ {_("Rebase")}
+ ,
+
+ {_("Cancel")}
+
+ ];
+
+ const repositoryComponent = availableRemotes.length > 1
+ ? (
+ setSelectRemoteExpanded(expanded) }
+ onSelect={(_, remote) => { setSelectRemoteExpanded(false); handeRemoteSelect(remote) }}
+ menuAppendTo="parent"
+ >
+ {availableRemotes.map(remote => {remote} )}
+
+ )
+ : (
+ {availableRemotes[0]}
+ );
+
+ const branchComponent = branchLoadError
+ ? (
+
+
+ {branchLoadError.replace("error: ", "")}
+
+ )
+ : (
+ availableBranches.length > 1
+ ? (
+ setSelectBranchExpanded(expanded) }
+ onSelect={(_, branch) => { setSelectBranchExpanded(false); setSelectedBranch(branch) }}
+ menuAppendTo="parent"
+ >
+ {availableBranches.map(branch => {branch} )}
+
+ )
+ : (
+ {availableBranches[0]}
+ )
+ );
+
+ const loadingComponent = (
+
+
+ {_("Loading branches")}
+
+ );
+
+ return (
+
+ {error && }
+
+
+ {repositoryComponent}
+
+
+ {loadingBranches
+ ? loadingComponent
+ : branchComponent
+ }
+
+
+
+ );
+};
+
+RebaseRepositoryModal.propTypes = {
+ origin: PropTypes.object.isRequired,
+ availableRemotes: PropTypes.array.isRequired,
+ currentOriginBranches: PropTypes.array,
+ currentBranchLoadError: PropTypes.string,
+ onChangeBranch: PropTypes.func.isRequired,
+ onChangeRemoteOrigin: PropTypes.func.isRequired,
+};
diff --git a/test/check-ostree b/test/check-ostree
index 3b0abab3..c6b5ef1e 100755
--- a/test/check-ostree
+++ b/test/check-ostree
@@ -53,11 +53,11 @@ def wait_deployment_prop(b, index, prop, value):
deployment = f"#available-deployments > tbody:nth-child({index + 1})"
if prop == "Actions":
if value == "":
- b.wait_text(f"{deployment} tr:nth-child(1) td:last-child", value)
+ b.wait_text(f"{deployment} tr:nth-child(1) td:nth-child(6)", value)
else:
- b.wait_text(f"{deployment} tr:nth-child(1) td:last-child button", value)
+ b.wait_text(f"{deployment} tr:nth-child(1) td:nth-child(6) button", value)
else:
- b.wait_text(f"{deployment} td[data-label={prop}]", value)
+ b.wait_in_text(f"{deployment} td[data-label={prop}]", value)
def wait_packages(b, index, packages):
@@ -83,8 +83,17 @@ def check_package_count(b, assertIn, index):
def do_deployment_action(b, index, action):
- wait_deployment_prop(b, index, "Actions", action)
- b.click(f"#available-deployments > tbody:nth-child({index + 1}) tr:nth-child(1) td:last-child button")
+ deployment_row = f"#available-deployments > tbody:nth-child({index + 1})"
+ if action in ["Pin", "Unpin", "Delete"]:
+ b.click(f"{deployment_row} tr:nth-child(1) td:last-child button.pf-v5-c-dropdown__toggle")
+ b.click(f"{deployment_row} #deployment-actions")
+ b.click(f"{deployment_row} #deployment-actions ul li:contains({action})")
+ else:
+ wait_deployment_prop(b, index, "Actions", action)
+ b.click(f"{deployment_row} tr:nth-child(1) td:nth-child(6) button")
+ b.wait_visible("#confirm-modal")
+ b.click("#confirm-modal button.pf-m-warning")
+ b.wait_not_present("#confirm-modal")
def ensure_remote_http_port(m, remote="local"):
@@ -161,6 +170,13 @@ def get_name(self):
return "fedora-coreos"
+def do_card_action(browser, kebab, action):
+ browser.wait_visible(f"{kebab} .pf-v5-c-dropdown__toggle")
+ browser.click(f"{kebab} .pf-v5-c-dropdown__toggle")
+ browser.wait_visible(f"{kebab} .pf-v5-c-dropdown__menu")
+ browser.click(f"{kebab} .pf-v5-c-dropdown__menu li:contains({action}) a")
+
+
class OstreeRestartCase(testlib.MachineCase):
def testOstree(self):
@@ -180,18 +196,19 @@ class OstreeRestartCase(testlib.MachineCase):
b.enter_page("/updates")
# Check current and rollback target
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} cockpit-base.1")
- wait_deployment_prop(b, 1, "State", "Running")
+ wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
+ wait_deployment_prop(b, 1, "Status", "Current")
wait_deployment_prop(b, 1, "Actions", "")
wait_deployment_details_prop(b, 1, "Tree", "#osname", get_name(self))
wait_deployment_details_prop(b, 1, "Tree", "#osversion", "cockpit-base.1")
- b.assert_pixels("#repo-remote-toolbar", "remote-toolbar")
+ b.assert_pixels("#ostree-status", "status")
+ b.assert_pixels("#ostree-source", "source")
b.assert_pixels("#available-deployments > tbody:nth-child(2)", "deployment",
ignore=[".timestamp",
# The columns change size dependent on the second deployment's name.
- "td[data-label=Name]", "td[data-label=State]"])
+ "td[data-label=Name]", "td[data-label=State]", "td[data-label='Time']"])
wait_packages(b, 1, {"rpms-col1": [chrony], "rpms-col2": [tzdata]})
wait_packages(b, 1, {"rpms-col2": [remove_pkg]})
@@ -202,15 +219,13 @@ class OstreeRestartCase(testlib.MachineCase):
# Require signatures
m.execute("sed -i /gpg-verify/d /etc/ostree/remotes.d/local.conf")
- b.wait_not_in_text("#available-deployments > tbody:nth-child(3) td[data-label=Name]", "cockpit")
- wait_deployment_prop(b, 2, "State", "Available")
- wait_deployment_prop(b, 2, "Actions", "Roll back and reboot")
+ b.wait_not_in_text("#available-deployments > tbody:nth-child(3) td[data-label=Version]", "cockpit")
+ wait_deployment_prop(b, 2, "Actions", "Roll back")
wait_deployment_details_prop(b, 2, "Tree", "#osname", get_name(self))
# Check for new commit, get error
- b.wait_in_text("#check-for-updates-btn", "Check for updates")
b.click("#check-for-updates-btn")
- b.wait_visible('#app .pf-v5-c-alert.pf-m-warning')
+ b.wait_visible('#app .pf-v5-c-alert.pf-m-danger')
b.wait_visible("#check-for-updates-btn:not(disabled)")
# Serve repo
@@ -234,9 +249,10 @@ class OstreeRestartCase(testlib.MachineCase):
# Check again have update data
b.click("#check-for-updates-btn")
- wait_deployment_prop(b, 1, "State", "Available")
- wait_deployment_prop(b, 1, "Actions", "Update and reboot")
- wait_deployment_prop(b, 2, "State", "Running")
+ b.wait_in_text("#ostree-status li:first-child > div > div:last-child", "Update available")
+ wait_deployment_prop(b, 1, "Status", "New")
+ wait_deployment_prop(b, 1, "Actions", "Update")
+ wait_deployment_prop(b, 2, "Status", "Current")
# Check update data
wait_deployment_details_prop(b, 1, "Tree", "#osname", get_name(self))
@@ -260,14 +276,15 @@ class OstreeRestartCase(testlib.MachineCase):
# Force an error
stop_trivial_httpd(m, server_pid)
- wait_deployment_prop(b, 1, "State", "Available")
- do_deployment_action(b, 1, "Update and reboot")
- wait_deployment_prop(b, 1, "State", "Failedview more...")
+ wait_deployment_prop(b, 1, "Status", "New")
+ do_deployment_action(b, 1, "Update")
+ with b.wait_timeout(60):
+ wait_deployment_prop(b, 1, "Status", "Failedview more...New")
server_pid = start_trivial_httpd(m)
# Apply update
- do_deployment_action(b, 1, "Update and reboot")
- wait_deployment_prop(b, 1, "State", "Updating")
+ do_deployment_action(b, 1, "Update")
+ wait_deployment_prop(b, 1, "Status", "Updating")
b.switch_to_top()
with b.wait_timeout(120):
@@ -280,9 +297,10 @@ class OstreeRestartCase(testlib.MachineCase):
b.reload()
b.login_and_go("/updates")
+ b.wait_in_text("#ostree-status li:first-child > div > div:last-child", "System is up to date")
# After reboot, check commit
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} cockpit-base.2")
- wait_deployment_prop(b, 1, "State", "Running")
+ wait_deployment_prop(b, 1, "Version", "cockpit-base.2")
+ wait_deployment_prop(b, 1, "Status", "Current")
wait_deployment_prop(b, 1, "Actions", "")
wait_packages(b, 1, {"rpms-col1": [INSTALL_RPMS[0], INSTALL_RPMS[1]],
"rpms-col2": [INSTALL_RPMS[2]],
@@ -299,9 +317,9 @@ class OstreeRestartCase(testlib.MachineCase):
b.wait_in_text(sel, "When")
# Check rollback target
- wait_deployment_prop(b, 2, "Name", f"{get_name(self)} cockpit-base.1")
- wait_deployment_prop(b, 2, "State", "Available")
- wait_deployment_prop(b, 2, "Actions", "Roll back and reboot")
+ wait_deployment_prop(b, 2, "Version", "cockpit-base.1")
+ wait_deployment_prop(b, 2, "Actions", "Roll back")
+ wait_deployment_prop(b, 2, "Status", "")
wait_deployment_details_prop(b, 2, "Tree", "#osname", get_name(self))
wait_packages(b, 2, {"down": [tzdata],
@@ -312,9 +330,9 @@ class OstreeRestartCase(testlib.MachineCase):
check_package_count(b, self.assertIn, 2)
# Rollback
- do_deployment_action(b, 2, "Roll back and reboot")
+ do_deployment_action(b, 2, "Roll back")
b.wait_visible("button:contains('Roll back')")
- wait_deployment_prop(b, 2, "State", "Updating")
+ wait_deployment_prop(b, 2, "Status", "Updating")
b.switch_to_top()
with b.wait_timeout(120):
@@ -326,12 +344,12 @@ class OstreeRestartCase(testlib.MachineCase):
b.reload()
b.login_and_go("/updates")
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} cockpit-base.1")
- wait_deployment_prop(b, 1, "State", "Running")
+ wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
+ wait_deployment_prop(b, 1, "Status", "Current")
- wait_deployment_prop(b, 2, "Name", f"{get_name(self)} cockpit-base.2")
- wait_deployment_prop(b, 2, "State", "Available")
- wait_deployment_prop(b, 2, "Actions", "Roll back and reboot")
+ wait_deployment_prop(b, 2, "Version", "cockpit-base.2")
+ wait_deployment_prop(b, 2, "Actions", "Roll back")
+ wait_deployment_prop(b, 2, "Status", "")
# Only two deployments
b.wait_not_present("#available-deployments > tbody:nth-child(4)")
@@ -355,30 +373,39 @@ class OstreeRestartCase(testlib.MachineCase):
b.login_and_go("/updates")
b.enter_page("/updates")
- b.wait_text("#change-repo", "local")
- b.wait_in_text("#change-branch", branch)
- # open the branches menu to see the entries
- b.click("#change-branch")
- b.wait_not_in_text("#change-branch + ul li:first-child button", "error")
- b.wait_in_text("#change-branch + ul li:last-of-type", "znew-branch")
- b.call_js_func("ph_count_check", "#change-branch + ul li", 2)
- b.click("#change-branch + ul li:last-of-type button")
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} cockpit-base.1")
- wait_deployment_prop(b, 1, "State", "Running")
+ b.wait_in_text("#current-repository", "local")
+ b.wait_in_text("#current-branch", branch)
+
+ # open rebase modal
+ do_card_action(b, "#ostree-source-actions", "Rebase")
+ b.wait_visible("#rebase-repository-modal")
+ b.wait_in_text("#change-repository .pf-v5-c-select__toggle-text", "local")
+ b.wait_in_text("#change-branch .pf-v5-c-select__toggle-text", branch)
+
+ # rebase to new branch
+ b.click("#change-branch button.pf-v5-c-select__toggle")
+ b.wait_not_in_text("#change-branch li:first-child button", "error")
+ b.wait_in_text("#change-branch li:last-of-type button", "znew-branch")
+ b.call_js_func("ph_count_check", "#change-branch li", 2)
+ b.click("#change-branch li:last-of-type button")
+ b.click("#rebase-repository-modal button.pf-m-primary")
+ b.wait_in_text("#current-repository", "local")
+ b.wait_in_text("#current-branch", "znew-branch")
+ wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
+ wait_deployment_prop(b, 1, "Status", "Current")
wait_deployment_details_prop(b, 1, "Tree", "#osorigin", f"local:{branch}")
- b.wait_in_text("#check-for-updates-btn", "Check for updates")
b.click("#check-for-updates-btn")
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} branch-version")
- wait_deployment_prop(b, 1, "State", "Available")
- wait_deployment_prop(b, 1, "Actions", "Rebase and reboot")
+ wait_deployment_prop(b, 1, "Version", "branch-version")
+ wait_deployment_prop(b, 1, "Status", "New")
+ wait_deployment_prop(b, 1, "Actions", "Rebase")
wait_deployment_details_prop(b, 1, "Tree", "#osname", get_name(self))
wait_deployment_details_prop(b, 1, "Tree", "#osorigin", "local:znew-branch")
# Apply update
- do_deployment_action(b, 1, "Rebase and reboot")
- wait_deployment_prop(b, 1, "State", "Updating")
+ do_deployment_action(b, 1, "Rebase")
+ wait_deployment_prop(b, 1, "Status", "Updating")
b.switch_to_top()
with b.wait_timeout(120):
@@ -391,8 +418,8 @@ class OstreeRestartCase(testlib.MachineCase):
b.login_and_go("/updates")
# After reboot, check commit
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} branch-version")
- wait_deployment_prop(b, 1, "State", "Running")
+ wait_deployment_prop(b, 1, "Version", "branch-version")
+ wait_deployment_prop(b, 1, "Status", "Current")
wait_deployment_details_prop(b, 1, "Tree", "#osorigin", "local:znew-branch")
self.allow_restart_journal_messages()
@@ -419,8 +446,8 @@ class OstreeRestartCase(testlib.MachineCase):
self.login_and_go("/updates")
# our image defaults to local OSTree repo
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} cockpit-base.1")
- wait_deployment_prop(b, 1, "State", "Running")
+ wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
+ wait_deployment_prop(b, 1, "Status", "Current")
wait_deployment_details_prop(b, 1, "Tree", "#osorigin", f"local:{branch}")
wait_packages(b, 1, {"rpms-col1": [bash_ver]})
@@ -429,16 +456,16 @@ class OstreeRestartCase(testlib.MachineCase):
m.execute("rpm-ostree rebase ostree-unverified-registry:localhost:5000/ostree-oci:cockpit1")
# UI picks this up
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} cockpit-base.1")
- wait_deployment_prop(b, 1, "State", "Available")
+ wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
+ wait_deployment_prop(b, 1, "Status", "")
# OCI deployments have no origin
wait_deployment_details_prop(b, 1, "Tree", "#osorigin", "")
wait_deployment_details_prop(b, 1, "Packages", ".same-packages",
"This deployment contains the same packages as your currently booted system")
# current deployment is now second
- wait_deployment_prop(b, 2, "Name", f"{get_name(self)} cockpit-base.1")
- wait_deployment_prop(b, 2, "State", "Running")
+ wait_deployment_prop(b, 2, "Version", "cockpit-base.1")
+ wait_deployment_prop(b, 2, "Status", "Current")
wait_deployment_details_prop(b, 2, "Tree", "#osorigin", f"local:{branch}")
wait_packages(b, 2, {"rpms-col1": [bash_ver]})
@@ -451,17 +478,121 @@ class OstreeRestartCase(testlib.MachineCase):
self.login_and_go("/updates")
# now the status is reversed: OCI deployment is running and has the package list
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} cockpit-base.1")
- wait_deployment_prop(b, 1, "State", "Running")
+ wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
+ wait_deployment_prop(b, 1, "Status", "Current")
wait_deployment_details_prop(b, 1, "Tree", "#osorigin", "")
wait_packages(b, 1, {"rpms-col1": [bash_ver]})
# ... and second deployment is the ostree repo one
- wait_deployment_prop(b, 2, "Name", f"{get_name(self)} cockpit-base.1")
- wait_deployment_prop(b, 2, "State", "Available")
+ wait_deployment_prop(b, 2, "Version", "cockpit-base.1")
+ wait_deployment_prop(b, 2, "Status", "")
wait_deployment_details_prop(b, 2, "Tree", "#osorigin", f"local:{branch}")
wait_deployment_details_prop(b, 2, "Packages", ".same-packages",
"This deployment contains the same packages as your currently booted system")
+ def testDeploymentManagement(self):
+ m = self.machine
+ b = self.browser
+
+ remove_pkg = m.execute("rpm -qa | grep socat").strip()
+
+ m.start_cockpit()
+ b.login_and_go("/updates")
+ b.enter_page("/updates")
+
+ # Check current and rollback deployment
+ wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
+ wait_deployment_prop(b, 1, "Status", "Current")
+ wait_deployment_prop(b, 1, "Actions", "")
+ wait_deployment_prop(b, 2, "Status", "")
+ wait_deployment_prop(b, 2, "Actions", "Roll back")
+
+ # Overlay a package
+ OVERLAY_RPM = "empty-1-0.noarch"
+ m.upload([f"files/{OVERLAY_RPM}.rpm"], "/home/admin/")
+ m.execute(f"rpm-ostree install -C /home/admin/{OVERLAY_RPM}.rpm")
+
+ # Deployment with overlay package
+ wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
+ wait_deployment_prop(b, 1, "Actions", "")
+ wait_deployment_prop(b, 1, "Status", "")
+ # Current deployment
+ wait_deployment_prop(b, 2, "Version", "cockpit-base.1")
+ wait_deployment_prop(b, 2, "Actions", "")
+ wait_deployment_prop(b, 2, "Status", "Current")
+
+ # Reboot to apply overlay package
+ m.reboot()
+ m.start_cockpit()
+ b.reload()
+ b.login_and_go("/updates")
+
+ # Deployment with overlay package
+ wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
+ wait_deployment_prop(b, 1, "Actions", "")
+ wait_deployment_prop(b, 1, "Status", "Current")
+ # Previous deployment
+ wait_deployment_prop(b, 2, "Version", "cockpit-base.1")
+ wait_deployment_prop(b, 2, "Actions", "Roll back")
+ wait_deployment_prop(b, 2, "Status", "")
+
+ wait_packages(b, 1, {"rpms-col1": [OVERLAY_RPM]})
+ do_card_action(b, "#deployments-actions", "Reset")
+ b.wait_visible("#reset-modal")
+ b.click("#remove-overlays-checkbox")
+ b.click("#reset-modal button.pf-m-warning")
+ b.wait_not_present("#reset-modal")
+
+ wait_deployment_details_prop(b, 1, "Packages", ".removes", "Removalsempty-1-0.noarch")
+
+ m.reboot()
+ m.start_cockpit()
+ b.reload()
+ b.login_and_go("/updates")
+ wait_not_packages(b, 1, [OVERLAY_RPM])
+ b.click("#available-deployments > tbody:nth-child(2) .pf-v5-c-table__toggle button")
+
+ # Generate new commit
+ generate_new_commit(m, remove_pkg)
+
+ b.click("#check-for-updates-btn")
+ wait_deployment_prop(b, 1, "Version", "cockpit-base.2")
+ wait_deployment_prop(b, 1, "Actions", "Update")
+ wait_deployment_prop(b, 1, "Status", "New")
+ b.call_js_func("ph_count_check", "#available-deployments tbody tr", 3)
+
+ wait_deployment_prop(b, 1, "Version", "cockpit-base.2")
+ wait_deployment_prop(b, 1, "Status", "New")
+ wait_deployment_prop(b, 1, "Actions", "Update")
+
+ # Pin rollback deployment
+ do_deployment_action(b, 3, "Pin")
+ wait_deployment_prop(b, 3, "Status", "Pinned")
+ # Kebab menu is not available for update
+ b.wait_not_present("""#available-deployments > tbody:nth-child(2) tr:nth-child(1)
+ td:last-child button.pf-v5-c-dropdown__toggle""")
+
+ # Use clean up to remove pending and rollback deployments
+ do_card_action(b, "#deployments-actions", "Clean up")
+ b.wait_visible("#cleanup-deployment-modal")
+ b.click("#pending-deployment-checkbox")
+ b.click("#rollback-deployment-checkbox")
+ b.click("#cleanup-deployment-modal button.pf-m-primary")
+ b.wait_not_present("#cleanup-deployment-modal")
+
+ # Rollback deployment wasn't deleted because it's pinned
+ b.call_js_func("ph_count_check", "#available-deployments tbody tr", 2)
+
+ # Manually delete base.1 deployment
+ do_deployment_action(b, 2, "Unpin")
+ b.wait_not_present("#available-deployments .pf-v5-c-dropdown__menu")
+ wait_deployment_prop(b, 2, "Version", "cockpit-base.1")
+ wait_deployment_prop(b, 2, "Status", "")
+ wait_deployment_prop(b, 2, "Actions", "Roll back")
+ do_deployment_action(b, 2, "Delete")
+
+ # only one available deployment
+ b.call_js_func("ph_count_check", "#available-deployments tbody tr", 1)
+
class OstreeCase(testlib.MachineCase):
def testRemoteManagement(self):
@@ -475,50 +606,55 @@ class OstreeCase(testlib.MachineCase):
b.login_and_go("/updates")
b.enter_page("/updates")
- b.wait_not_present('#app .pf-v5-c-alert.pf-m-warning')
- b.wait_in_text("#change-branch", branch)
+ b.wait_not_present('#ostree-status .pf-v5-u-warning-color-100')
+
+ do_card_action(b, "#ostree-source-actions", "Rebase")
+ b.wait_visible("#rebase-repository-modal")
+ b.wait_in_text(".pf-v5-c-modal-box h1", "Rebase repository and branch")
+ b.wait_in_text("#change-repository .pf-v5-c-select__toggle-text", "local")
# open the branches menu to see the entries
- b.click("#change-branch")
- b.wait_not_in_text("#change-branch + ul li:first-child button", "error")
- b.call_js_func("ph_count_check", "#change-branch + ul li", 1)
- b.wait_in_text("#change-branch + ul li", branch)
- b.click("#change-branch")
- b.wait_text("#change-repo", "local")
- b.click("#change-repo")
-
- b.wait_in_text(".pf-v5-c-modal-box h1", "Change repository")
- b.wait_in_text(".pf-v5-c-modal-box .pf-v5-c-simple-list .pf-m-current", "local")
- b.wait_in_text(".pf-v5-c-modal-box li:last-of-type", "Add new repository")
-
- b.click(".pf-v5-c-modal-box li:last-of-type button")
- b.click(".pf-v5-c-modal-box #new-gpg-verify")
- b.set_input_text(".pf-v5-c-modal-box #new-remote-url", "http://localhost:12344")
- b.set_input_text(".pf-v5-c-modal-box #new-remote-name", "zremote test")
- b.click(".pf-v5-c-modal-box #add-remote-btn")
- b.wait_in_text(".pf-v5-c-modal-box .pf-v5-c-simple-list__item-link .pf-m-danger", "Invalid remote name")
- b.set_input_text(".pf-v5-c-modal-box #new-remote-name", "zremote-test1")
+ b.wait_in_text("#change-branch div.pf-v5-c-form__group-control p", branch)
+ b.click("#rebase-repository-modal button.pf-m-link")
+
+ # Add new repository
+ do_card_action(b, "#ostree-source-actions", "Add repository")
+ b.wait_visible("#add-repository-modal")
+ b.set_input_text("#add-repository-modal #new-remote-name", "zremote test")
+ b.set_input_text("#add-repository-modal #new-remote-url", "http://localhost:12344")
+ b.click("#add-repository-modal #new-gpg-verify")
+ b.click("#add-repository-modal button.pf-m-primary")
+ b.wait_in_text("#add-repository-modal .pf-v5-c-alert.pf-m-danger", "Invalid remote name")
+ b.set_input_text("#add-repository-modal #new-remote-name", "zremote-test1")
b.assert_pixels(".pf-v5-c-modal-box", "dialog-add-repository")
- b.click(".pf-v5-c-modal-box #add-remote-btn")
-
- b.wait_not_present(".pf-v5-c-modal-box #new-remote-name")
- b.wait_not_present(".pf-v5-c-modal-box #add-remote-btn")
- b.wait_not_present(".pf-v5-c-modal-box .pf-v5-c-modal-box__footer button:disabled")
- b.wait_visible(".pf-v5-c-modal-box .pf-v5-c-modal-box__footer button.pf-m-primary")
- b.wait_in_text(".pf-v5-c-modal-box .pf-v5-c-simple-list .pf-m-current", "local")
- b.wait_in_text(".pf-v5-c-modal-box li:last-of-type button", "Add new repository")
- b.click(".pf-v5-c-modal-box #zremote-test1 a")
-
- b.click(".pf-v5-c-modal-box .pf-v5-c-modal-box__footer button.pf-m-primary")
- b.wait_not_present(".pf-v5-c-modal-box")
-
- b.wait_text("#change-repo", "zremote-test1")
- # Branch is still default
- b.wait_in_text("#change-branch", branch)
- # But can't list
- b.click("#change-branch")
- # Actual error message changes between versions
- b.wait_in_text("#change-branch + ul li button", "error: While fetching")
+ b.click("#add-repository-modal button.pf-m-primary")
+ b.wait_not_present("#add-repository-modal #new-remote-name")
+ b.wait_not_present("#add-repository-modal .pf-m-primary")
+
+ # Try to add repository with same name
+ do_card_action(b, "#ostree-source-actions", "Add repository")
+ b.wait_visible("#add-repository-modal")
+ b.set_input_text("#add-repository-modal #new-remote-name", "zremote-test1")
+ b.set_input_text("#add-repository-modal #new-remote-url", "http://localhost:12344")
+ b.click("#add-repository-modal button.pf-m-primary")
+ b.wait_in_text("#add-repository-modal .pf-v5-c-alert.pf-m-danger", 'already exists')
+ b.click("#add-repository-modal button.pf-m-link")
+ b.wait_not_present("#add-repository-modal #new-remote-name")
+ b.wait_not_present("#add-repository-modal .pf-m-primary")
+
+ # rebase to newly added remote
+ do_card_action(b, "#ostree-source-actions", "Rebase")
+ b.wait_visible("#rebase-repository-modal")
+ b.click("#change-repository button.pf-v5-c-select__toggle")
+ b.wait_in_text("#change-repository li#zremote-test1", "zremote-test1")
+ b.click("#change-repository li#zremote-test1 button")
+ b.wait_in_text("#change-branch .pf-v5-u-danger-color-200 > div:last-child", "While fetching")
+ b.click("#rebase-repository-modal button.pf-m-primary")
+ b.wait_not_present("#rebase-repository-modal")
+
+ b.wait_in_text("#current-repository", "zremote-test1")
+ b.wait_in_text("#current-branch", branch)
+ b.wait_in_text("#ostree-status li:last-child > div > div:last-child", "While fetching")
# Config created
self.assertEqual(m.execute("cat /etc/ostree/remotes.d/zremote-test1.conf").strip(),
@@ -529,8 +665,8 @@ class OstreeCase(testlib.MachineCase):
# Refresh goes back to default
b.reload()
b.enter_page("/updates")
- b.wait_text("#change-repo", "local")
- b.wait_in_text("#change-branch", branch)
+ b.wait_in_text("#current-repository", "local")
+ b.wait_in_text("#current-branch", branch)
# Create a new remote with commits, just use the rpm dir
zrepo = "/var/zrepo"
@@ -552,64 +688,69 @@ class OstreeCase(testlib.MachineCase):
"--add-metadata-string", "version=bad-version"], timeout=600)
m.execute(["ostree", "summary", f"--repo={REPO_LOCATION}", "-u"])
- # Edit
- b.click("#change-repo")
- b.click(".pf-v5-c-modal-box li:contains('zremote-test1') button.pf-m-secondary.edit-remote")
- b.wait_visible(".pf-v5-c-modal-box .pf-v5-c-modal-box__footer button:disabled")
- b.wait_visible(".pf-v5-c-modal-box #edit-remote-url[value='http://localhost:12344']")
- b.wait_visible(".pf-v5-c-modal-box .pf-v5-c-form #gpg-verify:checked")
- b.wait_not_present(".pf-v5-c-modal-box #gpg-data")
- b.click(".pf-v5-c-modal-box .pf-v5-c-form button.pf-m-secondary")
- b.wait_not_present(".pf-v5-c-modal-box .pf-v5-c-form button.pf-m-secondary")
- b.set_input_text(".pf-v5-c-modal-box #gpg-data", "bad")
- b.click(".pf-v5-c-modal-box .apply-btn")
- b.wait_visible(".pf-v5-c-modal-box div.pf-m-danger")
+ # Edit zremote-test1 repository
+ do_card_action(b, "#ostree-source-actions", "Edit repository")
+ b.wait_visible("#edit-repository-modal")
+ b.click("#edit-repository-modal #select-repository button.pf-v5-c-select__toggle")
+ b.click("#edit-repository-modal .pf-m-expanded li:last-of-type button")
+ b.wait_visible("#edit-repository-modal #edit-remote-url[value='http://localhost:12344']")
+ b.wait_visible("#edit-repository-modal #gpg-verify:checked")
+ b.wait_not_present("redit-repository-modal #gpg-data")
+ b.click("#edit-repository-modal button.pf-m-secondary")
+ b.wait_not_present("#edit-repository-modal button.pf-m-secondary")
+ # set invalid key
+ b.set_input_text("#edit-repository-modal #gpg-data", "bad")
+ b.click("#edit-repository-modal button.pf-m-primary")
+ b.wait_visible("#edit-repository-modal div.pf-m-danger")
with open(os.path.join(testlib.TEST_DIR, "files", "publickey.asc"), 'r') as fp:
gpg_data = fp.read()
- b.set_val(".pf-v5-c-modal-box #gpg-data", gpg_data)
- b.wait_val(".pf-v5-c-modal-box #gpg-data", gpg_data)
- b.focus(".pf-v5-c-modal-box #gpg-data")
+ # set valid key
+ b.set_val("#edit-repository-modal #gpg-data", gpg_data)
+ b.wait_val("#edit-repository-modal #gpg-data", gpg_data)
+ b.focus("#edit-repository-modal #gpg-data")
b.key_press("\b") # Backspace
- b.click(".pf-v5-c-modal-box .apply-btn")
- b.wait_not_present(".pf-v5-c-modal-box #apply-btn")
- b.wait_not_present(".pf-v5-c-modal-box .pf-v5-c-form")
- b.wait_visible(".pf-v5-c-modal-box .pf-v5-c-modal-box__footer button.pf-m-primary:not(disabled)")
+ b.click("#edit-repository-modal button.pf-m-primary")
+ b.wait_not_present("#edit-repository-modal")
m.execute("ls /sysroot/ostree/repo/zremote-test1.trustedkeys.gpg")
- b.click(".pf-v5-c-modal-box li:contains('zremote-test1') .edit-remote")
- b.wait_visible(".pf-v5-c-modal-box .pf-v5-c-modal-box__footer button.pf-m-primary:disabled")
- b.wait_visible(".pf-v5-c-modal-box .apply-btn")
- b.wait_visible(".pf-v5-c-modal-box #edit-remote-url[value='http://localhost:12344']")
- b.wait_visible(".pf-v5-c-modal-box #gpg-verify:checked")
- b.click(".pf-v5-c-modal-box #gpg-verify")
- b.click(".pf-v5-c-modal-box .apply-btn")
- b.wait_not_present(".pf-v5-c-modal-box .pf-v5-c-form")
- b.wait_not_present(".pf-v5-c-modal-box .pf-v5-c-modal-box__footer button:disabled")
- b.click(".pf-v5-c-modal-box li:contains('zremote-test1')")
- b.click(".pf-v5-c-modal-box .pf-v5-c-modal-box__footer button.pf-m-primary")
- b.wait_not_present(".pf-v5-c-modal-box")
-
- b.wait_text("#change-repo", "zremote-test1")
- b.wait_in_text("#change-branch", "zremote-branch1")
- b.click("#change-branch")
- b.wait_in_text("#change-branch + ul li:nth-child(1) button", "zremote-branch1")
- b.wait_in_text("#change-branch + ul li:nth-child(2) button", "zremote-branch2")
- b.call_js_func("ph_count_check", "#change-branch + ul li", 2)
+ # disable gpg verification
+ do_card_action(b, "#ostree-source-actions", "Edit repository")
+ b.wait_visible("#edit-repository-modal")
+ b.click("#edit-repository-modal #select-repository button.pf-v5-c-select__toggle")
+ b.click("#edit-repository-modal .pf-m-expanded li:last-of-type button")
+ b.wait_visible("#edit-repository-modal #edit-remote-url[value='http://localhost:12344']")
+ b.wait_visible("#edit-repository-modal #gpg-verify:checked")
+ b.click("#edit-repository-modal #gpg-verify")
+ b.click("#edit-repository-modal button.pf-m-primary")
+ b.wait_not_present("#edit-repository-modal")
+
+ # rebase to zremote-branch1
+ do_card_action(b, "#ostree-source-actions", "Rebase")
+ b.wait_visible("#rebase-repository-modal")
+ b.click("#rebase-repository-modal #change-repository button.pf-v5-c-select__toggle")
+ b.click("#rebase-repository-modal .pf-m-expanded li:last-of-type button")
+ b.wait_in_text("#change-repository .pf-v5-c-select__toggle-text", "zremote-test1")
+ b.wait_in_text("#change-branch .pf-v5-c-select__toggle-text", "zremote-branch1")
+ b.click("#rebase-repository-modal #change-branch button.pf-v5-c-select__toggle")
+ b.wait_in_text("#change-branch li:nth-child(1) button", "zremote-branch1")
+ b.wait_in_text("#change-branch li:nth-child(2) button", "zremote-branch2")
+ b.call_js_func("ph_count_check", "#change-branch li", 2)
+ b.click("#rebase-repository-modal button.pf-m-primary")
self.assertEqual(m.execute("cat /etc/ostree/remotes.d/zremote-test1.conf").strip(),
- '[remote "zremote-test1"]\nurl=http://localhost:12344\ngpg-verify = false')
+ '[remote "zremote-test1"]\nurl = http://localhost:12344\ngpg-verify = false')
# Check updates display
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} cockpit-base.1")
+ wait_deployment_prop(b, 1, "Version", "cockpit-base.1")
wait_deployment_details_prop(b, 1, "Tree", "#osorigin", f"local:{branch}")
b.click("#check-for-updates-btn")
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} zremote-branch1.1")
- wait_deployment_prop(b, 1, "State", "Available")
- wait_deployment_prop(b, 1, "Actions", "Rebase and reboot")
+ wait_deployment_prop(b, 1, "Version", "zremote-branch1.1")
+ wait_deployment_prop(b, 1, "Status", "New")
+ wait_deployment_prop(b, 1, "Actions", "Rebase")
wait_deployment_details_prop(b, 1, "Tree", "#osname", get_name(self))
wait_deployment_details_prop(b, 1, "Tree", "#osorigin", "zremote-test1:zremote-branch1")
@@ -617,60 +758,85 @@ class OstreeCase(testlib.MachineCase):
"This deployment contains the same packages as your currently booted system")
# Switching back shows pulled
- b.click("#change-branch")
- b.wait_in_text("#change-branch + ul li:first-of-type button", "zremote-branch1")
- b.click("#change-branch + ul li:first-of-type button")
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} zremote-branch1.1")
+ do_card_action(b, "#ostree-source-actions", "Rebase")
+ b.wait_visible("#rebase-repository-modal")
+ b.click("#rebase-repository-modal #change-branch button.pf-v5-c-select__toggle")
+ b.wait_in_text("#rebase-repository-modal #change-branch li:first-child button", "zremote-branch1")
+ b.click("#rebase-repository-modal #change-branch li:first-child button")
+ wait_deployment_prop(b, 1, "Version", "zremote-branch1.1")
# Refresh, back to local, pull in update
b.reload()
b.enter_page("/updates")
- b.wait_in_text("#change-branch", branch)
+ b.wait_in_text("#current-branch", branch)
b.click("#check-for-updates-btn")
- wait_deployment_prop(b, 1, "Actions", "Update and reboot")
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} bad-version")
+ wait_deployment_prop(b, 1, "Version", "bad-version")
+ wait_deployment_prop(b, 1, "Actions", "Update")
# Switching to branch shows pulled
- b.wait_text("#change-repo", "local")
- b.click("#change-repo")
- b.wait_visible(".pf-v5-c-modal-box .pf-v5-c-simple-list")
- b.click(".pf-v5-c-modal-box #zremote-test1 a")
- b.click(".pf-v5-c-modal-box .pf-v5-c-modal-box__footer button.pf-m-primary")
- b.wait_not_present(".pf-v5-c-modal-box")
-
- b.wait_text("#change-repo", "zremote-test1")
- b.wait_in_text("#change-branch", "zremote-branch1")
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} zremote-branch1.1")
- wait_deployment_prop(b, 1, "State", "Available")
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} zremote-branch1.1")
-
- # delete
- b.click("#change-repo")
- b.wait_in_text(".pf-v5-c-modal-box #zremote-test1", "zremote-test1")
- b.click(".pf-v5-c-modal-box #zremote-test1 .edit-remote")
- b.wait_visible(".pf-v5-c-modal-box button.pf-m-danger")
- b.wait_visible(".pf-v5-c-modal-box .pf-v5-c-modal-box__footer button.pf-m-primary:disabled")
- b.wait_visible(".pf-v5-c-modal-box #edit-remote-url[value='http://localhost:12344']")
- b.wait_visible("#gpg-verify")
- b.wait_visible(".pf-v5-c-modal-box .pf-v5-c-form #gpg-verify:not(:checked)")
- b.click(".pf-v5-c-modal-box button.pf-m-danger")
- b.wait_not_present(".pf-v5-c-modal-box .pf-v5-c-form")
-
- b.wait_not_in_text(".pf-v5-c-modal-box .pf-v5-c-simple-list", "zremote-test1")
- b.wait_in_text(".pf-v5-c-modal-box .pf-v5-c-simple-list", "local")
- b.wait_not_present(".pf-v5-c-modal-box .pf-v5-c-simple-list .pf-m-current")
- b.wait_visible(".pf-v5-c-modal-box .pf-v5-c-modal-box__footer button:disabled")
- b.click(".pf-v5-c-modal-box #local a")
- b.wait_visible(".pf-v5-c-modal-box .pf-v5-c-simple-list .pf-m-current")
- b.click(".pf-v5-c-modal-box .pf-v5-c-modal-box__footer button.pf-m-primary")
- b.wait_not_present(".pf-v5-c-modal-box")
- b.wait_text("#change-repo", "local")
- b.wait_in_text("#change-branch", branch)
- b.click("#change-branch")
- b.wait_not_in_text("#change-branch + ul li:first-child button", "error")
- b.call_js_func("ph_count_check", "#change-branch + ul li", 1)
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} bad-version")
- wait_deployment_prop(b, 1, "State", "Available")
+ b.wait_in_text("#current-repository", "local")
+ do_card_action(b, "#ostree-source-actions", "Rebase")
+ b.wait_visible("#rebase-repository-modal")
+ b.wait_in_text("#rebase-repository-modal #change-repository .pf-v5-c-select__toggle-text", "local")
+ b.click("#rebase-repository-modal #change-repository button.pf-v5-c-select__toggle")
+ b.click("#rebase-repository-modal .pf-m-expanded li:last-of-type button")
+ b.wait_in_text("#rebase-repository-modal #change-repository .pf-v5-c-select__toggle-text", "zremote-test1")
+ b.wait_in_text("#rebase-repository-modal #change-branch .pf-v5-c-select__toggle-text", "zremote-branch1")
+ b.click("#rebase-repository-modal button.pf-m-primary")
+ b.wait_not_present("#rebase-repository-modal")
+
+ b.wait_in_text("#current-repository", "zremote-test1")
+ b.wait_in_text("#current-branch", "zremote-branch1")
+ wait_deployment_prop(b, 1, "Version", "zremote-branch1.1")
+ wait_deployment_prop(b, 1, "Status", "New")
+
+ # Remove zremote-test1 repository
+ do_card_action(b, "#ostree-source-actions", "Remove repository")
+ b.wait_visible("#remove-repository-modal")
+ b.call_js_func("ph_count_check", "#remove-repository-modal .pf-v5-c-form__group-control input", 4)
+ # Current repository checkbox is disabled
+ b.wait_visible("#remove-repository-modal #zremote-test1 + label.pf-m-disabled")
+ b.click("#remove-repository-modal button.pf-m-link")
+
+ # Change back to local repository
+ do_card_action(b, "#ostree-source-actions", "Rebase")
+ b.wait_visible("#rebase-repository-modal")
+ b.click("#rebase-repository-modal #change-repository button.pf-v5-c-select__toggle")
+ b.click("#rebase-repository-modal .pf-m-expanded li button:contains('local')")
+ b.wait_in_text("#rebase-repository-modal #change-repository .pf-v5-c-select__toggle-text", "local")
+ b.wait_in_text("#rebase-repository-modal #change-branch", branch)
+ b.click("#rebase-repository-modal button.pf-m-primary")
+ b.wait_not_present("#rebase-repository-modal")
+
+ # Remove zremote-test1 repository
+ do_card_action(b, "#ostree-source-actions", "Remove repository")
+ b.wait_visible("#remove-repository-modal")
+ b.call_js_func("ph_count_check", "#remove-repository-modal .pf-v5-c-form__group-control input", 4)
+ b.wait_in_text("#remove-repository-modal #zremote-test1 + label", "zremote-test1")
+
+ # Remove button is disabled when nothing is selected
+ b.wait_visible("#remove-repository-modal button.pf-m-danger[aria-disabled='true']")
+
+ b.click("#remove-repository-modal #zremote-test1")
+ b.click("#remove-repository-modal button.pf-m-danger[aria-disabled='false']")
+ b.wait_not_present("#remove-repository-modal")
+
+ # Verify that the repository is gone
+ do_card_action(b, "#ostree-source-actions", "Rebase")
+ b.wait_visible("#rebase-repository-modal")
+ b.click("#rebase-repository-modal #change-repository button.pf-v5-c-select__toggle")
+ b.call_js_func("ph_count_check", "#rebase-repository-modal #change-repository li", 3)
+ b.wait_in_text("#rebase-repository-modal .pf-m-expanded button.pf-m-selected", "local")
+ b.click("#rebase-repository-modal #change-repository button.pf-v5-c-select__toggle")
+ b.wait_in_text("#change-branch div.pf-v5-c-form__group-control p", branch)
+
+ b.click("#rebase-repository-modal button.pf-m-primary")
+ b.wait_not_present("#rebase-repository-modal")
+
+ b.wait_in_text("#current-repository", "local")
+ b.wait_in_text("#current-branch", branch)
+ wait_deployment_prop(b, 1, "Version", "bad-version")
+ wait_deployment_prop(b, 1, "Status", "New")
wait_deployment_details_prop(b, 1, "Tree", "#osorigin", f"local:{branch}")
@testlib.nondestructive
@@ -725,7 +891,7 @@ class OstreeCase(testlib.MachineCase):
# updates page sees the new version
b.click("#check-for-updates-btn")
- wait_deployment_prop(b, 1, "Name", f"{get_name(self)} cockpit-base.2")
+ wait_deployment_prop(b, 1, "Version", "cockpit-base.2")
# overview page notices the new version as well
b.go("/system")
diff --git a/test/reference b/test/reference
index 97c368f7..38f271ec 160000
--- a/test/reference
+++ b/test/reference
@@ -1 +1 @@
-Subproject commit 97c368f7780f680abceb7a8a9868281d7b9226dc
+Subproject commit 38f271ecb45797cae3c57c4829f50446166d1dbd