mirror of
https://github.com/Joxit/docker-registry-ui.git
synced 2025-04-26 15:09:53 +03:00
310 lines
11 KiB
Text
310 lines
11 KiB
Text
<!--
|
|
Copyright (C) 2016-2023 Jones Magloire @Joxit
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
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/>.
|
|
-->
|
|
<docker-registry-ui>
|
|
<header>
|
|
<material-navbar>
|
|
<span class="logo">
|
|
<span>Docker Registry UI</span>
|
|
<version-notification version="{ latest }" on-notify="{ notifySnackbar }"></version-notification>
|
|
</span>
|
|
<div class="menu">
|
|
<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>
|
|
</div>
|
|
</material-navbar>
|
|
</header>
|
|
<main>
|
|
<error-page
|
|
if="{ state.pageError && !Array.isArray(state.pageError.errors) }"
|
|
code="{ state.pageError.code }"
|
|
status="{ state.pageError.status }"
|
|
message="{ state.pageError.message }"
|
|
url="{ state.pageError.url }"
|
|
></error-page>
|
|
<error-page
|
|
if="{ state.pageError && Array.isArray(state.pageError.errors) }"
|
|
each="{ error in (state.pageError && state.pageError.errors) }"
|
|
code="{ error.code }"
|
|
detail="{ error.detail }"
|
|
message="{ error.message }"
|
|
url="{ state.pageError.url }"
|
|
></error-page>
|
|
<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-default-expanded="{ truthy(props.catalogDefaultExpanded) }"
|
|
catalog-min-branches="{ props.catalogMinBranches }"
|
|
catalog-max-branches="{ props.catalogMaxBranches }"
|
|
is-registry-secured="{ truthy(props.isRegistrySecured) }"
|
|
></catalog>
|
|
</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 }"
|
|
use-control-cache-header="{ truthy(props.useControlCacheHeader) }"
|
|
taglist-order="{ props.taglistOrder }"
|
|
tags-per-page="{ props.tagsPerPage }"
|
|
is-registry-secured="{ truthy(props.isRegistrySecured) }"
|
|
></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) }"
|
|
use-control-cache-header="{ truthy(props.useControlCacheHeader) }"
|
|
is-registry-secured="{ truthy(props.isRegistrySecured) }"
|
|
></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>
|
|
<material-snackbar message="{ state.snackbarMessage }" is-error="{ state.snackbarIsError }"></material-snackbar>
|
|
</main>
|
|
<footer>
|
|
<material-footer mini="true">
|
|
<a class="material-footer-logo" href="https://joxit.github.io/docker-registry-ui/">
|
|
Docker Registry UI { version }
|
|
</a>
|
|
<ul>
|
|
<li>
|
|
<a href="https://github.com/Joxit/docker-registry-ui">Contribute on GitHub</a>
|
|
</li>
|
|
<li>
|
|
<a href="https://github.com/Joxit/docker-registry-ui/blob/main/LICENSE">License AGPL-3.0</a>
|
|
</li>
|
|
<li if="{ props.theme === 'auto' || props.theme === '' }">
|
|
<material-switch
|
|
on-change="{ onThemeChange }"
|
|
checked="{ state.themeSwitch }"
|
|
track-selected-color="var(--accent-text)"
|
|
outline-selected-color="var(--accent-text)"
|
|
>
|
|
<i slot="thumb-icon" class="material-icons" style="color: white; font-size: 0.75em">wb_sunny</i>
|
|
<i slot="thumb-selected-icon" class="material-icons" style="color: #79747e; font-size: 0.75em">
|
|
brightness_2
|
|
</i>
|
|
</material-switch>
|
|
</li>
|
|
</ul>
|
|
</material-footer>
|
|
</footer>
|
|
<script>
|
|
import { version, latest } from '../../.version.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 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';
|
|
|
|
export default {
|
|
components: {
|
|
Catalog,
|
|
TagList,
|
|
TagHistory,
|
|
DialogsMenu,
|
|
SearchBar,
|
|
Router,
|
|
Route,
|
|
ErrorPage,
|
|
VersionNotification,
|
|
},
|
|
onUpdated(props, state) {
|
|
state.snackbarIsError = false;
|
|
state.snackbarMessage = undefined;
|
|
},
|
|
onBeforeMount(props) {
|
|
if (
|
|
(props.defaultRegistries && props.defaultRegistries.length > 0 && getRegistryServers().length === 0) ||
|
|
truthy(props.readOnlyRegistries)
|
|
) {
|
|
setRegistryServers(props.defaultRegistries);
|
|
}
|
|
|
|
window.onselectstart = (e) => {
|
|
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);
|
|
}
|
|
};
|
|
|
|
// 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 || 1000;
|
|
this.state.pullUrl = this.pullUrl(this.state.registryUrl, props.pullUrl);
|
|
this.state.useControlCacheHeader = props.useControlCacheHeader;
|
|
const theme = loadTheme(props, this.root.parentNode.style);
|
|
this.state.themeSwitch = theme === 'dark';
|
|
},
|
|
onServerChange(registryUrl) {
|
|
this.update({
|
|
registryUrl,
|
|
name: stripHttps(registryUrl),
|
|
pullUrl: this.pullUrl(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();
|
|
req.addEventListener('loadend', () => {
|
|
try {
|
|
const bearer = JSON.parse(req.responseText);
|
|
onAuthenticated(bearer);
|
|
} catch (e) {
|
|
this.notifySnackbar(`Failed to log in: ${e.message}`, true);
|
|
}
|
|
});
|
|
req.withCredentials = true;
|
|
req.open('GET', `${realm}?service=${service}&scope=${scope}`);
|
|
req.send();
|
|
} else {
|
|
onAuthenticated();
|
|
}
|
|
},
|
|
onAuthenticationClose() {
|
|
this.update({
|
|
authenticationDialogOpened: false,
|
|
});
|
|
},
|
|
pullUrl(registryUrl, pullUrl) {
|
|
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,
|
|
});
|
|
} else if (message && (message.code || message.errors)) {
|
|
this.update({
|
|
pageError: message,
|
|
});
|
|
setTimeout(() => delete this.state['pageError'], 1000);
|
|
} else if (message && message.message) {
|
|
this.update({
|
|
snackbarMessage: message.message,
|
|
snackbarIsError: message.isError,
|
|
});
|
|
}
|
|
},
|
|
onSearch(value) {
|
|
this.update({
|
|
filter: value,
|
|
});
|
|
},
|
|
onThemeChange(e) {
|
|
const theme = e.target.checked ? 'dark' : 'light';
|
|
loadTheme({ ...this.props, theme }, this.root.parentNode.style);
|
|
this.update({ themeSwitch: e.target.checked });
|
|
},
|
|
baseRoute: '([^#]*?)/(\\?[^#]*?)?(#!)?(/?)',
|
|
router,
|
|
version,
|
|
latest,
|
|
truthy,
|
|
stringToArray,
|
|
};
|
|
</script>
|
|
<style>
|
|
:host {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 100vh;
|
|
}
|
|
:host main {
|
|
flex-grow: 1;
|
|
margin: 0.5em 0;
|
|
}
|
|
material-navbar {
|
|
height: 64px;
|
|
color: var(--header-text);
|
|
background-color: var(--header-background);
|
|
}
|
|
|
|
material-navbar .menu {
|
|
display: flex;
|
|
}
|
|
|
|
material-navbar .nav-wrapper .menu {
|
|
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);
|
|
}
|
|
material-footer .material-footer-logo {
|
|
color: var(--footer-text);
|
|
}
|
|
|
|
material-switch i {
|
|
user-select: none;
|
|
}
|
|
</style>
|
|
</docker-registry-ui>
|