feat: check for new versions of Docker Registry UI at start up and notify the user

This commit is contained in:
Joxit 2023-06-07 00:24:11 +02:00
parent ffb6d14baf
commit b88dc4567d
No known key found for this signature in database
GPG Key ID: F526592B8E012263
6 changed files with 232 additions and 3 deletions

View File

@ -17,7 +17,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<docker-registry-ui>
<header>
<material-navbar>
<span class="logo">Docker Registry UI</span>
<span class="logo">
<span>Docker Registry UI</span>
<version-notification version="{ version }" on-notify="{ notifySnackbar }"></version-notification>
</span>
<div class="menu">
<search-bar on-search="{ onSearch }"></search-bar>
<dialogs-menu
@ -142,6 +145,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import DialogsMenu from './dialogs/dialogs-menu.riot';
import SearchBar from './search-bar.riot';
import ErrorPage from './error-page.riot';
import VersionNotification from './version-notification.riot';
import { stripHttps, getRegistryServers, setRegistryServers, truthy, stringToArray } from '../scripts/utils';
import router from '../scripts/router';
import { loadTheme } from '../scripts/theme';
@ -156,6 +160,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
Router,
Route,
ErrorPage,
VersionNotification,
},
onUpdated(props, state) {
state.snackbarIsError = false;
@ -170,7 +175,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}
window.onselectstart = (e) => {
if (e.target && e.target.className) {
if (e.target && e.target.className && typeof e.target.className.indexOf === 'function') {
return !['checkbox', 'checkmark', 'remove-tag'].find((elt) => e.target.className.indexOf(elt) >= 0);
}
};
@ -274,6 +279,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
flex-shrink: 1;
}
material-navbar .nav-wrapper .logo {
display: flex;
align-items: center;
flex-direction: row;
}
material-footer {
color: var(--footer-neutral-text);
background-color: var(--footer-background);

View File

@ -0,0 +1,109 @@
<version-notification>
<span if="{ state.tag_name && !isNewestVersion(props.version, state.tag_name) }" onclick="{ onClick }"></span>
<material-popup opened="{ state.open }" onClick="{ onClose }">
<div class="material-popup-title">Check for updates</div>
<div class="material-popup-content">
<p>The version <b>{ state.tag_name }</b> of Docker Regisrty UI now available.</p>
<p>You can download the lastest version with docker.</p>
<code>joxit/docker-registry-ui:{ state.tag_name }</code>
</div>
<div class="material-popup-action">
<material-button
class="dialog-button release-note"
waves-color="var(--hover-background)"
href="{ state.latest && state.latest.html_url }"
color="inherit"
text-color="var(--accent-text)"
target="_blank"
>
Release Note
</material-button>
<material-button
class="dialog-button"
waves-color="var(--hover-background)"
onClick="{ onClose }"
color="inherit"
text-color="var(--primary-text)"
>
Close
</material-button>
</div>
</material-popup>
<script>
import rocketIcon from '../images/rocket.svg';
import { Http } from '../scripts/http';
import { isNewestVersion, parseJSON } from '../scripts/utils';
const LATEST = 'version-notification:latest';
const EXPIRES = 'version-notification:expiration-date';
const ONE_DAY = 24 * 60 * 60 * 1000;
export default {
onMounted(props, state) {
const latest = parseJSON(localStorage.getItem(LATEST));
const expires = parseInt(localStorage.getItem(EXPIRES));
if (latest && latest.tag_name) {
this.update({ tag_name: latest.tag_name, latest });
}
if (!latest || isNaN(expires) || new Date().getTime() > expires) {
this.checkForUpdates(props, state);
}
},
onUpdated(props, state) {
const span = this.$('span');
if (span) {
span.innerHTML = rocketIcon().firstElementChild.outerHTML;
}
},
onClose() {
this.update({ open: false });
},
onClick() {
this.update({ open: true });
},
checkForUpdates(props, state) {
const oReq = new Http();
const self = this;
oReq.addEventListener('load', function () {
if (this.status === 200) {
const latest = parseJSON(this.responseText);
if (latest && self.tag_name !== latest.tag_name && !isNewestVersion(props.version, latest.tag_name)) {
props.onNotify('A new version of Docker Registry UI is available!');
}
localStorage.setItem(LATEST, this.responseText);
localStorage.setItem(EXPIRES, new Date().getTime() + ONE_DAY);
self.update({ tag_name: latest.tag_name, latest });
} else {
props.onNotify('Cannot check for new updates. See the browser console.');
console.error(`Got status code ${this.status} from Github API with response ${this.responseText}`);
}
});
oReq.open('GET', 'https://api.github.com/repos/joxit/docker-registry-ui/releases/latest');
oReq.send();
},
isNewestVersion,
};
</script>
<style>
:host {
display: inline;
}
:host svg {
margin-left: 10px;
cursor: pointer;
}
material-popup material-button > a:first-child {
display: flex;
align-items: center;
}
material-popup .material-popup-content code {
background-color: var(--hover-background);
padding: 0 5px;
border-radius: 4px;
line-height: 1.5em;
}
material-popup .material-popup-content b {
color: var(--accent-text);
}
</style>
</version-notification>

20
src/images/rocket.svg Normal file
View File

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 128 128">
<path fill="#ca2c31" d="M3.77 71.73l16.34-16.1 27.82-4.93-2.75 14.56L7.57 76.82l-2.43-1.05z"/>
<path fill="#a02422" d="M22.94 59.76L5.2 75.88l13.05 6.36 19.81-10.11v-4.77l4.05-10.92zm41.98 28.39l-8.57 3.72-8.09 17.15s7.12 15.77 7.44 15.77c.32 0 4.37.32 4.37.32l14.4-16.1 3.64-27.5-13.19 6.64z"/>
<path d="M56.5 100.84s4.77-.97 8.17-2.59c3.4-1.62 7.6-4.04 7.6-4.04l-1.54 13.43-15.05 17.13s-.59-.73-3.09-6.17c-1.99-4.34-2.68-5.89-2.68-5.89l6.59-11.87z" fill="#ca2c31"/>
<path d="M31.58 80.66s-5.74-.48-12.03 7.47c-5.74 7.26-8.43 19.08-9.47 22.12s-3.53 3.66-2.7 5.05 4.42 1.31 8.85.76 8.23-1.94 8.23-1.94-.19.48-.83 1.52c-.23.37-1.03.9-.97 1.45.14 1.31 11.36 1.34 20.32-7.88 9.68-9.95 4.98-18.11 4.98-18.11L31.58 80.66z" fill="#f7d74d"/>
<path d="M33.31 85.29s-6.19.33-11.31 8.28-7.5 17.16-7.01 17.78c.48.62 10.02-2.83 12.31-2.14 1.57.48.76 2.07 1.18 2.49.35.35 4.49.94 11.19-6.32 6.71-7.26 5.12-17.46 5.12-17.46l-11.48-2.63z" fill="#fbf0b4"/>
<path d="M36.35 74.44s-3.11 2.77-4.22 4.36c-1.11 1.59-1.11 1.73-1.04 2.21.07.48 1.22 5.75 6.01 10.37 5.88 5.67 11.13 6.43 11.89 6.43.76 0 5.81-5.67 5.81-5.67l-18.45-17.7z" fill="#858585"/>
<path d="M50.1 91.24s5.04 3.31 13.49.47c11.55-3.88 20.02-12.56 30.51-23.52 10.12-10.58 18.61-23.71 18.61-23.71l-5.95-19.93L50.1 91.24z" fill="#437687"/>
<path d="M67.99 80.33l1.39-4.32 3.48.49s2.65 1.25 4.6 2.16c1.95.91 4.46 1.6 4.46 1.6l-4.95 4.18s-2.7-1.02-4.67-1.88c-2.22-.97-4.31-2.23-4.31-2.23z" fill="#3f545f"/>
<path d="M84.32 16.14s-9.62 5.58-23.41 18.63c-12.43 11.76-21.64 22.4-23.87 31.45-1.86 7.58-.87 12.18 3.36 17.15 4.47 5.26 9.71 7.87 9.71 7.87s3.94.06 20.38-12.59C91 62.86 107.43 36.42 107.43 36.42L84.32 16.14z" fill="#8dafbf"/>
<path d="M104.18 41.84s-8.37-3.57-14.34-11.9c-5.93-8.27-5.46-13.86-5.46-13.86s4.96-3.89 16.11-8.34c7.5-2.99 17.71-4.52 21.07-2.03s-2.3 14.98-2.3 14.98l-10.31 19.96-4.77 1.19z" fill="#d83f22"/>
<path d="M68.17 80.4s-7.23-3.69-11.83-8.94c-8.7-9.91-10.5-20.79-10.5-20.79l4.37-5.13S51.3 57.1 60.63 67.09c6.08 6.51 12.43 9.49 12.43 9.49s-1.27 1.07-2.63 2.11c-.87.67-2.26 1.71-2.26 1.71z" fill="#6896a5"/>
<path d="M112.71 44.48s4.34-5.23 8.45-17.02c5.74-16.44.74-21.42.74-21.42s-1.69 7.82-7.56 18.69c-4.71 8.71-10.41 17-10.41 17s3.14 1.41 4.84 1.9c2.14.62 3.94.85 3.94.85z" fill="#a02422"/>
<path d="M39.81 69.66c1.3 1.24 3.27-.06 4.56-3.1 1.3-3.04 1.28-4.74.28-5.46-1.24-.9-3.32 1.07-4.23 2.82-1 1.94-1.59 4.8-.61 5.74zm45.14-49.53s-7.61 5.47-15.73 12.91c-7.45 6.83-12.39 12.17-13.07 13.41-.72 1.33-.73 3.21-.17 4.17s1.8 1.46 2.93.62c1.13-.85 9.18-9.75 16.45-16.11 6.65-5.82 11.78-9.51 11.78-9.51s2.08-3.68 1.74-4.52c-.34-.85-3.93-.97-3.93-.97z" fill="#b3e1ee"/>
<path d="M84.95 20.13s5.62-4.31 11.74-7.34c5.69-2.82 11.35-5.17 12.37-3.13.97 1.94-5.37 4.58-10.95 8.14-5.58 3.56-10.95 7.81-10.95 7.81s-.82-1.5-1.35-2.89a23.7 23.7 0 01-.86-2.59z" fill="#ed6a65"/>
<path d="M89.59 39.25c-5.57-5.13-13.32-3.75-17.14.81-3.92 4.7-3.63 11.88 1 16.2 4.21 3.92 12.04 4.81 16.76-.69 4.2-4.88 3.94-12.13-.62-16.32z" fill="#e1e1e1"/>
<path d="M75.33 41.87c-3.31 3.25-3.13 9.69.81 12.63 3.44 2.57 8.32 2.44 11.38-.69 3.06-3.13 3.06-8.82.19-11.76-3.3-3.37-8.59-3.9-12.38-.18z" fill="#3f545f"/>
<path d="M50 76.89s6.19-6.28 6.87-5.6c.68.68.59 4.49-2.37 8.73-2.97 4.24-9.5 11.79-14.67 16.88-5.1 5.01-12.29 10.74-12.97 10.64-.53-.08-2.68-1.15-3.54-2.19-.84-1.03 1.67-5.9 2.68-7.51 1.02-1.61 24-20.95 24-20.95z" fill="#a02524"/>
<path d="M21.23 101.85c-.08 1.44 2.12 3.54 2.12 3.54L56.87 71.3s-1.57-1.77-6.19 1.1c-4.66 2.9-8.74 6.38-14.76 12.21-8.39 8.14-14.61 15.8-14.69 17.24z" fill="#ca2c31"/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -220,3 +220,40 @@ export function truthy(value) {
export function stringToArray(value) {
return value && typeof value === 'string' ? value.split(',') : [];
}
const compareNumbers = (a, b) => {
const na = parseInt(a);
const nb = parseInt(b);
if (na > nb) return 1;
if (nb > na) return -1;
if (!isNaN(na) && isNaN(nb)) return 1;
if (isNaN(na) && !isNaN(nb)) return -1;
return 0;
};
export function isNewestVersion(current = '0.0.0', release = '0.0.0') {
if (current === release) {
return true;
}
current = current.split('.');
release = release.split('.');
const isDev = current[2].indexOf('-') >= 0;
const major = compareNumbers(current[0], release[0]);
const minor = compareNumbers(current[1], release[1]);
const patch = compareNumbers(current[2], release[2]);
if (!isDev && (major > 0 || (major === 0 && minor > 0) || (major === 0 && minor === 0 && patch >= 0))) {
return true;
} else if (isDev && (major > 0 || (major === 0 && minor > 0))) {
return true;
}
return false;
}
export function parseJSON(json) {
if (!json) {
return;
}
try {
return JSON.parse(json);
} catch (e) {}
}

View File

@ -3,7 +3,7 @@ import { getTagComparator } from '../src/scripts/taglist-order.js';
import { DockerRegistryUIError } from '../src/scripts/error.js';
import assert from 'assert';
describe('utils tests', () => {
describe('taglist-order tests', () => {
describe('taglistOrderVariants', () => {
it(`should return the input when it's well formed and num first`, () => {
const expected = ['num-asc;alpha-asc', 'num-asc;alpha-desc', 'num-desc;alpha-asc', 'num-desc;alpha-asc'];

52
test/utils.test.js Normal file
View File

@ -0,0 +1,52 @@
import { isNewestVersion } from '../src/scripts/utils.js';
import assert from 'assert';
describe('utils tests', () => {
describe('isNewestVersion', () => {
it(`should return true for the same version`, () => {
const expected = ['2.0.0', '2.4.1', '2.5.0', null, undefined];
expected.forEach((e) => assert.ok(isNewestVersion(e, e)));
});
it(`should return true with on common versions`, () => {
assert.ok(isNewestVersion('2.5.1', '2.5.0'));
assert.ok(isNewestVersion('2.5.0', '2.0.0'));
assert.ok(isNewestVersion('2.15.0', '1.25.10'));
assert.ok(isNewestVersion('10.10.10', '2.25.20'));
});
it(`should return false on common versions`, () => {
assert.equal(isNewestVersion('1.0.0', '2.5.0'), false);
assert.equal(isNewestVersion('10.10.10', '20.20.20'), false);
assert.equal(isNewestVersion('2.4.10', '2.5.0'), false);
assert.equal(isNewestVersion('2.5.0', '2.6.0'), false);
});
it(`should return true for -dev next versions`, () => {
assert.ok(isNewestVersion('2.5.0-dev', '2.4.1'));
assert.ok(isNewestVersion('2.6.0-dev', '2.5.0'));
assert.ok(isNewestVersion('2.15.0-dev', '2.14.1'));
assert.ok(isNewestVersion('2.15.0-dev', '1.16.0'));
});
it(`should return false for -dev with current minor version`, () => {
assert.equal(isNewestVersion('2.5.0-dev', '2.5.0'), false);
assert.equal(isNewestVersion('2.5.0-dev', '2.5.10'), false);
assert.equal(isNewestVersion('2.15.0-dev', '2.15.0'), false);
assert.equal(isNewestVersion('2.0.0-dev', '2.15.0'), false);
});
it(`should return true for -{commit sha} next versions`, () => {
assert.ok(isNewestVersion('2.5.0-ffb6d14baf', '2.4.1'));
assert.ok(isNewestVersion('2.6.0-ffb6d14baf', '2.5.0'));
assert.ok(isNewestVersion('2.15.0-ffb6d14baf', '2.14.1'));
assert.ok(isNewestVersion('2.15.0-ffb6d14baf', '1.16.0'));
});
it(`should return false for -{commit sha} with current minor version`, () => {
assert.equal(isNewestVersion('2.5.0-ffb6d14baf', '2.5.0'), false);
assert.equal(isNewestVersion('2.5.0-ffb6d14baf', '2.5.10'), false);
assert.equal(isNewestVersion('2.15.0-ffb6d14baf', '2.15.0'), false);
assert.equal(isNewestVersion('2.0.0-ffb6d14baf', '2.15.0'), false);
});
});
});