mirror of
https://github.com/cockpit-project/cockpit.git
synced 2026-11-03 21:49:50 -09:00
pkg/packagekit: Allow updating individual packages
Ability to select individual updates, making updating more flexible. The selection of individual updates is implemented by using a context provider and a simple reducer to make UI updates more snappy. A more naive approach that requires re-rendering the whole state every time update is (un)selected will freeze up the UI for multiple seconds if there are hundreds of updates. And for tens of seconds if there are thousands of updates.
This commit is contained in:
parent
8d9bd7e3dd
commit
4c9541e0f8
2 changed files with 287 additions and 20 deletions
|
|
@ -63,6 +63,7 @@ import { read_os_release } from "os-release.js";
|
||||||
import callTracerScript from './callTracer.py';
|
import callTracerScript from './callTracer.py';
|
||||||
|
|
||||||
import "./updates.scss";
|
import "./updates.scss";
|
||||||
|
import { Checkbox } from '@patternfly/react-core/dist/esm/components/Checkbox/index.js';
|
||||||
import { Truncate } from '@patternfly/react-core/dist/esm/components/Truncate/index.js';
|
import { Truncate } from '@patternfly/react-core/dist/esm/components/Truncate/index.js';
|
||||||
import { Severity, TransactionExitStatus, UpdateProgressType, UpdateError } from '_internal/packagemanager-abstract';
|
import { Severity, TransactionExitStatus, UpdateProgressType, UpdateError } from '_internal/packagemanager-abstract';
|
||||||
import { getPackageManager } from 'packagemanager';
|
import { getPackageManager } from 'packagemanager';
|
||||||
|
|
@ -79,8 +80,25 @@ const UPDATES = {
|
||||||
ALL: 0,
|
ALL: 0,
|
||||||
SECURITY: 1,
|
SECURITY: 1,
|
||||||
KPATCHES: 2,
|
KPATCHES: 2,
|
||||||
|
SELECTED: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PackageKit doesn't provide a way to determine what backends support
|
||||||
|
* partial updates, so we need to manually add the ones we know has partial
|
||||||
|
* package support
|
||||||
|
*/
|
||||||
|
const SUPPORTED_PARTIAL_UPDATE_BACKEND = [
|
||||||
|
"apt",
|
||||||
|
"dnf",
|
||||||
|
"dnf5",
|
||||||
|
"zypp",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function is_partial_updates_supported(backend) {
|
||||||
|
return SUPPORTED_PARTIAL_UPDATE_BACKEND.includes(backend);
|
||||||
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
STATE_HEADINGS.loading = _("Loading available updates, please wait...");
|
STATE_HEADINGS.loading = _("Loading available updates, please wait...");
|
||||||
STATE_HEADINGS.locked = _("Some other program is currently using the package manager, please wait...");
|
STATE_HEADINGS.locked = _("Some other program is currently using the package manager, please wait...");
|
||||||
|
|
@ -103,9 +121,147 @@ function init() {
|
||||||
STATUS_LOG_STRINGS[UpdateProgressType.SIGCHECK] = _("Verified");
|
STATUS_LOG_STRINGS[UpdateProgressType.SIGCHECK] = _("Verified");
|
||||||
}
|
}
|
||||||
|
|
||||||
const WebConsoleRestartWarn = () => {
|
/** @import { UpdateDetail } from "_internal/packagemanager-abstract" */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef SelectedState
|
||||||
|
* @type {object}
|
||||||
|
* @property {Object.<string, UpdateDetail | null | undefined>} selected - (Un)selected updates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef SelectedAction
|
||||||
|
* @type {object}
|
||||||
|
* @property {"ADD" | "REMOVE" | "RESET"} type - Type of reducer action
|
||||||
|
* @property {UpdateDetail[]=} updates - Added/removed items, only used by "ADD" and "REMOVE"
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SelectedContext = React.createContext({ selected: {} });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selected store for conveniently updating the state of selected items.
|
||||||
|
* Updating all items naively causes performance issues if there are hundreds of updates
|
||||||
|
*/
|
||||||
|
const SelectedStore = props => {
|
||||||
|
/**
|
||||||
|
* @argument {SelectedState} state
|
||||||
|
* @argument {SelectedAction} action
|
||||||
|
*/
|
||||||
|
const reducer = (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "ADD":
|
||||||
|
if (action.updates) {
|
||||||
|
for (const update of action.updates) {
|
||||||
|
state.selected[update.id] = update;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "REMOVE":
|
||||||
|
if (action.updates) {
|
||||||
|
for (const update of action.updates) {
|
||||||
|
delete state.selected[update.id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "RESET":
|
||||||
|
state.selected = {};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...state };
|
||||||
|
};
|
||||||
|
|
||||||
|
const [state, dispatch] = React.useReducer(reducer, { selected: {} });
|
||||||
|
|
||||||
|
return <SelectedContext.Provider value={{ state, dispatch }} {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {{state: SelectedState, dispatch: (arg: SelectedAction) => void}}
|
||||||
|
*/
|
||||||
|
export const useSelected = () => React.useContext(SelectedContext);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{
|
||||||
|
* onClick: (state: SelectedState) => void
|
||||||
|
* securityOnly: boolean
|
||||||
|
* num_updates: number
|
||||||
|
* }} props;
|
||||||
|
*/
|
||||||
|
const SelectedButton = (props) => {
|
||||||
|
const { state, dispatch } = useSelected();
|
||||||
|
const {
|
||||||
|
onClick,
|
||||||
|
securityOnly,
|
||||||
|
num_updates,
|
||||||
|
} = props;
|
||||||
|
const buttonId = securityOnly ? "install-security" : "install-all";
|
||||||
|
|
||||||
|
const buttonText = () => {
|
||||||
|
const selectedCount = Object.keys(state.selected).length;
|
||||||
|
if (selectedCount === 0 || selectedCount === num_updates) {
|
||||||
|
if (securityOnly) {
|
||||||
|
return _("Install security updates");
|
||||||
|
} else {
|
||||||
|
return _("Install all updates");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return `${_("Install selected updates")} (${selectedCount})`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button id={buttonId} ouiaId={buttonId} variant="primary" onClick={ () => { onClick(state); dispatch({ type: "RESET" }) } }>
|
||||||
|
{buttonText()}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{
|
||||||
|
* info: UpdateDetail,
|
||||||
|
* updates: UpdateDetail[],
|
||||||
|
* }} props;
|
||||||
|
*/
|
||||||
|
const SelectedSwitch = (props) => {
|
||||||
|
const { state, dispatch } = useSelected();
|
||||||
|
const checkboxId = () => `selectable-${props.info.id.replaceAll(';', '-')}`;
|
||||||
|
|
||||||
|
const dispatchChecked = checked => {
|
||||||
|
if (checked) {
|
||||||
|
dispatch({ type: "ADD", updates: props.updates });
|
||||||
|
} else {
|
||||||
|
dispatch({ type: "REMOVE", updates: props.updates });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isChecked = () => {
|
||||||
|
return !!state.selected[props.info.id];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
id={checkboxId()}
|
||||||
|
ouiaId={checkboxId()}
|
||||||
|
aria-label="select-update-checkbox"
|
||||||
|
isChecked={isChecked()}
|
||||||
|
onChange={(_event, checked) => dispatchChecked(checked)} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{
|
||||||
|
* updates: UpdateDetail[],
|
||||||
|
* }} props;
|
||||||
|
*/
|
||||||
|
const WebConsoleRestartWarn = (props) => {
|
||||||
const { state } = useSelected();
|
const { state } = useSelected();
|
||||||
|
|
||||||
|
if (calculateSelected(props.updates, state).findIndex((value) => value.id.includes("cockpit-ws")) === -1)
|
||||||
|
return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flex={{ default: 'inlineFlex' }} className="cockpit-update-warning">
|
<Flex flex={{ default: 'inlineFlex' }} className="cockpit-update-warning">
|
||||||
<FlexItem>
|
<FlexItem>
|
||||||
|
|
@ -125,6 +281,21 @@ const WebConsoleRestartWarn = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {UpdateDetail[]} allUpdates
|
||||||
|
* @param {SelectedState} state
|
||||||
|
* @returns {UpdateDetail[]}
|
||||||
|
*/
|
||||||
|
function calculateSelected(allUpdates, state) {
|
||||||
|
const selected = Object.values(state.selected).filter(update => !!update);
|
||||||
|
|
||||||
|
if (selected.length === 0) {
|
||||||
|
return allUpdates;
|
||||||
|
} else {
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function deduplicate(list) {
|
function deduplicate(list) {
|
||||||
return [...new Set(list)].sort();
|
return [...new Set(list)].sort();
|
||||||
}
|
}
|
||||||
|
|
@ -258,7 +429,7 @@ const markdownOptions = {
|
||||||
forceInline: true,
|
forceInline: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
function updateItem(info, pkgNames, key) {
|
function updateItem(info, pkgNames, key, partialUpdates) {
|
||||||
const bug_urls = (info.bug_urls || []).filter(isSafeUrl);
|
const bug_urls = (info.bug_urls || []).filter(isSafeUrl);
|
||||||
const cve_urls = (info.cve_urls || []).filter(isSafeUrl);
|
const cve_urls = (info.cve_urls || []).filter(isSafeUrl);
|
||||||
const vendor_urls = (info.vendor_urls || []).filter(isSafeUrl);
|
const vendor_urls = (info.vendor_urls || []).filter(isSafeUrl);
|
||||||
|
|
@ -386,8 +557,16 @@ function updateItem(info, pkgNames, key) {
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let selectColumn = [];
|
||||||
|
if (partialUpdates) {
|
||||||
|
selectColumn = [
|
||||||
|
{ title: <SelectedSwitch info={info} updates={ pkgNames } />, props: { className: "select-update" } },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
columns: [
|
columns: [
|
||||||
|
...selectColumn,
|
||||||
{ title: pkgsTruncated },
|
{ title: pkgsTruncated },
|
||||||
{ title: <TableText wrapModifier="truncate">{info.version}</TableText>, props: { className: "version" } },
|
{ title: <TableText wrapModifier="truncate">{info.version}</TableText>, props: { className: "version" } },
|
||||||
{ title: <TableText wrapModifier="nowrap">{type}</TableText>, props: { className: "type" } },
|
{ title: <TableText wrapModifier="nowrap">{type}</TableText>, props: { className: "type" } },
|
||||||
|
|
@ -402,7 +581,7 @@ function updateItem(info, pkgNames, key) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const UpdatesList = ({ updates }) => {
|
const UpdatesList = ({ updates, partialUpdates }) => {
|
||||||
const combined_updates = [];
|
const combined_updates = [];
|
||||||
|
|
||||||
// PackageKit doesn"t expose source package names, so group packages with the same version and changelog
|
// PackageKit doesn"t expose source package names, so group packages with the same version and changelog
|
||||||
|
|
@ -414,11 +593,11 @@ const UpdatesList = ({ updates }) => {
|
||||||
const hash = u.version + u.description;
|
const hash = u.version + u.description;
|
||||||
const seenId = sameUpdate[hash];
|
const seenId = sameUpdate[hash];
|
||||||
if (seenId) {
|
if (seenId) {
|
||||||
packageNames[seenId].push({ name: u.name, arch: u.arch, summary: u.summary });
|
packageNames[seenId].push(u);
|
||||||
} else {
|
} else {
|
||||||
// this is a new update
|
// this is a new update
|
||||||
sameUpdate[hash] = u.id;
|
sameUpdate[hash] = u.id;
|
||||||
packageNames[u.id] = [{ name: u.name, arch: u.arch, summary: u.summary }];
|
packageNames[u.id] = [u];
|
||||||
combined_updates.push(u);
|
combined_updates.push(u);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -432,16 +611,30 @@ const UpdatesList = ({ updates }) => {
|
||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let selectColumn = [];
|
||||||
|
if (partialUpdates) {
|
||||||
|
selectColumn = [
|
||||||
|
{ title: "", props: { hidden: true, 'aria-label': _("Select update") } }
|
||||||
|
];
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<ListingTable aria-label={_("Available updates")}
|
<ListingTable aria-label={_("Available updates")}
|
||||||
gridBreakPoint='grid-lg'
|
gridBreakPoint='grid-lg'
|
||||||
columns={[
|
columns={[
|
||||||
|
...selectColumn,
|
||||||
{ title: _("Name"), props: { width: 40 } },
|
{ title: _("Name"), props: { width: 40 } },
|
||||||
{ title: _("Version"), props: { width: 15 } },
|
{ title: _("Version"), props: { width: 15 } },
|
||||||
{ title: _("Severity"), props: { width: 15 } },
|
{ title: _("Severity"), props: { width: 15 } },
|
||||||
{ title: _("Details"), props: { width: 30 } },
|
{ title: _("Details"), props: { width: 30 } },
|
||||||
]}
|
]}
|
||||||
rows={combined_updates.map(update => updateItem(update, packageNames[update.id].sort((a, b) => a.name > b.name), update.id))} />
|
rows={combined_updates.map(update => {
|
||||||
|
return updateItem(update,
|
||||||
|
packageNames[update.id].sort((a, b) => a.name > b.name),
|
||||||
|
update.id,
|
||||||
|
partialUpdates,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -800,13 +993,13 @@ class CardsPage extends React.Component {
|
||||||
id: "available-updates",
|
id: "available-updates",
|
||||||
title: _("Available updates"),
|
title: _("Available updates"),
|
||||||
actions: (<div className="pk-updates--header--actions">
|
actions: (<div className="pk-updates--header--actions">
|
||||||
{this.props.cockpitUpdate && <WebConsoleRestartWarn />}
|
<WebConsoleRestartWarn updates={this.props.updates} />
|
||||||
{this.props.applyKpatches}
|
{this.props.applyKpatches}
|
||||||
{this.props.applySecurity}
|
{this.props.applySecurity}
|
||||||
{this.props.applyAll}
|
{this.props.applySelected}
|
||||||
</div>),
|
</div>),
|
||||||
containsList: true,
|
containsList: true,
|
||||||
body: <UpdatesList updates={this.props.updates} />
|
body: <UpdatesList updates={this.props.updates} partialUpdates={this.props.partialUpdates} />
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -859,6 +1052,7 @@ class OsUpdates extends React.Component {
|
||||||
backend: "",
|
backend: "",
|
||||||
rebootAfterSuccess: false,
|
rebootAfterSuccess: false,
|
||||||
packageManager: null,
|
packageManager: null,
|
||||||
|
partialUpdates: false,
|
||||||
};
|
};
|
||||||
this.handleLoadError = this.handleLoadError.bind(this);
|
this.handleLoadError = this.handleLoadError.bind(this);
|
||||||
this.handleRefresh = this.handleRefresh.bind(this);
|
this.handleRefresh = this.handleRefresh.bind(this);
|
||||||
|
|
@ -880,7 +1074,8 @@ class OsUpdates extends React.Component {
|
||||||
// HACK: force usage of PackageKit backend
|
// HACK: force usage of PackageKit backend
|
||||||
const packageManager = await getPackageManager(true);
|
const packageManager = await getPackageManager(true);
|
||||||
const backend = await packageManager.get_backend();
|
const backend = await packageManager.get_backend();
|
||||||
this.setState({ packageManager, backend });
|
const partialUpdates = is_partial_updates_supported(backend);
|
||||||
|
this.setState({ packageManager, backend, partialUpdates });
|
||||||
|
|
||||||
// check if there is an upgrade in progress already; if so, switch to "applying" state right away
|
// check if there is an upgrade in progress already; if so, switch to "applying" state right away
|
||||||
try {
|
try {
|
||||||
|
|
@ -1149,13 +1344,19 @@ class OsUpdates extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyUpdates(type) {
|
/**
|
||||||
|
* @param {SelectedState=} selected
|
||||||
|
*/
|
||||||
|
applyUpdates(type, selected) {
|
||||||
let updates = [...this.state.updates];
|
let updates = [...this.state.updates];
|
||||||
if (type === UPDATES.SECURITY)
|
if (type === UPDATES.SECURITY)
|
||||||
updates = updates.filter(update => update.severity === Severity.CRITICAL);
|
updates = updates.filter(update => update.severity === Severity.CRITICAL);
|
||||||
if (type === UPDATES.KPATCHES) {
|
if (type === UPDATES.KPATCHES) {
|
||||||
updates = updates.filter(update => isKpatchPackage(update.name));
|
updates = updates.filter(update => isKpatchPackage(update.name));
|
||||||
}
|
}
|
||||||
|
if (type === UPDATES.SELECTED && selected) {
|
||||||
|
updates = calculateSelected(updates, selected);
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ state: "applying", applyTransactionProps: {}, applyActions: [] });
|
this.setState({ state: "applying", applyTransactionProps: {}, applyActions: [] });
|
||||||
|
|
||||||
|
|
@ -1174,7 +1375,7 @@ class OsUpdates extends React.Component {
|
||||||
renderContent() {
|
renderContent() {
|
||||||
let applySecurity;
|
let applySecurity;
|
||||||
let applyKpatches;
|
let applyKpatches;
|
||||||
let applyAll;
|
let applySelected;
|
||||||
|
|
||||||
/* On unregistered RHEL systems we need some heuristics: If the "main" OS repos (which provide coreutils) require
|
/* 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
|
* a subscription, then point this out and don't show available updates, even if there are some auxiliary
|
||||||
|
|
@ -1227,12 +1428,10 @@ class OsUpdates extends React.Component {
|
||||||
const num_kpatches = count_kpatch_updates(this.state.updates);
|
const num_kpatches = count_kpatch_updates(this.state.updates);
|
||||||
const highest_severity = find_highest_severity(this.state.updates);
|
const highest_severity = find_highest_severity(this.state.updates);
|
||||||
|
|
||||||
applyAll = (
|
applySelected = <SelectedButton
|
||||||
<Button id={num_updates == num_security_updates ? "install-security" : "install-all"} variant="primary" onClick={ () => this.applyUpdates(UPDATES.ALL) }>
|
num_updates={num_updates}
|
||||||
{ num_updates == num_security_updates
|
securityOnly={num_updates === num_security_updates}
|
||||||
? _("Install security updates")
|
onClick={(items) => this.applyUpdates(UPDATES.SELECTED, items)} />;
|
||||||
: _("Install all updates") }
|
|
||||||
</Button>);
|
|
||||||
|
|
||||||
if (num_security_updates > 0 && num_updates > num_security_updates) {
|
if (num_security_updates > 0 && num_updates > num_security_updates) {
|
||||||
applySecurity = (
|
applySecurity = (
|
||||||
|
|
@ -1272,7 +1471,7 @@ class OsUpdates extends React.Component {
|
||||||
<Gallery className='ct-cards-grid' hasGutter>
|
<Gallery className='ct-cards-grid' hasGutter>
|
||||||
<CardsPage handleRefresh={this.handleRefresh}
|
<CardsPage handleRefresh={this.handleRefresh}
|
||||||
applySecurity={applySecurity}
|
applySecurity={applySecurity}
|
||||||
applyAll={applyAll}
|
applySelected={applySelected}
|
||||||
applyKpatches={applyKpatches}
|
applyKpatches={applyKpatches}
|
||||||
highestSeverity={highest_severity}
|
highestSeverity={highest_severity}
|
||||||
onValueChanged={this.onValueChanged}
|
onValueChanged={this.onValueChanged}
|
||||||
|
|
@ -1469,5 +1668,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = createRoot(document.getElementById('app'));
|
const root = createRoot(document.getElementById('app'));
|
||||||
root.render(<OsUpdates />);
|
root.render(
|
||||||
|
<SelectedStore>
|
||||||
|
<OsUpdates />
|
||||||
|
</SelectedStore>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -744,6 +744,70 @@ ExecStart=/usr/local/bin/{packageName}
|
||||||
b.wait_not_in_text("#restart-services-modal .pf-v6-l-stack__item", packageName)
|
b.wait_not_in_text("#restart-services-modal .pf-v6-l-stack__item", packageName)
|
||||||
b.wait_visible("#restart-services-modal .pf-v6-c-alert")
|
b.wait_visible("#restart-services-modal .pf-v6-c-alert")
|
||||||
|
|
||||||
|
@testlib.skipImage("Partial updates are not supported", "arch", "opensuse-tumbleweed")
|
||||||
|
def testSelectUpdates(self):
|
||||||
|
b = self.browser
|
||||||
|
|
||||||
|
self.createPackage("xeroxed", "10", "1", install=True)
|
||||||
|
self.createPackage("xeroxed", "12", "1", severity="enhancement", bugs=["789"],
|
||||||
|
changes="Uniqueness is boring")
|
||||||
|
self.createPackage("norefs-bin", "1", "1", install=True)
|
||||||
|
self.createPackage("norefs-bin", "2", "1", severity="enhancement",
|
||||||
|
changes="Now 10% *more* [unicorns](http://unicorn.example.com)")
|
||||||
|
self.createPackage("norefs-doc", "1", "1", install=True)
|
||||||
|
self.createPackage("norefs-doc", "2", "1", severity="enhancement",
|
||||||
|
changes="Now 10% *more* [unicorns](http://unicorn.example.com)")
|
||||||
|
self.createPackage("buggy", "2", "1", install=True)
|
||||||
|
self.createPackage("buggy", "2", "2", changes="* Fixit", bugs=["123", "456"])
|
||||||
|
self.enableRepo(refresh_repo=True)
|
||||||
|
|
||||||
|
self.login_and_go("/updates")
|
||||||
|
with b.wait_timeout(30):
|
||||||
|
b.wait_visible("#available-updates")
|
||||||
|
b.wait_in_text("#status", "4 updates available")
|
||||||
|
self.assertEqual(b.text("[data-ouia-component-id='install-all']"), "Install all updates")
|
||||||
|
|
||||||
|
# Select one
|
||||||
|
b.click("[data-ouia-component-id^='selectable-buggy']")
|
||||||
|
self.assertEqual(b.text("[data-ouia-component-id='install-all']"), "Install selected updates (1)")
|
||||||
|
# Select one with two packages
|
||||||
|
b.click("[data-ouia-component-id^='selectable-norefs']")
|
||||||
|
self.assertEqual(b.text("[data-ouia-component-id='install-all']"), "Install selected updates (3)")
|
||||||
|
# Select last unselected package
|
||||||
|
b.click("[data-ouia-component-id^='selectable-xeroxed']")
|
||||||
|
self.assertEqual(b.text("[data-ouia-component-id='install-all']"), "Install all updates")
|
||||||
|
|
||||||
|
# Unselect all, make sure we still have "Install all updates"
|
||||||
|
b.click("[data-ouia-component-id^='selectable-buggy']")
|
||||||
|
b.click("[data-ouia-component-id^='selectable-norefs']")
|
||||||
|
b.click("[data-ouia-component-id^='selectable-xeroxed']")
|
||||||
|
self.assertEqual(b.text("[data-ouia-component-id='install-all']"), "Install all updates")
|
||||||
|
|
||||||
|
# Select one and install it
|
||||||
|
b.click("[data-ouia-component-id^='selectable-buggy']")
|
||||||
|
b.click("[data-ouia-component-id='install-all']")
|
||||||
|
with b.wait_timeout(60):
|
||||||
|
b.wait_in_text(".pf-v6-c-empty-state__title", "Update was successful")
|
||||||
|
b.click("#ignore")
|
||||||
|
b.wait_in_text("#status", "3 updates available")
|
||||||
|
|
||||||
|
# Select one with 2 packages
|
||||||
|
b.click("[data-ouia-component-id^='selectable-norefs']")
|
||||||
|
b.click("[data-ouia-component-id='install-all']")
|
||||||
|
with b.wait_timeout(60):
|
||||||
|
b.wait_in_text(".pf-v6-c-empty-state__title", "Update was successful")
|
||||||
|
b.click("#ignore")
|
||||||
|
b.wait_in_text("#status", "1 update available")
|
||||||
|
|
||||||
|
# Install last one without selection
|
||||||
|
b.click("[data-ouia-component-id='install-all']")
|
||||||
|
with b.wait_timeout(60):
|
||||||
|
b.wait_in_text(".pf-v6-c-empty-state__title", "Update was successful")
|
||||||
|
|
||||||
|
# All updates should now be installed
|
||||||
|
b.click("#ignore")
|
||||||
|
b.wait_in_text("#status", "System is up to date")
|
||||||
|
|
||||||
@testlib.skipImage("No security changelog support in packagekit", "arch")
|
@testlib.skipImage("No security changelog support in packagekit", "arch")
|
||||||
def testInfoSecurity(self):
|
def testInfoSecurity(self):
|
||||||
b = self.browser
|
b = self.browser
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue