chore(cleanup): format code and move from js-beautify to prettier

This commit is contained in:
Joxit 2022-04-09 00:02:52 +02:00
parent ab12cceefc
commit 49fcba3f6c
No known key found for this signature in database
GPG Key ID: F526592B8E012263
21 changed files with 373 additions and 302 deletions

View File

@ -2,6 +2,10 @@
"name": "docker-registry-ui",
"version": "2.1.0",
"scripts": {
"format": "npm run format-html && npm run format-js && npm run format-riot",
"format-html": "find src rollup rollup.config.js -name '*.html' -exec prettier --config .prettierrc -w --parser html {} \\;",
"format-js": "find src rollup rollup.config.js -name '*.js' -exec prettier --config .prettierrc -w {} \\;",
"format-riot": "find src rollup rollup.config.js -name '*.riot' -exec prettier --config .prettierrc -w --parser html {} \\;",
"start": "rollup -c -w --environment ROLLUP_SERVE:true",
"build": "rollup -c",
"build:electron": "npm run build && cd examples/electron && npm install && npm run dist"
@ -25,8 +29,8 @@
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.1.3",
"core-js": "^3.19.1",
"js-beautify": "^1.14.0",
"node-sass": "^7.0.1",
"prettier": "^2.6.2",
"riot": "^6.1.2",
"riot-mui": "github:joxit/riot-5-mui#4d68d7f",
"rollup": "^2.59.0",

View File

@ -15,4 +15,4 @@ export default `/*
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @license AGPL
*/`
*/`;

View File

@ -96,10 +96,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
onAuthentication: props.onAuthentication,
});
oReq.addEventListener('load', function () {
if (this.status == 200) {
if (this.status === 200) {
const nbTags = (JSON.parse(this.responseText).tags || []).length;
self.update({ nbTags });
} else if (this.status == 404) {
} else if (this.status === 404) {
props.onNotify('Server not found', true);
} else {
props.onNotify(this.responseText, true);

View File

@ -73,14 +73,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
onAuthentication: this.props.onAuthentication,
});
oReq.addEventListener('load', function () {
if (this.status == 200) {
if (this.status === 200) {
repositories = JSON.parse(this.responseText).repositories || [];
repositories.sort();
repositories = repositories.reduce(function (acc, e) {
const slash = e.indexOf('/');
if (slash > 0) {
const repoName = e.substring(0, slash) + '/';
if (acc.length == 0 || acc[acc.length - 1].repo != repoName) {
if (acc.length === 0 || acc[acc.length - 1].repo != repoName) {
acc.push({
repo: repoName,
images: [],
@ -92,7 +92,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
acc.push(e);
return acc;
}, []);
} else if (this.status == 404) {
} else if (this.status === 404) {
self.props.onNotify('Server not found', true);
} else {
self.props.onNotify(this.responseText);

View File

@ -31,9 +31,7 @@
</div>
</material-popup>
<script>
import {
addRegistryServers
} from '../../scripts/utils';
import { addRegistryServers } from '../../scripts/utils';
import router from '../../scripts/router';
export default {
@ -52,11 +50,11 @@
return this.props.onNotify('The input field should start with http:// or https://.', true);
}
const url = addRegistryServers(input.value);
router.home()
router.home();
this.props.onServerChange(url);
this.props.onClose()
this.props.onClose();
setTimeout(() => router.updateUrlQueryParam(url), 100);
}
}
},
};
</script>
</add-registry-url>
</add-registry-url>

View File

@ -32,10 +32,7 @@
</div>
</material-popup>
<script>
import {
addRegistryServers,
getRegistryServers
} from '../../scripts/utils';
import { addRegistryServers, getRegistryServers } from '../../scripts/utils';
import router from '../../scripts/router';
export default {
change(event) {
@ -47,13 +44,13 @@
return this.props.onNotify('The select field should start with http:// or https://.', true);
}
const url = addRegistryServers(select.value);
router.home()
router.home();
this.props.onServerChange(url);
this.props.onClose()
this.props.onClose();
setTimeout(() => router.updateUrlQueryParam(url), 100);
},
getRegistryServers
}
getRegistryServers,
};
</script>
<style>
:host select {
@ -74,4 +71,4 @@
margin: 1.5em 0;
}
</style>
</change-registry-url>
</change-registry-url>

View File

@ -52,7 +52,7 @@
const oReq = new Http({ onAuthentication });
const self = this;
oReq.addEventListener('loadend', function () {
if (this.status == 200 || this.status == 202) {
if (this.status === 200 || this.status === 202) {
oReq.getContentDigest(function (digest) {
if (!digest) {
onNotify(ERROR_CAN_NOT_READ_CONTENT_DIGEST);
@ -60,7 +60,7 @@
self.deleteImage({ name, tag, digest }, opts);
}
});
} else if (this.status == 404) {
} else if (this.status === 404) {
onNotify(`Manifest for ${name}:${tag} not found`, true);
} else {
onNotify(this.responseText);
@ -77,10 +77,10 @@
const { registryUrl, ignoreError, onNotify, onAuthentication, onClick } = opts;
const oReq = new Http({ onAuthentication });
oReq.addEventListener('loadend', function () {
if (this.status == 200 || this.status == 202) {
if (this.status === 200 || this.status === 202) {
router.taglist(name);
onNotify(`Deleting ${name}:${tag} image. Run \`registry garbage-collect config.yml\` on your registry`);
} else if (this.status == 404) {
} else if (this.status === 404) {
ignoreError ||
onNotify({
message: 'Digest not found for this image in your registry.',

View File

@ -15,18 +15,35 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<dialogs-menu>
<add-registry-url if="{ !props.readOnlyRegistries }" opened="{ state['add-registry-url'] }" on-close="{ onClose('add-registry-url') }"
on-notify="{ props.onNotify }" on-server-change="{ props.onServerChange }"></add-registry-url>
<change-registry-url opened="{ state['change-registry-url'] }" on-close="{ onClose('change-registry-url') }"
on-notify="{ props.onNotify }" on-server-change="{ props.onServerChange }"></change-registry-url>
<remove-registry-url if="{ !props.readOnlyRegistries }" opened="{ state['remove-registry-url'] }" on-close="{ onClose('remove-registry-url') }"
on-notify="{ props.onNotify }" on-server-change="{ props.onServerChange }"></remove-registry-url>
<add-registry-url
if="{ !props.readOnlyRegistries }"
opened="{ state['add-registry-url'] }"
on-close="{ onClose('add-registry-url') }"
on-notify="{ props.onNotify }"
on-server-change="{ props.onServerChange }"
></add-registry-url>
<change-registry-url
opened="{ state['change-registry-url'] }"
on-close="{ onClose('change-registry-url') }"
on-notify="{ props.onNotify }"
on-server-change="{ props.onServerChange }"
></change-registry-url>
<remove-registry-url
if="{ !props.readOnlyRegistries }"
opened="{ state['remove-registry-url'] }"
on-close="{ onClose('remove-registry-url') }"
on-notify="{ props.onNotify }"
on-server-change="{ props.onServerChange }"
></remove-registry-url>
<div class="container">
<material-button onClick="{ onClick }" waves-center="true" rounded="true" waves-opacity="0.6" waves-duration="600">
<i class="material-icons">more_vert</i>
</material-button>
<material-dropdown-list items="{ dropdownItems.filter(item => item.ro || !props.readOnlyRegistries) }" onSelect="{ onDropdownSelect }"
opened="{ state.isDropdownOpened }" />
<material-dropdown-list
items="{ dropdownItems.filter(item => item.ro || !props.readOnlyRegistries) }"
onSelect="{ onDropdownSelect }"
opened="{ state.isDropdownOpened }"
/>
</div>
<div class="overlay" onclick="{ onClick }" if="{ state.isDropdownOpened }"></div>
<script>
@ -38,44 +55,48 @@
components: {
AddRegistryUrl,
ChangeRegistryUrl,
RemoveRegistryUrl
RemoveRegistryUrl,
},
dropdownItems: [{
title: 'Add URL',
name: 'add-registry-url',
ro: false
}, {
title: 'Change URL',
name: 'change-registry-url',
ro: true
}, {
title: 'Remove URL',
name: 'remove-registry-url',
ro: false
}],
dropdownItems: [
{
title: 'Add URL',
name: 'add-registry-url',
ro: false,
},
{
title: 'Change URL',
name: 'change-registry-url',
ro: true,
},
{
title: 'Remove URL',
name: 'remove-registry-url',
ro: false,
},
],
onDropdownSelect(key, item) {
this.update({
[item.name]: true,
isDropdownOpened: false
isDropdownOpened: false,
});
},
onClose(name) {
return () => {
this.update({
[name]: false,
isDropdownOpened: false
})
}
isDropdownOpened: false,
});
};
},
onClick() {
this.update({
isDropdownOpened: !this.state.isDropdownOpened
})
}
}
isDropdownOpened: !this.state.isDropdownOpened,
});
},
};
</script>
<style>
:host > .container{
:host > .container {
position: absolute;
top: 0px;
right: 16px;
@ -128,4 +149,4 @@
line-height: 36px;
}
</style>
</dialogs-menu>
</dialogs-menu>

View File

@ -21,8 +21,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<ul class="list">
<li each="{ url in getRegistryServers() }">
<span>
<material-button onClick="{ remove }" url="{ url }" rounded="true" waves-color="rgba(158,158,158,.4)"
waves-center="true">
<material-button
onClick="{ remove }"
url="{ url }"
rounded="true"
waves-color="rgba(158,158,158,.4)"
waves-center="true"
>
<i class="material-icons">delete</i>
</material-button>
<span class="url">{ url }</span>
@ -37,18 +42,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
</material-popup>
<script>
import {
getRegistryServers,
removeRegistryServers
} from '../../scripts/utils';
import { getRegistryServers, removeRegistryServers } from '../../scripts/utils';
export default {
remove(event) {
const url = event.currentTarget.attributes.url && event.currentTarget.attributes.url.value;
removeRegistryServers(url);
setTimeout(() => this.update(), 100);
},
getRegistryServers
}
getRegistryServers,
};
</script>
<style>
:host material-popup .popup material-button {
@ -59,4 +61,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
color: #777;
}
</style>
</remove-registry-url>
</remove-registry-url>

View File

@ -19,35 +19,63 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<material-navbar>
<div class="logo">Docker Registry UI</div>
<search-bar on-search="{ onSearch }"></search-bar>
<dialogs-menu if="{props.singleRegistry !== 'true'}" on-notify="{ notifySnackbar }"
on-server-change="{ onServerChange }" default-registries="{ props.defaultRegistries }"
read-only-registries="{ truthy(props.readOnlyRegistries) }"></dialogs-menu>
<dialogs-menu
if="{props.singleRegistry !== 'true'}"
on-notify="{ notifySnackbar }"
on-server-change="{ onServerChange }"
default-registries="{ props.defaultRegistries }"
read-only-registries="{ truthy(props.readOnlyRegistries) }"
></dialogs-menu>
</material-navbar>
</header>
<main>
<router base="#!">
<route path="{baseRoute}">
<catalog registry-url="{ state.registryUrl }" registry-name="{ state.name }"
catalog-elements-limit="{ state.catalogElementsLimit }" on-notify="{ notifySnackbar }"
filter-results="{ state.filter }" on-authentication="{ onAuthentication }"
show-catalog-nb-tags="{ truthy(props.showCatalogNbTags) }"/>
<catalog
registry-url="{ state.registryUrl }"
registry-name="{ state.name }"
catalog-elements-limit="{ state.catalogElementsLimit }"
on-notify="{ notifySnackbar }"
filter-results="{ state.filter }"
on-authentication="{ onAuthentication }"
show-catalog-nb-tags="{ truthy(props.showCatalogNbTags) }"
/>
</route>
<route path="{baseRoute}taglist/(.*)">
<tag-list registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }"
image="{ router.getTagListImage() }" show-content-digest="{ truthy(props.showContentDigest) }"
is-image-remove-activated="{ truthy(props.isImageRemoveActivated) }" on-notify="{ notifySnackbar }"
filter-results="{ state.filter }" on-authentication="{ onAuthentication }"></tag-list>
<tag-list
registry-url="{ state.registryUrl }"
registry-name="{ state.name }"
pull-url="{ state.pullUrl }"
image="{ router.getTagListImage() }"
show-content-digest="{ truthy(props.showContentDigest) }"
is-image-remove-activated="{ truthy(props.isImageRemoveActivated) }"
on-notify="{ notifySnackbar }"
filter-results="{ state.filter }"
on-authentication="{ onAuthentication }"
></tag-list>
</route>
<route path="{baseRoute}taghistory/(.*)">
<tag-history registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }"
image="{ router.getTagHistoryImage() }" tag="{ router.getTagHistoryTag() }"
is-image-remove-activated="{ truthy(props.isImageRemoveActivated) }" on-notify="{ notifySnackbar }"
on-authentication="{ onAuthentication }" history-custom-labels="{ stringToArray(props.historyCustomLabels) }"></tag-history>
<tag-history
registry-url="{ state.registryUrl }"
registry-name="{ state.name }"
pull-url="{ state.pullUrl }"
image="{ router.getTagHistoryImage() }"
tag="{ router.getTagHistoryTag() }"
is-image-remove-activated="{ truthy(props.isImageRemoveActivated) }"
on-notify="{ notifySnackbar }"
on-authentication="{ onAuthentication }"
history-custom-labels="{ stringToArray(props.historyCustomLabels) }"
></tag-history>
</route>
</router>
<registry-authentication realm="{ state.realm }" scope="{ state.scope }" service="{ state.service }"
on-close="{ onAuthenticationClose }" on-authenticated="{ state.onAuthenticated }"
opened="{ state.authenticationDialogOpened }"></registry-authentication>
<registry-authentication
realm="{ state.realm }"
scope="{ state.scope }"
service="{ state.service }"
on-close="{ onAuthenticationClose }"
on-authenticated="{ state.onAuthenticated }"
opened="{ state.authenticationDialogOpened }"
></registry-authentication>
<material-snackbar message="{ state.snackbarMessage }" is-error="{ state.snackbarIsError }"></material-snackbar>
</main>
<footer>
@ -64,25 +92,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</material-footer>
</footer>
<script>
import {
version
} from '../../package.json';
import {
Router,
Route,
} from '@riotjs/route'
import { version } from '../../package.json';
import { Router, Route } from '@riotjs/route';
import Catalog from './catalog/catalog.riot';
import TagList from './tag-list/tag-list.riot';
import TagHistory from './tag-history/tag-history.riot';
import DialogsMenu from './dialogs/dialogs-menu.riot';
import SearchBar from './search-bar.riot'
import {
stripHttps,
getRegistryServers,
setRegistryServers,
truthy,
stringToArray
} from '../scripts/utils';
import SearchBar from './search-bar.riot';
import { stripHttps, getRegistryServers, setRegistryServers, truthy, stringToArray } from '../scripts/utils';
import router from '../scripts/router';
export default {
@ -93,22 +110,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
DialogsMenu,
SearchBar,
Router,
Route
Route,
},
onUpdated(props, state) {
state.snackbarIsError = false;
state.snackbarMessage = undefined;
},
onBeforeMount(props) {
if ((props.defaultRegistries && props.defaultRegistries.length > 0 && getRegistryServers().length === 0) ||
truthy(props.readOnlyRegistries)) {
if (
(props.defaultRegistries && props.defaultRegistries.length > 0 && getRegistryServers().length === 0) ||
truthy(props.readOnlyRegistries)
) {
setRegistryServers(props.defaultRegistries);
}
// props.singleRegistry === 'true' means old static version
const registryUrl = props.registryUrl ||
(props.singleRegistry === 'true' ? undefined : (router.getUrlQueryParam() || getRegistryServers(0))) ||
(window.location.origin + window.location.pathname.replace(/\/+$/, ''));
// props.singleRegistry === 'true' means old static version
const registryUrl =
props.registryUrl ||
(props.singleRegistry === 'true' ? undefined : router.getUrlQueryParam() || getRegistryServers(0)) ||
window.location.origin + window.location.pathname.replace(/\/+$/, '');
this.state.registryUrl = registryUrl.replace(/\/$/, '').replace(/index(\.html?)?$/, '');
this.state.name = props.name || stripHttps(props.registryUrl);
this.state.catalogElementsLimit = props.catalogElementsLimit || 100000;
@ -119,65 +139,59 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
registryUrl,
name: stripHttps(registryUrl),
pullUrl: this.pullUrl(registryUrl),
snackbarMessage: 'Registry server changed to `' + registryUrl + '`.'
})
snackbarMessage: 'Registry server changed to `' + registryUrl + '`.',
});
},
onAuthentication(opts, onAuthenticated) {
if (opts && opts.realm && opts.service && opts.scope) {
const {
realm,
service,
scope,
} = opts;
const req = new XMLHttpRequest()
const { realm, service, scope } = opts;
const req = new XMLHttpRequest();
req.addEventListener('loadend', () => {
try {
const bearer = JSON.parse(req.responseText);
onAuthenticated(bearer)
onAuthenticated(bearer);
} catch (e) {
this.notifySnackbar(`Failed to log in: ${e.message}`, true)
this.notifySnackbar(`Failed to log in: ${e.message}`, true);
}
})
req.open('GET', `${realm}?service=${service}&scope=${scope}`)
req.send()
});
req.open('GET', `${realm}?service=${service}&scope=${scope}`);
req.send();
} else {
onAuthenticated()
onAuthenticated();
}
},
onAuthenticationClose() {
this.update({
authenticationDialogOpened: false
})
authenticationDialogOpened: false,
});
},
pullUrl(registryUrl, pullUrl) {
const url = pullUrl ||
(registryUrl && registryUrl.length > 0 && registryUrl) ||
window.location.host;
const url = pullUrl || (registryUrl && registryUrl.length > 0 && registryUrl) || window.location.host;
return stripHttps(url);
},
notifySnackbar(message, isError) {
if (typeof message === 'string') {
this.update({
snackbarMessage: message,
snackbarIsError: isError || false
snackbarIsError: isError || false,
});
} else if (message && message.message) {
this.update({
snackbarMessage: message.message,
snackbarIsError: message.isError
snackbarIsError: message.isError,
});
}
},
onSearch(value) {
this.update({
filter: value
})
filter: value,
});
},
baseRoute: '([^#]*?)/(\\?[^#]*?)?(#!)?(/?)',
router,
version,
truthy,
stringToArray
}
stringToArray,
};
</script>
</docker-registry-ui>
</docker-registry-ui>

View File

@ -1,9 +1,7 @@
<search-bar>
<material-input placeholder="Search in page"></material-input>
<script>
import {
router
} from '@riotjs/route';
import { router } from '@riotjs/route';
export default {
onMounted(props, state) {
@ -11,16 +9,16 @@
let value = '';
const notify = () => {
if (value !== input.value) {
props.onSearch(input.value.toLowerCase())
props.onSearch(input.value.toLowerCase());
}
value = input.value;
}
};
input.addEventListener('keyup', notify);
router.on.value(() => {
input.value = '';
notify();
})
window.addEventListener('keydown', e => {
});
window.addEventListener('keydown', (e) => {
// F3 or CTRL + F
if (e.keyCode === 114 || (e.ctrlKey && e.keyCode === 70)) {
// already focused, fallback to default behavior
@ -31,9 +29,9 @@
input.focus();
}
}
})
}
}
});
},
};
export function matchSearch(search, value) {
return !search || (value && value.toLowerCase().indexOf(search) >= 0);
@ -57,4 +55,4 @@
color: #fff;
}
</style>
</search-bar>
</search-bar>

View File

@ -16,16 +16,19 @@
-->
<copy-to-clipboard>
<div class="copy-to-clipboard">
<input style="display: none; width: 1px; height: 1px;" value="{ getDockerCmd(props) }">
<material-button waves-center="true" rounded="true" waves-color="#ddd" onClick="{ copy }"
title="Copy pull command.">
<input style="display: none; width: 1px; height: 1px" value="{ getDockerCmd(props) }" />
<material-button
waves-center="true"
rounded="true"
waves-color="#ddd"
onClick="{ copy }"
title="Copy pull command."
>
<i class="material-icons">content_copy</i>
</material-button>
</div>
<script>
import {
ERROR_CAN_NOT_READ_CONTENT_DIGEST
} from '../../scripts/utils';
import { ERROR_CAN_NOT_READ_CONTENT_DIGEST } from '../../scripts/utils';
export default {
onMounted(props, state) {
this.load(props, state);
@ -37,13 +40,13 @@
if (props.target === 'tag') {
return `docker pull ${props.pullUrl}/${props.image.name}:${props.image.tag}`;
} else {
return `docker pull ${props.pullUrl}/${props.image.name}@${props.image.digest}`
return `docker pull ${props.pullUrl}/${props.image.name}@${props.image.digest}`;
}
},
load(props, state) {
if (props.target !== 'tag' && !props.image.digest) {
props.image.one('content-digest', (digest) => {
this.update()
this.update();
});
props.image.trigger('get-content-digest');
}
@ -60,8 +63,8 @@
document.execCommand('copy');
copyText.style.display = 'none';
this.props.onNotify('`' + copyText.value + '` has been copied to clipboard.')
}
}
this.props.onNotify('`' + copyText.value + '` has been copied to clipboard.');
},
};
</script>
</copy-to-clipboard>
</copy-to-clipboard>

View File

@ -39,12 +39,12 @@ Copyright (C) 2016-2021 Jones Magloire @Joxit
onResize(chars) {
if (chars !== this.state.chars) {
this.update({
chars
chars,
});
}
},
getTitle(image, chars) {
return chars >= 70 ? '' : (image.digest || '');
return chars >= 70 ? '' : image.digest || '';
},
getDigest(image, chars) {
if (chars >= 70) {
@ -54,7 +54,7 @@ Copyright (C) 2016-2021 Jones Magloire @Joxit
} else {
return image.digest && image.digest.slice(0, chars) + '...';
}
}
}
},
};
</script>
</image-content-digest>
</image-content-digest>

View File

@ -17,9 +17,7 @@
<image-size>
<div title="Compressed size of your image.">{ getImageSize(props.image) }</div>
<script>
import {
bytesToSize,
} from '../../scripts/utils';
import { bytesToSize } from '../../scripts/utils';
export default {
onMounted(props, state) {
this.load(props, state);
@ -33,15 +31,14 @@
}
props.image.on('size', (size) => {
this.update({
size
size,
});
});
props.image.trigger('get-size');
},
getImageSize(image) {
return bytesToSize(image.size)
}
}
return bytesToSize(image.size);
},
};
</script>
</image-size>
</image-size>

View File

@ -17,14 +17,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<pagination>
<div class="conatianer">
<div class="pagination-centered">
<material-button waves-color="rgba(158,158,158,.4)" each="{p in props.pages}"
<material-button
waves-color="rgba(158,158,158,.4)"
each="{p in props.pages}"
class="{ p.current ? 'current' : ''} { p['space-left'] ? 'space-left' : '' } { p['space-right'] ? 'space-right' : ''}"
onClick="{() => props.onPageUpdate(p.page)}">
onClick="{() => props.onPageUpdate(p.page)}"
>
<i if="{ p.icon }" class="material-icons">{ p.icon }</i>
<div if="{ !p.icon }">{ p.page }</div>
</material-button>
</div>
</div>
<script>
</script>
</pagination>
<script></script>
</pagination>

View File

@ -15,29 +15,35 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<remove-image>
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete the image."
if="{ !props.multiDelete }" disabled="{ !state.digest }" onClick="{ deleteImage }">
<material-button
waves-center="true"
rounded="true"
waves-color="#ddd"
title="This will delete the image."
if="{ !props.multiDelete }"
disabled="{ !state.digest }"
onClick="{ deleteImage }"
>
<i class="material-icons">delete</i>
</material-button>
<material-checkbox if="{ props.multiDelete }" title="Select this tag to delete it." disabled="{ !state.digest }"
onChange="{ handleCheckboxChange }" checked="{ state.checked }">
<material-checkbox
if="{ props.multiDelete }"
title="Select this tag to delete it."
disabled="{ !state.digest }"
onChange="{ handleCheckboxChange }"
checked="{ state.checked }"
>
</material-checkbox>
<script>
import {
Http
} from '../../scripts/http';
import router from '../../scripts/router'
import {
ACTION_CHECK_TO_DELETE,
ACTION_UNCHECK_TO_DELETE,
ACTION_DELETE_IMAGE
} from './tag-table.riot';
import { Http } from '../../scripts/http';
import router from '../../scripts/router';
import { ACTION_CHECK_TO_DELETE, ACTION_UNCHECK_TO_DELETE, ACTION_DELETE_IMAGE } from './tag-table.riot';
export default {
onBeforeMount(props, state) {
state.checked = props.checked;
props.image.one('content-digest', (digest) => {
this.update({
digest
digest,
});
});
},
@ -53,7 +59,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
handleCheckboxChange(checked) {
const action = checked ? ACTION_CHECK_TO_DELETE : ACTION_UNCHECK_TO_DELETE;
this.props.handleCheckboxChange(action, this.props.image);
}
}
},
};
</script>
</remove-image>
</remove-image>

View File

@ -16,15 +16,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<tag-list>
<material-card class="header">
<div class="material-card-title-action ">
<div class="material-card-title-action">
<material-button waves-center="true" rounded="true" waves-color="#ddd" onClick="{ router.home }">
<i class="material-icons">arrow_back</i>
</material-button>
<h2>
Tags of { props.image }
<div class="source-hint">
Sourced from { state.registryName + '/' + props.image }
</div>
<div class="source-hint">Sourced from { state.registryName + '/' + props.image }</div>
<div class="item-count">{ state.tags.length } tags</div>
</h2>
</div>
@ -36,30 +34,31 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination>
<tag-table if="{ state.loadend }" tags="{state.tags}" asc="{state.asc}" page="{ state.page }"
show-content-digest="{props.showContentDigest}" is-image-remove-activated="{props.isImageRemoveActivated}"
onReverseOrder="{ onReverseOrder }" registry-url="{ props.registryUrl }" pull-url="{ props.pullUrl }"
on-notify="{ props.onNotify }" filter-results="{ props.filterResults }"
on-authentication="{ props.onAuthentication }">
<tag-table
if="{ state.loadend }"
tags="{state.tags}"
asc="{state.asc}"
page="{ state.page }"
show-content-digest="{props.showContentDigest}"
is-image-remove-activated="{props.isImageRemoveActivated}"
onReverseOrder="{ onReverseOrder }"
registry-url="{ props.registryUrl }"
pull-url="{ props.pullUrl }"
on-notify="{ props.onNotify }"
filter-results="{ props.filterResults }"
on-authentication="{ props.onAuthentication }"
>
</tag-table>
<pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination>
<script>
import {
Http
} from '../../scripts/http';
import {
DockerImage,
compare
} from '../../scripts/docker-image';
import {
getNumPages,
getPageLabels
} from '../../scripts/utils'
import Pagination from './pagination.riot'
import TagTable from './tag-table.riot'
import router from '../../scripts/router'
import { Http } from '../../scripts/http';
import { DockerImage, compare } from '../../scripts/docker-image';
import { getNumPages, getPageLabels } from '../../scripts/utils';
import Pagination from './pagination.riot';
import TagTable from './tag-table.riot';
import router from '../../scripts/router';
export default {
components: {
Pagination,
@ -71,11 +70,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
tags: [],
loadend: false,
asc: true,
page: router.getPageQueryParam() || 1
}
page: router.getPageQueryParam() || 1,
};
},
onMounted(props, state) {
this.display(props, state)
this.display(props, state);
window.addEventListener('resize', this.onResize);
// this may be run before the final document size is available, so schedule
// a correction once everything is set up.
@ -85,23 +84,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
state.tags = [];
const self = this;
const oReq = new Http({
onAuthentication: props.onAuthentication
onAuthentication: props.onAuthentication,
});
oReq.addEventListener('load', function () {
if (this.status == 200) {
if (this.status === 200) {
const tags = (JSON.parse(this.responseText).tags || [])
.map(tag => new DockerImage(props.image, tag, {
registryUrl: props.registryUrl,
onNotify: props.onNotify,
onAuthentication: props.onAuthentication
}))
.map(
(tag) =>
new DockerImage(props.image, tag, {
registryUrl: props.registryUrl,
onNotify: props.onNotify,
onAuthentication: props.onAuthentication,
})
)
.sort(compare);
window.requestAnimationFrame(self.onResize);
self.update({
page: Math.min(state.page, getNumPages(tags)),
tags
})
} else if (this.status == 404) {
tags,
});
} else if (this.status === 404) {
self.props.onNotify('Server not found', true);
} else {
self.props.onNotify(this.responseText, true);
@ -113,7 +115,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
});
oReq.addEventListener('loadend', function () {
self.update({
loadend: true
loadend: true,
});
});
oReq.open('GET', props.registryUrl + '/v2/' + props.image + '/tags/list');
@ -123,7 +125,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
onPageUpdate(page) {
this.update({
page: page
page: page,
});
router.updatePageQueryParam(page);
},
@ -143,8 +145,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// SHA256:12345678 + scaled between 1024 and 1440px
chars = 15 + 56 * ((innerWidth - 1024) / 416);
}
if (max > 20) chars -= (max - 20);
chars = Math.floor(chars)
if (max > 20) chars -= max - 20;
chars = Math.floor(chars);
this.state.tags.map(function (image) {
image.trigger('content-digest-chars', chars);
});
@ -162,7 +164,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
},
getPageLabels,
getNumPages,
router
}
router,
};
</script>
</tag-list>
</tag-list>

View File

@ -15,39 +15,61 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<tag-table>
<confirm-delete-image opened="{ state.confirmDeleteImage }" on-click="{ onConfirmDeleteImageClick }"
registry-url="{ props.registryUrl }" on-notify="{ props.onNotify }" on-authentication="{ props.onAuthentication }"
tags="{ props.tags }" to-delete="{ state.toDelete }"></confirm-delete-image>
<confirm-delete-image
opened="{ state.confirmDeleteImage }"
on-click="{ onConfirmDeleteImageClick }"
registry-url="{ props.registryUrl }"
on-notify="{ props.onNotify }"
on-authentication="{ props.onAuthentication }"
tags="{ props.tags }"
to-delete="{ state.toDelete }"
></confirm-delete-image>
<material-card class="taglist">
<table style="border: none;">
<table style="border: none">
<thead>
<tr>
<th
class="creation-date { (state.desc && state.orderType === 'date') ? 'material-card-th-sorted-descending' : 'material-card-th-sorted-ascending' }"
onclick="{() => onPageReorder('date') }">
onclick="{() => onPageReorder('date') }"
>
Creation date
</th>
<th
class="image-size { (state.desc && state.orderType === 'size') ? 'material-card-th-sorted-descending' : 'material-card-th-sorted-ascending' }"
onclick="{() => onPageReorder('size') }">
onclick="{() => onPageReorder('size') }"
>
Size
</th>
<th id="image-content-digest-header" if="{ props.showContentDigest }">Content Digest</th>
<th id="image-tag-header"
<th
id="image-tag-header"
class="{ props.asc ? 'material-card-th-sorted-ascending' : 'material-card-th-sorted-descending' }"
onclick="{ onReverseOrder }">Tag
onclick="{ onReverseOrder }"
>
Tag
</th>
<th class="show-tag-history">History</th>
<th class="remove-tag { state.toDelete.size > 0 && !state.singleDeleteAction ? 'delete' : '' }"
if="{ props.isImageRemoveActivated }">
<material-checkbox class="indeterminate" checked="{ state.multiDelete }"
<th
class="remove-tag { state.toDelete.size > 0 && !state.singleDeleteAction ? 'delete' : '' }"
if="{ props.isImageRemoveActivated }"
>
<material-checkbox
class="indeterminate"
checked="{ state.multiDelete }"
if="{ state.toDelete.size === 0 || state.singleDeleteAction }"
title="Toggle multi-delete. Alt+Click to select all tags." onChange="{ onRemoveImageHeaderChange }">
title="Toggle multi-delete. Alt+Click to select all tags."
onChange="{ onRemoveImageHeaderChange }"
>
</material-checkbox>
<material-button waves-center="true" rounded="true" waves-color="#ddd"
title="This will delete selected images." onClick="{ deleteImages }"
if="{ state.toDelete.size > 0 && !state.singleDeleteAction }">
<material-button
waves-center="true"
rounded="true"
waves-color="#ddd"
title="This will delete selected images."
onClick="{ deleteImages }"
if="{ state.toDelete.size > 0 && !state.singleDeleteAction }"
>
<i class="material-icons">delete</i>
</material-button>
</th>
@ -63,30 +85,42 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</td>
<td if="{ props.showContentDigest }">
<image-content-digest image="{ image }" />
<copy-to-clipboard target="digest" image="{ image }" pull-url="{ props.pullUrl }"
on-notify="{ props.onNotify }" />
<copy-to-clipboard
target="digest"
image="{ image }"
pull-url="{ props.pullUrl }"
on-notify="{ props.onNotify }"
/>
</td>
<td>
<image-tag image="{ image }" />
<copy-to-clipboard target="tag" image="{ image }" pull-url="{ props.pullUrl }"
on-notify="{ props.onNotify }" />
<copy-to-clipboard
target="tag"
image="{ image }"
pull-url="{ props.pullUrl }"
on-notify="{ props.onNotify }"
/>
</td>
<td class="show-tag-history">
<tag-history-button image="{ image }" />
</td>
<td if="{ props.isImageRemoveActivated }" class="remove-tag">
<remove-image multi-delete="{ state.multiDelete }" image="{ image }" registry-url="{ props.registryUrl }"
handleCheckboxChange="{ onRemoveImageChange }" checked="{ state.toDelete.has(image) }"
on-notify="{ props.onNotify }" on-authentication="{ props.onAuthentication }" />
<remove-image
multi-delete="{ state.multiDelete }"
image="{ image }"
registry-url="{ props.registryUrl }"
handleCheckboxChange="{ onRemoveImageChange }"
checked="{ state.toDelete.has(image) }"
on-notify="{ props.onNotify }"
on-authentication="{ props.onAuthentication }"
/>
</td>
</tr>
</tbody>
</table>
</material-card>
<script>
import {
getPage,
} from '../../scripts/utils';
import { getPage } from '../../scripts/utils';
import ImageDate from './image-date.riot';
import ImageSize from './image-size.riot';
import ImageTag from './image-tag.riot';
@ -94,9 +128,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import CopyToClipboard from './copy-to-clipboard.riot';
import TagHistoryButton from './tag-history-button.riot';
import RemoveImage from './remove-image.riot';
import {
matchSearch
} from '../search-bar.riot';
import { matchSearch } from '../search-bar.riot';
import ConfirmDeleteImage from '../dialogs/confirm-delete-image.riot';
const ACTION_CHECK_TO_DELETE = 'CHECK';
const ACTION_UNCHECK_TO_DELETE = 'UNCHECK';
@ -118,18 +150,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
toDelete: new Set(),
multiDelete: false,
page: props.page,
}
};
},
onBeforeUpdate(props, state) {
if (state.page !== props.page) {
state.toDelete.clear();
}
state.page = props.page
state.page = props.page;
},
deleteImages() {
this.update({
confirmDeleteImage: true
})
confirmDeleteImage: true,
});
},
onConfirmDeleteImageClick() {
if (this.state.singleDeleteAction) {
@ -137,22 +169,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}
this.update({
singleDeleteAction: false,
confirmDeleteImage: false
})
confirmDeleteImage: false,
});
},
onRemoveImageHeaderChange(checked, event) {
if (event.altKey === true) {
const tags = getPage(this.props.tags, this.props.page);
tags.filter(image => matchSearch(this.props.filterResults, image.tag))
.forEach(tag => this.state.toDelete.add(tag));
tags
.filter((image) => matchSearch(this.props.filterResults, image.tag))
.forEach((tag) => this.state.toDelete.add(tag));
this.update({
multiDelete: true,
toDelete: this.state.toDelete
})
toDelete: this.state.toDelete,
});
} else {
this.update({
multiDelete: checked
})
multiDelete: checked,
});
}
},
onRemoveImageChange(action, image) {
@ -177,8 +210,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
this.update({
toDelete: this.state.toDelete,
confirmDeleteImage,
singleDeleteAction
})
singleDeleteAction,
});
},
onReverseOrder() {
this.state.orderType = null;
@ -188,30 +221,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
onPageReorder(type) {
this.update({
orderType: type,
desc: (this.state.orderType && this.state.orderType !== type) || !this.state.desc
})
desc: (this.state.orderType && this.state.orderType !== type) || !this.state.desc,
});
},
getPage(tags, page) {
const sortedTags = getPage(tags, page);
if (this.state.orderType === 'date') {
sortedTags.sort((e1, e2) =>
!this.state.desc ?
e2.creationDate.getTime() - e1.creationDate.getTime() :
e1.creationDate.getTime() - e2.creationDate.getTime());
!this.state.desc
? e2.creationDate.getTime() - e1.creationDate.getTime()
: e1.creationDate.getTime() - e2.creationDate.getTime()
);
} else if (this.state.orderType === 'size') {
sortedTags.sort((e1, e2) =>
!this.state.desc ?
e2.size - e1.size :
e1.size - e2.size);
sortedTags.sort((e1, e2) => (!this.state.desc ? e2.size - e1.size : e1.size - e2.size));
}
return sortedTags;
},
matchSearch
}
export {
ACTION_CHECK_TO_DELETE,
ACTION_UNCHECK_TO_DELETE,
ACTION_DELETE_IMAGE
}
matchSearch,
};
export { ACTION_CHECK_TO_DELETE, ACTION_UNCHECK_TO_DELETE, ACTION_DELETE_IMAGE };
</script>
</tag-table>
</tag-table>

View File

@ -97,7 +97,7 @@ export class DockerImage {
const oReq = new Http({ onAuthentication: this.opts.onAuthentication });
const self = this;
oReq.addEventListener('loadend', function () {
if (this.status == 200 || this.status == 202) {
if (this.status === 200 || this.status === 202) {
const response = JSON.parse(this.responseText);
if (response.mediaType === 'application/vnd.docker.distribution.manifest.list.v2+json' && self.opts.list) {
self.trigger('list', response);
@ -131,7 +131,7 @@ export class DockerImage {
self.trigger('blobs');
self.trigger('oci-image');
}
} else if (this.status == 404) {
} else if (this.status === 404) {
self.opts.onNotify(`Manifest for ${self.name}:${self.tag} not found`, true);
} else {
self.opts.onNotify(this.responseText);
@ -149,7 +149,7 @@ export class DockerImage {
const oReq = new Http({ onAuthentication: this.opts.onAuthentication });
const self = this;
oReq.addEventListener('loadend', function () {
if (this.status == 200 || this.status == 202) {
if (this.status === 200 || this.status === 202) {
const response = JSON.parse(this.responseText);
self.creationDate = new Date(response.created);
self.blobs = response;
@ -164,7 +164,7 @@ export class DockerImage {
self.blobs.id = blob.replace('sha256:', '');
self.trigger('creation-date', self.creationDate);
self.trigger('blobs', self.blobs);
} else if (this.status == 404) {
} else if (this.status === 404) {
self.opts.onNotify(`Blobs for ${self.name}:${self.tag} not found`, true);
} else {
self.opts.onNotify(this.responseText);

View File

@ -52,7 +52,7 @@ export class Http {
switch (e) {
case 'loadend': {
self.oReq.addEventListener('loadend', function () {
if (this.status == 401 && !this.withCredentials) {
if (this.status === 401 && !this.withCredentials) {
const tokenAuth =
this.hasHeader('www-authenticate') && parseAuthenticateHeader(this.getResponseHeader('www-authenticate'));
self.onAuthentication(tokenAuth, (bearer) => {

View File

@ -4,7 +4,7 @@ export function bytesToSize(bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes == undefined || isNaN(bytes)) {
return '?';
} else if (bytes == 0) {
} else if (bytes === 0) {
return '0 Byte';
}
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));