Accepting request 1247655 from systemsmanagement:cockpit

- Always apply 0007-Remove-DynamicUser-setting-as-these-conflict-with-re.patch
  for every build system. Fixes bsc#1237451

- Add functionality to cockpit-packagekit that allows selecting what updates
  should be applied
- Added packagekit-single-install.patch file that adds this functionality

OBS-URL: https://build.opensuse.org/request/show/1247655
OBS-URL: https://build.opensuse.org/package/show/openSUSE:Factory/cockpit?expand=0&rev=53
This commit is contained in:
Dominique Leuenberger 2025-02-22 18:04:24 +00:00 committed by Git OBS Bridge
commit 57aed05ea4
5 changed files with 394 additions and 7 deletions

View File

@ -1,5 +1,5 @@
mtime: 1739521267
commit: ba3323415857354d7a3517f77ac133fcd740ab3611acf123400d5cd9900a70f8
mtime: 1740129393
commit: de4b8becd9c83bea352cf5f0f707807082e022a1e5473186a39cb1c5d58e2013
url: https://src.opensuse.org/cockpit/cockpit.git
revision: ba3323415857354d7a3517f77ac133fcd740ab3611acf123400d5cd9900a70f8
projectscmsync: https://src.opensuse.org/cockpit/_ObsPrj.git#0920f033fc9b63d4acd622ef39ae7f6faab099134f58a645d8c8a1f2c1a6b912
revision: de4b8becd9c83bea352cf5f0f707807082e022a1e5473186a39cb1c5d58e2013
projectscmsync: https://src.opensuse.org/cockpit/_ObsPrj.git#64e9cfa9f6042fa7adfdd4b37913eb3942c31e4934277b1b3dd1de2176db4f01

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a38c0e7d7077887fbd767316dbb1727736b9ed522862fa2c0c006e2e42f4318
oid sha256:3bf7304920353eb3a6329abecbbaff3c30aae7147cf5408f58e23129a93f73bf
size 256

View File

@ -1,3 +1,16 @@
-------------------------------------------------------------------
Fri Feb 21 08:03:00 UTC 2025 - Alice Brooks <alice.brooks@suse.com>
- Always apply 0007-Remove-DynamicUser-setting-as-these-conflict-with-re.patch
for every build system. Fixes bsc#1237451
-------------------------------------------------------------------
Thu Feb 20 22:22:32 UTC 2025 - Miika Alikirri <miika.alikirri@suse.com>
- Add functionality to cockpit-packagekit that allows selecting what updates
should be applied
- Added packagekit-single-install.patch file that adds this functionality
-------------------------------------------------------------------
Fri Feb 7 09:24:33 UTC 2025 - Alice Brooks <alice.brooks@suse.com>
@ -52,6 +65,7 @@ Mon Nov 25 06:18:44 UTC 2024 - Luna D Dragon <luna.dragon@suse.com>
* 323:
- login: Prevent multiple logins in a single browser session
- Update documentation links
-------------------------------------------------------------------
Wed Oct 9 12:14:14 UTC 2024 - Alice Brooks <alice.brooks@suse.com>

View File

@ -76,6 +76,7 @@ Patch108: 0007-Remove-DynamicUser-setting-as-these-conflict-with-re.patch
Patch103: 0004-leap-gnu18-removal.patch
Patch104: selinux_libdir.patch
Patch105: fix-libexecdir.patch
Patch106: packagekit-single-install.patch
Patch201: remove_rh_links.patch
@ -212,7 +213,8 @@ BuildRequires: python3-pytest-timeout
%patch -P 3 -p1
%patch -P 4 -p1
%patch -P 5 -p1
%patch -P 106 -p1
%patch -P 108 -p1
# SLE Micro specific patches
%if 0%{?is_smo}
@ -228,7 +230,6 @@ BuildRequires: python3-pytest-timeout
%patch -P 103 -p1
%patch -P 104 -p1
%patch -P 105 -p1
%patch -P 108 -p1
%else
%patch -P 107 -p1
%endif

View File

@ -0,0 +1,372 @@
From 291aba8127cb3e44e82b164e75d1063f3ae7fe9c Mon Sep 17 00:00:00 2001
From: Miika Alikirri <miika.alikirri@suse.com>
Date: Wed, 29 Jan 2025 14:04:39 +0200
Subject: pkg/pacagekit: Update individual packages
Ability to select individual packages allows more control for updates.
The exact behavior is distrobution specific. For example, on tumbleweed
packagekit backend will ignore the list of packages and run "zypper dup"
instead.
The selection of individual packages is implemented by using a context
provider and a reducer to make the UI updates snappy. A more naive
approach that requires rendering the whole list of packages will freeze
up the UI for multiple seconds when there's hundreds of packages. And
tens of seconds when there are thousands of packages.
---
pkg/packagekit/updates.jsx | 239 ++++++++++++++++++++++++++++++++----
pkg/packagekit/updates.scss | 6 +-
2 files changed, 219 insertions(+), 26 deletions(-)
diff --git a/pkg/packagekit/updates.jsx b/pkg/packagekit/updates.jsx
index 1feb57a0f..4fb68847f 100644
--- a/pkg/packagekit/updates.jsx
+++ b/pkg/packagekit/updates.jsx
@@ -77,6 +77,7 @@ import * as python from "python.js";
import callTracerScript from './callTracer.py';
import "./updates.scss";
+import { Checkbox } from '@patternfly/react-core';
const _ = cockpit.gettext;
@@ -90,6 +91,7 @@ const UPDATES = {
ALL: 0,
SECURITY: 1,
KPATCHES: 2,
+ SELECTED: 3,
};
function init() {
@@ -114,6 +116,196 @@ function init() {
PK_STATUS_LOG_STRINGS[PK.Enum.STATUS_SIGCHECK] = _("Verified");
}
+/**
+ * @typedef SelecetedState
+ * @type {object}
+ * @property {boolean} allSelected - Are all items selected
+ * @property {Object.<string, boolean>} selected - (Un)selected items.
+ * If allSelected is set, this refers to unselecetd
+ */
+
+/**
+ * @typedef SelecetedAction
+ * @type {object}
+ * @property {"ADD" | "REMOVE" | "ALL" | "NONE"} type - Type of reducer action
+ * @property {string=} id - Added removed item, only used by "ADD" and "REMOVE"
+ */
+
+const SelectedContext = React.createContext({selected: {}, allSelected: true});
+
+const SelectedStore = props => {
+
+ /**
+ * @argument {SelecetedState} state
+ * @argument {SelecetedAction} action
+ */
+ const reducer = (state, action) => {
+ switch (action.type) {
+ case "ADD":
+ if (action.id) {
+ if (state.allSelected)
+ delete state.selected[action.id];
+ else
+ state.selected[action.id] = true;
+ }
+ break;
+ case "REMOVE":
+ if (action.id) {
+ if (state.allSelected)
+ state.selected[action.id] = true;
+ else
+ delete state.selected[action.id];
+ }
+ break;
+ case "ALL":
+ state.allSelected = true;
+ state.selected = {};
+ break;
+ case "NONE":
+ state.allSelected = false;
+ state.selected = {};
+ break;
+ default:
+ break;
+ }
+
+ return {...state};
+ }
+
+ const [state, dispatch] = React.useReducer(reducer, {selected: {}, allSelected: true});
+
+ return <SelectedContext.Provider value={{ state, dispatch }} {...props} />;
+};
+
+/**
+ * @returns {{state: SelecetedState, dispatch: (arg: SelecetedAction) => void}}
+ */
+export const useSelected = () => React.useContext(SelectedContext);
+
+/**
+ * @param {{
+ * onClick: (state: SelecetedState) => void
+ * updates: string[],
+ * num_updates: number
+ * }} props;
+ */
+const SelectedButton = (props) => {
+ const { state, dispatch } = useSelected();
+ const {
+ onClick,
+ updates,
+ num_updates,
+ } = props;
+
+
+ const buttonText = () => {
+ if (state.allSelected && Object.keys(state.selected).length == 0 ||
+ !state.allSelected && Object.keys(state.selected).length == num_updates)
+ return _("Install all updates");
+
+ const selectLen = calculateSelected(updates, state).length;
+ return `${_("Install selected updates")} (${selectLen})`;
+ }
+
+ return (
+ <Button isDisabled={calculateSelected(updates, state).length === 0} id="install-all" variant="primary" onClick={ () => {onClick(state); dispatch({type: "ALL"})} }>
+ {buttonText()}
+ </Button>
+ );
+}
+
+const SelectedAllButton = (props) => {
+ const { state, dispatch } = useSelected();
+
+ const dispatchSelect = () => {
+ if (state.allSelected) {
+ dispatch({type: "NONE"});
+ } else {
+ dispatch({type: "ALL"});
+ }
+ }
+
+ return (
+ <Button id="install-selected" variant="secondary" onClick={ () => dispatchSelect() }>
+ {state.allSelected ? _("Unselect all") : _("Select all") }
+ </Button>
+ );
+}
+
+const SelectedSwitch = (props) => {
+ const { state, dispatch } = useSelected();
+
+ const dispatchChecked = checked => {
+ if (checked) {
+ dispatch({type: "ADD", id: props.id});
+ } else {
+ dispatch({type: "REMOVE", id: props.id});
+ }
+ }
+
+ const isChecked = () => {
+ if (state.allSelected) {
+ return !!!state.selected[props.id];
+ } else {
+ return !!state.selected[props.id];
+ }
+ }
+
+ return (
+ <Checkbox aria-label="select-update-checkbox" isChecked={isChecked()} id={`selectable-${props.id}`} onChange={(_event, checked) => dispatchChecked(checked)} />
+ );
+}
+
+/**
+ * @param {{
+* updates: string[],
+* }} props;
+*/
+const WebConsoleRestartWarn = (props) => {
+ const { state } = useSelected();
+
+ if (calculateSelected(props.updates, state).findIndex((value) => value.includes("cockpit-ws")) === -1)
+ return null;
+
+ return (
+ <Flex flex={{ default: 'inlineFlex' }} className="cockpit-update-warning">
+ <FlexItem>
+ <ExclamationTriangleIcon className="ct-icon-exclamation-triangle cockpit-update-warning-icon" />
+ <strong className="cockpit-update-warning-text">
+ <span className="pf-screen-reader">{_("Danger alert:")}</span>
+ {_("Web Console will restart")}
+ </strong>
+ </FlexItem>
+ <FlexItem>
+ <Popover aria-label="More information popover"
+ bodyContent={_("When the Web Console is restarted, you will no longer see progress information. However, the update process will continue in the background. Reconnect to continue watching the update process.")}>
+ <Button variant="link" isInline>{_("More info...")}</Button>
+ </Popover>
+ </FlexItem>
+ </Flex>
+ );
+}
+
+/**
+ * @param {string[]} allIds
+ * @param {SelecetedState} state
+ * @returns {string[]}
+ */
+function calculateSelected(allIds, state) {
+ const selected = Object.keys(state.selected);
+
+ if (!state.allSelected) {
+ return selected;
+ }
+
+ if (selected.length === 0) {
+ return allIds;
+ }
+
+ return allIds.filter((id) => !!!state.selected[id]);
+}
+
+
// parse CVEs from an arbitrary text (changelog) and return URL array
function parseCVEs(text) {
if (!text)
@@ -398,6 +590,7 @@ function updateItem(remarkable, info, pkgNames, key) {
{ title: <TableText wrapModifier="truncate">{info.version}</TableText>, props: { className: "version" } },
{ title: <TableText wrapModifier="nowrap">{type}</TableText>, props: { className: "type" } },
{ title: descriptionFirstLine, props: { className: "changelog" } },
+ { title: <SelectedSwitch id={ key }/>, props: { className: "select-update" } },
],
props: {
key,
@@ -448,6 +641,7 @@ const UpdatesList = ({ updates }) => {
{ title: _("Version"), transforms: [cellWidth(15)] },
{ title: _("Severity"), transforms: [cellWidth(15)] },
{ title: _("Details"), transforms: [cellWidth(30)] },
+ { title: _("Select update") },
]}
rows={update_ids.map(id => updateItem(remarkable, updates[id], packageNames[id].sort((a, b) => a.name > b.name), id))} />
);
@@ -933,25 +1127,12 @@ class CardsPage extends React.Component {
id: "available-updates",
title: _("Available updates"),
actions: (<div className="pk-updates--header--actions">
- {this.props.cockpitUpdate &&
- <Flex flex={{ default: 'inlineFlex' }} className="cockpit-update-warning">
- <FlexItem>
- <ExclamationTriangleIcon className="ct-icon-exclamation-triangle cockpit-update-warning-icon" />
- <strong className="cockpit-update-warning-text">
- <span className="pf-screen-reader">{_("Danger alert:")}</span>
- {_("Web Console will restart")}
- </strong>
- </FlexItem>
- <FlexItem>
- <Popover aria-label="More information popover"
- bodyContent={_("When the Web Console is restarted, you will no longer see progress information. However, the update process will continue in the background. Reconnect to continue watching the update process.")}>
- <Button variant="link" isInline>{_("More info...")}</Button>
- </Popover>
- </FlexItem>
- </Flex>}
+ <WebConsoleRestartWarn updates={Object.keys(this.props.updates)} />
{this.props.applyKpatches}
{this.props.applySecurity}
{this.props.applyAll}
+ {this.props.applySelected}
+ {this.props.applySelectAll}
</div>),
containsList: true,
body: <UpdatesList updates={this.props.updates} />
@@ -1325,13 +1506,19 @@ class OsUpdates extends React.Component {
});
}
- applyUpdates(type) {
+ /**
+ * @param {SelecetedState=} selected
+ */
+ applyUpdates(type, selected) {
let ids = Object.keys(this.state.updates);
if (type === UPDATES.SECURITY)
ids = ids.filter(id => this.state.updates[id].severity === PK.Enum.INFO_SECURITY);
if (type === UPDATES.KPATCHES) {
ids = ids.filter(id => isKpatchPackage(this.state.updates[id].name));
}
+ if (type === UPDATES.SELECTED && selected) {
+ ids = calculateSelected(ids, selected);
+ }
PK.transaction()
.then(transactionPath => {
@@ -1354,7 +1541,7 @@ class OsUpdates extends React.Component {
}
renderContent() {
- let applySecurity, applyKpatches, applyAll;
+ let applySecurity, applyKpatches, applyAll, applySelected, applySelectAll;
/* On unregistered RHEL systems we need some heuristics: If the "main" OS repos (which provide coreutils) require
* a subscription, then point this out and don't show available updates, even if there are some auxiliary
@@ -1409,12 +1596,8 @@ class OsUpdates extends React.Component {
const num_kpatches = count_kpatch_updates(this.state.updates);
const highest_severity = find_highest_severity(this.state.updates);
- applyAll = (
- <Button id={num_updates == num_security_updates ? "install-security" : "install-all"} variant="primary" onClick={ () => this.applyUpdates(UPDATES.ALL) }>
- { num_updates == num_security_updates
- ? _("Install security updates")
- : _("Install all updates") }
- </Button>);
+ applySelected = <SelectedButton updates={Object.keys(this.state.updates)} num_updates={num_updates} onClick={ (items) => this.applyUpdates(UPDATES.SELECTED, items) }/>;
+ applySelectAll = <SelectedAllButton />;
if (num_security_updates > 0 && num_updates > num_security_updates) {
applySecurity = (
@@ -1455,6 +1638,8 @@ class OsUpdates extends React.Component {
<CardsPage handleRefresh={this.handleRefresh}
applySecurity={applySecurity}
applyAll={applyAll}
+ applySelected={applySelected}
+ applySelectAll={applySelectAll}
applyKpatches={applyKpatches}
highestSeverity={highest_severity}
onValueChanged={this.onValueChanged}
@@ -1645,5 +1830,9 @@ document.addEventListener("DOMContentLoaded", () => {
document.title = cockpit.gettext(document.title);
init();
const root = createRoot(document.getElementById('app'));
- root.render(<OsUpdates />);
+ root.render(
+ <SelectedStore>
+ <OsUpdates />
+ </SelectedStore>
+ );
});
diff --git a/pkg/packagekit/updates.scss b/pkg/packagekit/updates.scss
index 00718eff2..12bc5de2b 100644
--- a/pkg/packagekit/updates.scss
+++ b/pkg/packagekit/updates.scss
@@ -72,7 +72,7 @@
}
&, p {
- max-inline-size: 60vw;
+ max-inline-size: 54vw;
margin-block-end: 0; // counter-act <Markdown>
overflow: hidden;
text-overflow: ellipsis;
@@ -273,3 +273,7 @@ table.header-buttons {
.ct-info-circle {
color: var(--pf-v5-global--info-color--100);
}
+
+td.select-update {
+ min-width: 8vw;
+}
--
2.48.1