docker-registry-ui/src/components/docker-registry-ui.riot

273 lines
9.7 KiB
Plaintext

<!--
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">Docker Registry UI</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>
<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>
</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) }"
></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) }"
></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>
<error-page
if="{ state.pageError }"
code="{ state.pageError.code }"
status="{ state.pageError.status }"
message="{ state.pageError.message }"
url="{ state.pageError.url }"
></error-page>
<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' }">
<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 } 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 ErrorPage from './error-page.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,
},
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) {
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 || 100000;
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.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) {
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,
truthy,
stringToArray,
};
</script>
<style>
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-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>