Initial commit
commit
08f396a812
|
@ -0,0 +1,3 @@
|
||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not ie <= 8
|
|
@ -0,0 +1,17 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'plugin:vue/essential',
|
||||||
|
'@vue/airbnb',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
parser: 'babel-eslint',
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,21 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw*
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
# dri
|
||||||
|
|
||||||
|
A static Docker Distribution Registry viewer.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* View repositories and tags
|
||||||
|
* View tag details and sizes of tags
|
||||||
|
* Delete repositories and tags (if enabled on the registry)
|
||||||
|
* Optionally scan for vulnerabilities using CoreOS Clair
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/app',
|
||||||
|
],
|
||||||
|
};
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "dri",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"filesize": "^3.6.1",
|
||||||
|
"normalize.css": "^8.0.0",
|
||||||
|
"parse-link-header": "^1.0.1",
|
||||||
|
"vue": "^2.5.17",
|
||||||
|
"vue-router": "^3.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "^3.0.3",
|
||||||
|
"@vue/cli-plugin-eslint": "^3.0.3",
|
||||||
|
"@vue/cli-service": "^3.0.3",
|
||||||
|
"@vue/eslint-config-airbnb": "^3.0.3",
|
||||||
|
"vue-template-compiler": "^2.5.17"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,19 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<base href="<%= BASE_URL %>">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro|Source+Sans+Pro">
|
||||||
|
<title>dri</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but dri doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<router-view/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html, body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-size: 1rem;
|
||||||
|
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
}
|
||||||
|
pre, code, kbd, samp {
|
||||||
|
font-family: 'Source Code Pro';
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,77 @@
|
||||||
|
import parseLink from 'parse-link-header';
|
||||||
|
|
||||||
|
import { registryAPI, repositoriesPerPage, tagsPerPage, usePortusExplore } from '@/options';
|
||||||
|
|
||||||
|
async function paginatable(path, n, last = null) {
|
||||||
|
const url = new URL(`${await registryAPI()}${path}`);
|
||||||
|
if (n) url.searchParams.append('n', n);
|
||||||
|
if (last) url.searchParams.append('last', last);
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
let nextLast = null;
|
||||||
|
if (response.headers.has('Link')) {
|
||||||
|
const links = parseLink(response.headers.get('Link'));
|
||||||
|
if (links && links.next) {
|
||||||
|
nextLast = links.next.last;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.assign(await response.json(), { nextLast });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(path) {
|
||||||
|
const url = new URL(`${await registryAPI()}${path}`);
|
||||||
|
const response = await fetch(url);
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function head(path) {
|
||||||
|
const url = new URL(`${await registryAPI()}${path}`);
|
||||||
|
const response = await fetch(url, { method: 'HEAD' });
|
||||||
|
return response.headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function repos(last = null) {
|
||||||
|
if (await usePortusExplore()) {
|
||||||
|
// TODO: Use the Portus API when it enables anonymous access
|
||||||
|
const response = await fetch(`${await registryAPI()}/explore?explore%5Bsearch%5D=`);
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
// unconventionally parse out JSON from HTML
|
||||||
|
const startString = 'window.repositories = ';
|
||||||
|
const endString = ';</script>';
|
||||||
|
const string = html.substring(
|
||||||
|
html.lastIndexOf(startString) + startString.length,
|
||||||
|
html.indexOf(endString),
|
||||||
|
);
|
||||||
|
return JSON.parse(string);
|
||||||
|
}
|
||||||
|
return paginatable('/v2/_catalog', await repositoriesPerPage(), last);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function repo(name) {
|
||||||
|
return get(`/v2/${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tags(name, last = null) {
|
||||||
|
return paginatable(`/v2/${name}/tags/list`, await tagsPerPage(), last);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tag(name, ref) {
|
||||||
|
return get(`/v2/${name}/manifests/${ref}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function blob(name, digest) {
|
||||||
|
const headers = await head(`/v2/${name}/blobs/${digest}`);
|
||||||
|
return {
|
||||||
|
contentLength: parseInt(headers.get('Content-Length'), 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
repos,
|
||||||
|
repo,
|
||||||
|
tags,
|
||||||
|
tag,
|
||||||
|
blob,
|
||||||
|
};
|
|
@ -0,0 +1,28 @@
|
||||||
|
<template>
|
||||||
|
<LoadableText :text="size" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import filesize from 'filesize';
|
||||||
|
import { blob } from '@/api';
|
||||||
|
import LoadableText from '@/components/LoadableText.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
LoadableText,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
repo: String,
|
||||||
|
blob: String,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
size: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
const size = await blob(this.repo, this.blob);
|
||||||
|
this.size = filesize(size.contentLength);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,22 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="message">{{ message }}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
div {
|
||||||
|
text-align: center;
|
||||||
|
background: #fdd;
|
||||||
|
color: #800;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
margin: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,52 @@
|
||||||
|
<template>
|
||||||
|
<div class="wrapper">
|
||||||
|
<header>
|
||||||
|
<slot name="title" />
|
||||||
|
<slot name="error" />
|
||||||
|
<slot name="toolbar" />
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
<p>
|
||||||
|
<a :href="source">dri</a>
|
||||||
|
{{ version }}
|
||||||
|
by <a href="https://github.com/serverwentdown">@serverwentdown</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { version, source } from '@/options';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
version,
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
header, footer, main {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
header, footer, .content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
header, footer {
|
||||||
|
max-width: 38rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<template>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<slot name="header" />
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<slot />
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
table {
|
||||||
|
min-width: 28rem;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<tr>
|
||||||
|
<th class="col title"><slot name="title" /></th>
|
||||||
|
<th class="col detail" v-if="$slots.detail"><slot name="detail" /></th>
|
||||||
|
<th class="col date" v-if="$slots.date"><slot name="date" /></th>
|
||||||
|
<th class="col size" v-if="$slots.size"><slot name="size" /></th>
|
||||||
|
<th class="col buttons" v-if="$slots.buttons"><slot name="buttons" /></th>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
th {
|
||||||
|
padding: 0.5rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 0.8em;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,36 @@
|
||||||
|
<template>
|
||||||
|
<router-link tag="tr" :to="to || {}">
|
||||||
|
<td class="col title"><slot name="title" /></td>
|
||||||
|
<td class="col detail" v-if="$slots.detail"><slot name="detail" /></td>
|
||||||
|
<td class="col date" v-if="$slots.date"><slot name="date" /></td>
|
||||||
|
<td class="col size" v-if="$slots.size"><slot name="size" /></td>
|
||||||
|
<td class="col buttons" v-if="$slots.buttons"><slot name="buttons" /></td>
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
to: { type: [Object, String], required: false },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
tr:hover {
|
||||||
|
background: #f4f4f4;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
border-bottom-width: 0.5px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.detail {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<span :class="{ loading: !text }">{{ text }}<span /><span /><span /></span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
span.loading > span {
|
||||||
|
display: inline-block;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 0.09375em;
|
||||||
|
width: 0.1875em;
|
||||||
|
height: 0.1875em;
|
||||||
|
margin: 0.25em 0.075em;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
span.loading > span:nth-child(1) {
|
||||||
|
animation: fade 1s ease 0s infinite;
|
||||||
|
}
|
||||||
|
span.loading > span:nth-child(2) {
|
||||||
|
animation: fade 1s ease 0.333s infinite;
|
||||||
|
}
|
||||||
|
span.loading > span:nth-child(3) {
|
||||||
|
animation: fade 1s ease 0.666s infinite;
|
||||||
|
}
|
||||||
|
@keyframes fade {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
33.3% {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
66.6% {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- <span class="prev" @click="prev">Previous</span> -->
|
||||||
|
<span class="next" @click="next" v-if="nextLast">Next</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
nextLast: String,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
prev() {
|
||||||
|
this.$router.back();
|
||||||
|
},
|
||||||
|
next() {
|
||||||
|
this.$router.push(`?last=${this.nextLast}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
}
|
||||||
|
div > span {
|
||||||
|
padding: 0.75rem;
|
||||||
|
color: #008;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<LoadableText :text="size" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import filesize from 'filesize';
|
||||||
|
import { tag, blob } from '@/api';
|
||||||
|
import LoadableText from '@/components/LoadableText.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
LoadableText,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
repo: String,
|
||||||
|
tag: String,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
size: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
const r = await tag(this.repo, this.tag);
|
||||||
|
if (r.mediaType === 'application/vnd.docker.distribution.manifest.list.v2+json') {
|
||||||
|
console.error('V2 manifest lists not supported yet');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (r.schemaVersion === 1) {
|
||||||
|
r.layers = r.fsLayers.map(l => ({ digest: l.blobSum }));
|
||||||
|
}
|
||||||
|
const sizes = await Promise.all(r.layers.map(async layer => (
|
||||||
|
await blob(this.repo, layer.digest)
|
||||||
|
).contentLength));
|
||||||
|
const total = sizes.reduce((a, b) => a + b, 0);
|
||||||
|
this.size = filesize(total);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<span>No Toolbar</span>
|
||||||
|
</template>
|
|
@ -0,0 +1,12 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import App from '@/App.vue';
|
||||||
|
import router from '@/router';
|
||||||
|
|
||||||
|
import 'normalize.css';
|
||||||
|
|
||||||
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
router,
|
||||||
|
render: h => h(App),
|
||||||
|
}).$mount('#app');
|
|
@ -0,0 +1,90 @@
|
||||||
|
const version = process.env.VERSION || '[master]';
|
||||||
|
const source = process.env.SOURCE_LINK || 'https://github.com/productionwentdown/dri';
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
merged: false,
|
||||||
|
|
||||||
|
registryHost: process.env.VUE_APP_REGISTRY_HOST,
|
||||||
|
registryAPI: process.env.VUE_APP_REGISTRY_API,
|
||||||
|
|
||||||
|
repositoriesPerPage: process.env.VUE_APP_REPOSITORIES_PER_PAGE,
|
||||||
|
tagsPerPage: process.env.VUE_APP_TAGS_PER_PAGE,
|
||||||
|
|
||||||
|
usePortusExplore: process.env.VUE_APP_USE_PORTUS_EXPLORE,
|
||||||
|
};
|
||||||
|
|
||||||
|
async function config() {
|
||||||
|
if (defaultConfig.merged) {
|
||||||
|
return defaultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/config.json');
|
||||||
|
const jsonConfig = await response.json();
|
||||||
|
defaultConfig.merged = true;
|
||||||
|
return Object.assign(defaultConfig, jsonConfig);
|
||||||
|
} catch (_) {
|
||||||
|
defaultConfig.merged = true;
|
||||||
|
return defaultConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registryHost() {
|
||||||
|
const c = await config();
|
||||||
|
if (c.registryHost) {
|
||||||
|
return c.registryHost;
|
||||||
|
}
|
||||||
|
return window.location.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registryAPI() {
|
||||||
|
const c = await config();
|
||||||
|
if (c.registryAPI) {
|
||||||
|
return c.registryAPI;
|
||||||
|
}
|
||||||
|
const host = await registryHost();
|
||||||
|
// assume API uses the same protocol as the page
|
||||||
|
// this is because browsers don't allow mixed content
|
||||||
|
// if a HTTPS API needs to be accessed over HTTP, configure registryAPI
|
||||||
|
return `${window.location.protocol}//${host}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function usePortusExplore() {
|
||||||
|
const c = await config();
|
||||||
|
if (c.registryAPI) {
|
||||||
|
return c.registryAPI;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function repositoriesPerPage() {
|
||||||
|
const c = await config();
|
||||||
|
try {
|
||||||
|
const n = parseInt(c.repositoriesPerPage, 10);
|
||||||
|
if (n > 0) return n;
|
||||||
|
} catch (_) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tagsPerPage() {
|
||||||
|
const c = await config();
|
||||||
|
try {
|
||||||
|
const n = parseInt(c.tagsPerPage, 10);
|
||||||
|
if (n > 0) return n;
|
||||||
|
} catch (_) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
version,
|
||||||
|
source,
|
||||||
|
registryHost,
|
||||||
|
registryAPI,
|
||||||
|
repositoriesPerPage,
|
||||||
|
tagsPerPage,
|
||||||
|
usePortusExplore,
|
||||||
|
};
|
|
@ -0,0 +1,35 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Router from 'vue-router';
|
||||||
|
|
||||||
|
import Repos from '@/views/Repos.vue';
|
||||||
|
import Repo from '@/views/Repo.vue';
|
||||||
|
import Tag from '@/views/Tag.vue';
|
||||||
|
import Blob from '@/views/Blob.vue';
|
||||||
|
|
||||||
|
Vue.use(Router);
|
||||||
|
|
||||||
|
export default new Router({
|
||||||
|
mode: 'history',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'repos',
|
||||||
|
component: Repos,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:repo+/blobs/:digest',
|
||||||
|
name: 'blob',
|
||||||
|
component: Blob,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:repo+/tags/:tag',
|
||||||
|
name: 'tag',
|
||||||
|
component: Tag,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:repo+',
|
||||||
|
name: 'repo',
|
||||||
|
component: Repo,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
|
@ -0,0 +1,32 @@
|
||||||
|
<template>
|
||||||
|
<Layout>
|
||||||
|
<h1 slot="title">{{ $route.params.repo }}</h1>
|
||||||
|
<h2>Details</h2>
|
||||||
|
<List>
|
||||||
|
<ListItem>
|
||||||
|
<span slot="title">Full Digest</span>
|
||||||
|
<span slot="detail">{{ $route.params.digest }}</span>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<span slot="title">Size</span>
|
||||||
|
<BlobSize slot="detail" :repo="$route.params.repo" :blob="$route.params.digest" />
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</Layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Layout from '@/components/Layout.vue';
|
||||||
|
import List from '@/components/List.vue';
|
||||||
|
import ListItem from '@/components/ListItem.vue';
|
||||||
|
import BlobSize from '@/components/BlobSize.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Layout,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
BlobSize,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<Layout>
|
||||||
|
<h1 slot="title">{{ $route.params.repo }}</h1>
|
||||||
|
<Error slot="error" :message='error' />
|
||||||
|
<List>
|
||||||
|
<ListHeader slot="header">
|
||||||
|
<span slot="title">Tag</span>
|
||||||
|
<span slot="detail">Pull Commmand</span>
|
||||||
|
<span slot="size">Size</span>
|
||||||
|
</ListHeader>
|
||||||
|
<ListItem
|
||||||
|
v-for="tag in tags"
|
||||||
|
:key="tag"
|
||||||
|
:to="{ name: 'tag', params: { tag, }}">
|
||||||
|
<span slot="title">
|
||||||
|
{{ tag }}
|
||||||
|
</span>
|
||||||
|
<span slot="detail">
|
||||||
|
<code>docker pull {{ registryHost }}/{{ $route.params.repo }}:{{ tag }}</code>
|
||||||
|
</span>
|
||||||
|
<TagSize slot="size" :repo="$route.params.repo" :tag="tag" />
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
<Paginator :nextLast="nextLast" />
|
||||||
|
</Layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { registryHost } from '@/options';
|
||||||
|
import { tags } from '@/api';
|
||||||
|
|
||||||
|
import Layout from '@/components/Layout.vue';
|
||||||
|
import Error from '@/components/Error.vue';
|
||||||
|
import Toolbar from '@/components/Toolbar.vue';
|
||||||
|
import List from '@/components/List.vue';
|
||||||
|
import ListHeader from '@/components/ListHeader.vue';
|
||||||
|
import ListItem from '@/components/ListItem.vue';
|
||||||
|
import Paginator from '@/components/Paginator.vue';
|
||||||
|
import TagSize from '@/components/TagSize.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Layout,
|
||||||
|
Error,
|
||||||
|
Toolbar,
|
||||||
|
List,
|
||||||
|
ListHeader,
|
||||||
|
ListItem,
|
||||||
|
Paginator,
|
||||||
|
TagSize,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
error: '',
|
||||||
|
registryHost: '',
|
||||||
|
tags: [],
|
||||||
|
nextLast: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
this.registryHost = await registryHost();
|
||||||
|
await this.fetchTags();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchTags() {
|
||||||
|
try {
|
||||||
|
const r = await tags(this.$route.params.repo, this.$route.query.last);
|
||||||
|
this.tags = r.tags;
|
||||||
|
this.nextLast = r.nextLast;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = `Unable to fetch tags (${e.name})`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
async $route() {
|
||||||
|
await this.fetchTags();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,78 @@
|
||||||
|
<template>
|
||||||
|
<Layout>
|
||||||
|
<h1 slot="title">Repositories</h1>
|
||||||
|
<Error slot="error" :message='error' />
|
||||||
|
<List>
|
||||||
|
<ListHeader slot="header">
|
||||||
|
<span slot="title">Repository</span>
|
||||||
|
<span slot="detail">Pull Commmand</span>
|
||||||
|
</ListHeader>
|
||||||
|
<ListItem
|
||||||
|
v-for="repo in repos"
|
||||||
|
:key="repo"
|
||||||
|
:to="{ name: 'repo', params: { repo, }}">
|
||||||
|
<span slot="title">
|
||||||
|
{{ repo }}
|
||||||
|
</span>
|
||||||
|
<span slot="detail">
|
||||||
|
<code>docker pull {{ registryHost }}/{{ repo }}</code>
|
||||||
|
</span>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
<Paginator :nextLast="nextLast" />
|
||||||
|
</Layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { registryHost } from '@/options';
|
||||||
|
import { repos } from '@/api';
|
||||||
|
|
||||||
|
import Layout from '@/components/Layout.vue';
|
||||||
|
import Error from '@/components/Error.vue';
|
||||||
|
import Toolbar from '@/components/Toolbar.vue';
|
||||||
|
import List from '@/components/List.vue';
|
||||||
|
import ListHeader from '@/components/ListHeader.vue';
|
||||||
|
import ListItem from '@/components/ListItem.vue';
|
||||||
|
import Paginator from '@/components/Paginator.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Layout,
|
||||||
|
Error,
|
||||||
|
Toolbar,
|
||||||
|
List,
|
||||||
|
ListHeader,
|
||||||
|
ListItem,
|
||||||
|
Paginator,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
error: '',
|
||||||
|
registryHost: '',
|
||||||
|
repos: [],
|
||||||
|
nextLast: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
this.registryHost = await registryHost();
|
||||||
|
await this.fetchRepos();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchRepos() {
|
||||||
|
try {
|
||||||
|
const r = await repos(this.$route.query.last);
|
||||||
|
this.repos = r.repositories;
|
||||||
|
this.nextLast = r.nextLast;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = `Unable to fetch repositories (${e.name})`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
async $route() {
|
||||||
|
await this.fetchRepos();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,105 @@
|
||||||
|
<template>
|
||||||
|
<Layout>
|
||||||
|
<h1 slot="title">{{ $route.params.repo }}:{{ $route.params.tag }}</h1>
|
||||||
|
<Error slot="error" :message='error' />
|
||||||
|
<h2>Details</h2>
|
||||||
|
<List>
|
||||||
|
<ListItem>
|
||||||
|
<span slot="title">Schema Version</span>
|
||||||
|
<span slot="detail">{{ tag.schemaVersion }}</span>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<span slot="title">Architecture</span>
|
||||||
|
<span slot="detail">{{ tag.architecture }}</span>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<span slot="title">Size</span>
|
||||||
|
<span slot="detail"><TagSize :repo="$route.params.repo" :tag="$route.params.tag" /></span>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
<h2>Layers</h2>
|
||||||
|
<List>
|
||||||
|
<ListHeader slot="header">
|
||||||
|
<span slot="title">Digest</span>
|
||||||
|
<span slot="detail">Summary</span>
|
||||||
|
<span slot="size">Size</span>
|
||||||
|
</ListHeader>
|
||||||
|
<ListItem
|
||||||
|
v-for="(layer, i) in tag.layers"
|
||||||
|
:key="layer.digest"
|
||||||
|
:to="{ name: 'blob', params: { repo: $route.params.repo, digest: layer.digest }}">
|
||||||
|
<span slot="title" :title="layer.digest">{{ identifier(tag, i) }}</span>
|
||||||
|
<span slot="detail">{{ command(tag, i) }}</span>
|
||||||
|
<BlobSize slot="size" :repo="$route.params.repo" :blob="layer.digest" />
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</Layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { registryHost } from '@/options';
|
||||||
|
import { tag } from '@/api';
|
||||||
|
|
||||||
|
import Layout from '@/components/Layout.vue';
|
||||||
|
import Error from '@/components/Error.vue';
|
||||||
|
import Toolbar from '@/components/Toolbar.vue';
|
||||||
|
import List from '@/components/List.vue';
|
||||||
|
import ListHeader from '@/components/ListHeader.vue';
|
||||||
|
import ListItem from '@/components/ListItem.vue';
|
||||||
|
import TagSize from '@/components/TagSize.vue';
|
||||||
|
import BlobSize from '@/components/BlobSize.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Layout,
|
||||||
|
Error,
|
||||||
|
Toolbar,
|
||||||
|
List,
|
||||||
|
ListHeader,
|
||||||
|
ListItem,
|
||||||
|
TagSize,
|
||||||
|
BlobSize,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
error: '',
|
||||||
|
registryHost: '',
|
||||||
|
tag: {},
|
||||||
|
layers: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
this.registryHost = await registryHost();
|
||||||
|
await this.fetchTag();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchTag() {
|
||||||
|
try {
|
||||||
|
const r = await tag(this.$route.params.repo, this.$route.params.tag);
|
||||||
|
if (r.mediaType === 'application/vnd.docker.distribution.manifest.list.v2+json') {
|
||||||
|
this.error = 'V2 manifest lists not supported yet';
|
||||||
|
}
|
||||||
|
if (r.schemaVersion === 1) {
|
||||||
|
r.layers = r.fsLayers.map(l => ({ digest: l.blobSum }));
|
||||||
|
}
|
||||||
|
this.tag = r;
|
||||||
|
console.log(r);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.error = `Unable to fetch tag (${e.name})`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
identifier(t, n) {
|
||||||
|
return t.layers[n].digest.split(':')[1].slice(0, 10);
|
||||||
|
},
|
||||||
|
command() {
|
||||||
|
return 'not implemented';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
async $route() {
|
||||||
|
await this.fetchTag();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
lintOnSave: false,
|
||||||
|
};
|
Loading…
Reference in New Issue