diff --git a/package.json b/package.json index c6130d1d..3231705d 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@patternfly/react-core": "3.158.3", "moment": "2.27.0", "react": "16.13.1", - "react-dom": "16.13.1" + "react-dom": "16.13.1", + "throttle-debounce": "2.2.1" } } diff --git a/src/app.jsx b/src/app.jsx index 250d5da8..4a1fe35a 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -39,6 +39,7 @@ import { DataToolbar, DataToolbarItem, DataToolbarContent, } from '@patternfly/react-core/dist/esm/experimental'; import moment from 'moment'; +import { debounce } from 'throttle-debounce'; import cockpit from 'cockpit'; @@ -65,6 +66,44 @@ function track_id(item) { return key; } +function format_version(deployment) { + var formated = ""; + if (!deployment || !deployment.osname) + return; + + if (deployment.version) + formated = deployment.version.v; + + return cockpit.format("$0 $1", deployment.osname.v, formated); +} + +// https://github.com/cockpit-project/cockpit/blob/master/pkg/lib/notifications.js +function set_page_status(status) { + cockpit.transport.control("notify", { page_status: status }); +} + +/* client.changed often happens several times at the start, avoid flickering */ +const set_update_status = debounce(1000, versions => { + if (versions && versions.length > 0) { + /* if the latest version is booted, we are current */ + if (versions[0].booted && versions[0].booted.v) { + set_page_status({ + title: _("System is up to date"), + details: { icon: "fa fa-check-circle-o" } + }); + } else { + /* report the available update */ + set_page_status({ + title: cockpit.format(_("Update available: $0"), format_version(versions[0])), + type: "warning", + }); + } + } else { + console.warn("got invalid client.known_versions_for() result:", JSON.stringify(versions)); + set_page_status(null); + } +}); + /** * Empty state for connecting and errors */ @@ -414,13 +453,26 @@ class Application extends React.Component { } this.setState({ curtain: { state: 'failed', failure: true, message, final } }); + set_page_status(null); }; client.addEventListener("connectionLost", (event, ex) => show_failure(ex)); client.addEventListener("changed", () => this.forceUpdate()); client.connect() - .then(() => { timeout = window.setTimeout(check_empty, 1000) }) + .then(() => { + timeout = window.setTimeout(check_empty, 1000); + + /* notify overview card */ + set_page_status({ + type: null, + title: _("Checking for package updates..."), + details: { + link: false, + icon: "spinner spinner-xs", + }, + }); + }) .fail(ex => { window.clearTimeout(timeout); show_failure(ex); @@ -495,8 +547,10 @@ class Application extends React.Component { /* TODO: support more than one OS */ /* successful, deployments are available */ - const items = client.known_versions_for(this.state.os, this.state.origin.remote, this.state.origin.branch) - .map(item => { + const versions = client.known_versions_for(this.state.os, this.state.origin.remote, this.state.origin.branch); + set_update_status(versions); + + const items = versions.map(item => { const packages = client.packages(item); if (packages) packages.addEventListener("changed", () => this.setState({})); // re-render diff --git a/src/manifest.json b/src/manifest.json index b21d2286..d28e9887 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -6,9 +6,10 @@ }, "tools": { - "ostree": { - "label": "Software Updates", - "path": "index.html" + "index": { + "label": "Software Updates" } - } + }, + + "preload": [ "index" ] } diff --git a/test/check-ostree b/test/check-ostree index 69bf3175..c8309967 100755 --- a/test/check-ostree +++ b/test/check-ostree @@ -651,5 +651,46 @@ class OstreeCase(MachineCase): self.assertIn("Reconnect", b.text(".pf-c-empty-state button")) self.allow_authorize_journal_messages() + @skipImage("Too old cockpit-system", "rhel-atomic", "continuous-atomic") + def testPageStatus(self): + m = self.machine + b = self.browser + + rhsmcertd_hack(m) + + # preloading works, no updates available + self.login_and_go("/system") + b.wait_text("#page_status_notification_updates", "System is up to date") + self.assertEqual(b.attr("#page_status_notification_updates span:first-child", "class"), + "fa fa-check-circle-o") + # go to updates page + b.click("#page_status_notification_updates a") + b.enter_page("/updates") + + # now generate an update + remove_pkg = m.execute("rpm -qa | grep socat").strip() + generate_new_commit(m, remove_pkg) + m.upload(["files/publickey.asc"], "/root/") + m.execute("ostree remote gpg-import local -k /root/publickey.asc") + + # updates page sees the new version + b.click("#check-for-updates-btn") + b.wait_in_text(get_list_item(1) + " .version", "cockpit-base.2") + + # overview page notices the new version as well + b.go("/system") + b.enter_page("/system") + b.wait_in_text("#page_status_notification_updates", "Update available:") + b.wait_in_text("#page_status_notification_updates", "cockpit-base.2") + + # check with new session + b.logout() + self.login_and_go("/system") + b.wait_in_text("#page_status_notification_updates", "Update available:") + b.wait_in_text("#page_status_notification_updates", "cockpit-base.2") + + self.allow_authorize_journal_messages() + + if __name__ == "__main__": test_main()