Skip to content

Commit b88dc45

Browse files
committed
feat: check for new versions of Docker Registry UI at start up and notify the user
1 parent ffb6d14 commit b88dc45

File tree

6 files changed

+232
-3
lines changed

6 files changed

+232
-3
lines changed

src/components/docker-registry-ui.riot

+13-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
<docker-registry-ui>
1818
<header>
1919
<material-navbar>
20-
<span class="logo">Docker Registry UI</span>
20+
<span class="logo">
21+
<span>Docker Registry UI</span>
22+
<version-notification version="{ version }" on-notify="{ notifySnackbar }"></version-notification>
23+
</span>
2124
<div class="menu">
2225
<search-bar on-search="{ onSearch }"></search-bar>
2326
<dialogs-menu
@@ -142,6 +145,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
142145
import DialogsMenu from './dialogs/dialogs-menu.riot';
143146
import SearchBar from './search-bar.riot';
144147
import ErrorPage from './error-page.riot';
148+
import VersionNotification from './version-notification.riot';
145149
import { stripHttps, getRegistryServers, setRegistryServers, truthy, stringToArray } from '../scripts/utils';
146150
import router from '../scripts/router';
147151
import { loadTheme } from '../scripts/theme';
@@ -156,6 +160,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
156160
Router,
157161
Route,
158162
ErrorPage,
163+
VersionNotification,
159164
},
160165
onUpdated(props, state) {
161166
state.snackbarIsError = false;
@@ -170,7 +175,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
170175
}
171176
172177
window.onselectstart = (e) => {
173-
if (e.target && e.target.className) {
178+
if (e.target && e.target.className && typeof e.target.className.indexOf === 'function') {
174179
return !['checkbox', 'checkmark', 'remove-tag'].find((elt) => e.target.className.indexOf(elt) >= 0);
175180
}
176181
};
@@ -274,6 +279,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
274279
flex-shrink: 1;
275280
}
276281
282+
material-navbar .nav-wrapper .logo {
283+
display: flex;
284+
align-items: center;
285+
flex-direction: row;
286+
}
287+
277288
material-footer {
278289
color: var(--footer-neutral-text);
279290
background-color: var(--footer-background);
+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<version-notification>
2+
<span if="{ state.tag_name && !isNewestVersion(props.version, state.tag_name) }" onclick="{ onClick }"></span>
3+
<material-popup opened="{ state.open }" onClick="{ onClose }">
4+
<div class="material-popup-title">Check for updates</div>
5+
<div class="material-popup-content">
6+
<p>The version <b>{ state.tag_name }</b> of Docker Regisrty UI now available.</p>
7+
<p>You can download the lastest version with docker.</p>
8+
<code>joxit/docker-registry-ui:{ state.tag_name }</code>
9+
</div>
10+
<div class="material-popup-action">
11+
<material-button
12+
class="dialog-button release-note"
13+
waves-color="var(--hover-background)"
14+
href="{ state.latest && state.latest.html_url }"
15+
color="inherit"
16+
text-color="var(--accent-text)"
17+
target="_blank"
18+
>
19+
Release Note
20+
</material-button>
21+
<material-button
22+
class="dialog-button"
23+
waves-color="var(--hover-background)"
24+
onClick="{ onClose }"
25+
color="inherit"
26+
text-color="var(--primary-text)"
27+
>
28+
Close
29+
</material-button>
30+
</div>
31+
</material-popup>
32+
<script>
33+
import rocketIcon from '../images/rocket.svg';
34+
import { Http } from '../scripts/http';
35+
import { isNewestVersion, parseJSON } from '../scripts/utils';
36+
const LATEST = 'version-notification:latest';
37+
const EXPIRES = 'version-notification:expiration-date';
38+
const ONE_DAY = 24 * 60 * 60 * 1000;
39+
export default {
40+
onMounted(props, state) {
41+
const latest = parseJSON(localStorage.getItem(LATEST));
42+
const expires = parseInt(localStorage.getItem(EXPIRES));
43+
if (latest && latest.tag_name) {
44+
this.update({ tag_name: latest.tag_name, latest });
45+
}
46+
if (!latest || isNaN(expires) || new Date().getTime() > expires) {
47+
this.checkForUpdates(props, state);
48+
}
49+
},
50+
onUpdated(props, state) {
51+
const span = this.$('span');
52+
if (span) {
53+
span.innerHTML = rocketIcon().firstElementChild.outerHTML;
54+
}
55+
},
56+
onClose() {
57+
this.update({ open: false });
58+
},
59+
onClick() {
60+
this.update({ open: true });
61+
},
62+
checkForUpdates(props, state) {
63+
const oReq = new Http();
64+
const self = this;
65+
66+
oReq.addEventListener('load', function () {
67+
if (this.status === 200) {
68+
const latest = parseJSON(this.responseText);
69+
if (latest && self.tag_name !== latest.tag_name && !isNewestVersion(props.version, latest.tag_name)) {
70+
props.onNotify('A new version of Docker Registry UI is available!');
71+
}
72+
localStorage.setItem(LATEST, this.responseText);
73+
localStorage.setItem(EXPIRES, new Date().getTime() + ONE_DAY);
74+
self.update({ tag_name: latest.tag_name, latest });
75+
} else {
76+
props.onNotify('Cannot check for new updates. See the browser console.');
77+
console.error(`Got status code ${this.status} from Github API with response ${this.responseText}`);
78+
}
79+
});
80+
81+
oReq.open('GET', 'https://api.github.com/repos/joxit/docker-registry-ui/releases/latest');
82+
oReq.send();
83+
},
84+
isNewestVersion,
85+
};
86+
</script>
87+
<style>
88+
:host {
89+
display: inline;
90+
}
91+
:host svg {
92+
margin-left: 10px;
93+
cursor: pointer;
94+
}
95+
material-popup material-button > a:first-child {
96+
display: flex;
97+
align-items: center;
98+
}
99+
material-popup .material-popup-content code {
100+
background-color: var(--hover-background);
101+
padding: 0 5px;
102+
border-radius: 4px;
103+
line-height: 1.5em;
104+
}
105+
material-popup .material-popup-content b {
106+
color: var(--accent-text);
107+
}
108+
</style>
109+
</version-notification>

src/images/rocket.svg

+20
Loading

src/scripts/utils.js

+37
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,40 @@ export function truthy(value) {
220220
export function stringToArray(value) {
221221
return value && typeof value === 'string' ? value.split(',') : [];
222222
}
223+
224+
const compareNumbers = (a, b) => {
225+
const na = parseInt(a);
226+
const nb = parseInt(b);
227+
if (na > nb) return 1;
228+
if (nb > na) return -1;
229+
if (!isNaN(na) && isNaN(nb)) return 1;
230+
if (isNaN(na) && !isNaN(nb)) return -1;
231+
return 0;
232+
};
233+
234+
export function isNewestVersion(current = '0.0.0', release = '0.0.0') {
235+
if (current === release) {
236+
return true;
237+
}
238+
current = current.split('.');
239+
release = release.split('.');
240+
const isDev = current[2].indexOf('-') >= 0;
241+
const major = compareNumbers(current[0], release[0]);
242+
const minor = compareNumbers(current[1], release[1]);
243+
const patch = compareNumbers(current[2], release[2]);
244+
if (!isDev && (major > 0 || (major === 0 && minor > 0) || (major === 0 && minor === 0 && patch >= 0))) {
245+
return true;
246+
} else if (isDev && (major > 0 || (major === 0 && minor > 0))) {
247+
return true;
248+
}
249+
return false;
250+
}
251+
252+
export function parseJSON(json) {
253+
if (!json) {
254+
return;
255+
}
256+
try {
257+
return JSON.parse(json);
258+
} catch (e) {}
259+
}

test/taglist-order.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { getTagComparator } from '../src/scripts/taglist-order.js';
33
import { DockerRegistryUIError } from '../src/scripts/error.js';
44
import assert from 'assert';
55

6-
describe('utils tests', () => {
6+
describe('taglist-order tests', () => {
77
describe('taglistOrderVariants', () => {
88
it(`should return the input when it's well formed and num first`, () => {
99
const expected = ['num-asc;alpha-asc', 'num-asc;alpha-desc', 'num-desc;alpha-asc', 'num-desc;alpha-asc'];

test/utils.test.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { isNewestVersion } from '../src/scripts/utils.js';
2+
import assert from 'assert';
3+
4+
describe('utils tests', () => {
5+
describe('isNewestVersion', () => {
6+
it(`should return true for the same version`, () => {
7+
const expected = ['2.0.0', '2.4.1', '2.5.0', null, undefined];
8+
expected.forEach((e) => assert.ok(isNewestVersion(e, e)));
9+
});
10+
11+
it(`should return true with on common versions`, () => {
12+
assert.ok(isNewestVersion('2.5.1', '2.5.0'));
13+
assert.ok(isNewestVersion('2.5.0', '2.0.0'));
14+
assert.ok(isNewestVersion('2.15.0', '1.25.10'));
15+
assert.ok(isNewestVersion('10.10.10', '2.25.20'));
16+
});
17+
18+
it(`should return false on common versions`, () => {
19+
assert.equal(isNewestVersion('1.0.0', '2.5.0'), false);
20+
assert.equal(isNewestVersion('10.10.10', '20.20.20'), false);
21+
assert.equal(isNewestVersion('2.4.10', '2.5.0'), false);
22+
assert.equal(isNewestVersion('2.5.0', '2.6.0'), false);
23+
});
24+
25+
it(`should return true for -dev next versions`, () => {
26+
assert.ok(isNewestVersion('2.5.0-dev', '2.4.1'));
27+
assert.ok(isNewestVersion('2.6.0-dev', '2.5.0'));
28+
assert.ok(isNewestVersion('2.15.0-dev', '2.14.1'));
29+
assert.ok(isNewestVersion('2.15.0-dev', '1.16.0'));
30+
});
31+
32+
it(`should return false for -dev with current minor version`, () => {
33+
assert.equal(isNewestVersion('2.5.0-dev', '2.5.0'), false);
34+
assert.equal(isNewestVersion('2.5.0-dev', '2.5.10'), false);
35+
assert.equal(isNewestVersion('2.15.0-dev', '2.15.0'), false);
36+
assert.equal(isNewestVersion('2.0.0-dev', '2.15.0'), false);
37+
});
38+
it(`should return true for -{commit sha} next versions`, () => {
39+
assert.ok(isNewestVersion('2.5.0-ffb6d14baf', '2.4.1'));
40+
assert.ok(isNewestVersion('2.6.0-ffb6d14baf', '2.5.0'));
41+
assert.ok(isNewestVersion('2.15.0-ffb6d14baf', '2.14.1'));
42+
assert.ok(isNewestVersion('2.15.0-ffb6d14baf', '1.16.0'));
43+
});
44+
45+
it(`should return false for -{commit sha} with current minor version`, () => {
46+
assert.equal(isNewestVersion('2.5.0-ffb6d14baf', '2.5.0'), false);
47+
assert.equal(isNewestVersion('2.5.0-ffb6d14baf', '2.5.10'), false);
48+
assert.equal(isNewestVersion('2.15.0-ffb6d14baf', '2.15.0'), false);
49+
assert.equal(isNewestVersion('2.0.0-ffb6d14baf', '2.15.0'), false);
50+
});
51+
});
52+
});

0 commit comments

Comments
 (0)