Add docker registry config UI for multiple registries
This commit is contained in:
122
dashboard/src/components/DockerRegistryDialog.vue
Normal file
122
dashboard/src/components/DockerRegistryDialog.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<script setup>
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const i18n = useI18n();
|
||||
const t = i18n.t;
|
||||
|
||||
import { ref, useTemplateRef, computed } from 'vue';
|
||||
import { Dialog, TextInput, FormGroup, SingleSelect, PasswordInput } from 'pankow';
|
||||
import { isValidDomainOrURL } from 'pankow/utils';
|
||||
import DockerRegistriesModel from '../models/DockerRegistriesModel.js';
|
||||
|
||||
const dockerRegistriesModel = DockerRegistriesModel.create();
|
||||
|
||||
const providers = [
|
||||
{ name: 'AWS', value: 'aws' },
|
||||
{ name: 'Cloudron', value: 'cloudron' },
|
||||
{ name: 'Digital Ocean', value: 'digitalocean' },
|
||||
{ name: 'DockerHub', value: 'dockerhub' },
|
||||
{ name: 'Google Cloud', value: 'google-cloud' },
|
||||
{ name: 'Linode', value: 'linode' },
|
||||
{ name: 'Quay', value: 'quay' },
|
||||
{ name: 'Treescale', value: 'treescale' },
|
||||
{ name: t('settings.registryConfig.providerOther') || 'Other', value: 'other' },
|
||||
];
|
||||
|
||||
const emit = defineEmits([ 'success' ]);
|
||||
|
||||
const dialog = useTemplateRef('dialog');
|
||||
|
||||
const busy = ref(false);
|
||||
const formError = ref({});
|
||||
const registry = ref(null);
|
||||
const provider = ref('');
|
||||
const serverAddress = ref('');
|
||||
const username = ref('');
|
||||
const email = ref('');
|
||||
const password = ref('');
|
||||
|
||||
const isValid = computed(() => {
|
||||
if (!serverAddress.value) return false;
|
||||
if (!username.value) return false;
|
||||
if (!password.value) return false;
|
||||
if (!isValidDomainOrURL(serverAddress.value)) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
async function onSubmit() {
|
||||
busy.value = true;
|
||||
formError.value = {};
|
||||
|
||||
let error;
|
||||
if (registry.value) [error] = await dockerRegistriesModel.update(registry.value.id, provider.value, serverAddress.value, username.value, email.value, password.value);
|
||||
else [error] = await dockerRegistriesModel.add(provider.value, serverAddress.value, username.value, email.value, password.value);
|
||||
if (error) return console.error(error);
|
||||
|
||||
emit('success');
|
||||
dialog.value.close();
|
||||
busy.value = false;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
async open(r = null) {
|
||||
busy.value = false;
|
||||
formError.value = {};
|
||||
registry.value = r;
|
||||
provider.value = r ? r.provider : '';
|
||||
serverAddress.value = r ? r.serverAddress : '';
|
||||
username.value = r ? r.username : '';
|
||||
email.value = (r && typeof r.email === 'string') ? r.email : '';
|
||||
password.value = r ? r.password : '';
|
||||
|
||||
dialog.value.open();
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog ref="dialog"
|
||||
:title="$t('settings.privateDockerRegistryDialog.title')"
|
||||
:confirm-label="$t('main.dialog.save')"
|
||||
:confirm-busy="busy"
|
||||
:confirm-active="!busy && isValid"
|
||||
:reject-label="$t('main.dialog.cancel')"
|
||||
reject-style="secondary"
|
||||
@confirm="onSubmit()"
|
||||
>
|
||||
<p class="has-error text-center" v-show="configureError">{{ configureError }}</p>
|
||||
|
||||
<form novalidate @submit.prevent="onSubmit()" autocomplete="off">
|
||||
<fieldset :disabled="busy">
|
||||
<input style="display: none" type="submit" :disabled="!isValid"/>
|
||||
|
||||
<FormGroup>
|
||||
<label for="providerInput">{{ $t('settings.registryConfig.provider') }} <sup><a href="https://docs.cloudron.io/settings/#private-docker-registry" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<SingleSelect id="providerInput" v-model="provider" :options="providers" option-key="value" option-label="name" />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<label for="serverAddressInput">{{ $t('settings.privateDockerRegistry.server') }}</label>
|
||||
<TextInput id="serverAddressInput" v-model="serverAddress" placeholder="docker.io" required />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<label for="usernameInput">{{ $t('settings.privateDockerRegistry.username') }}</label>
|
||||
<TextInput id="usernameInput" v-model="username" required />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<label for="emailInput">{{ $t('settings.privateDockerRegistryDialog.email') }}</label>
|
||||
<TextInput id="emailInput" v-model="email" />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<label for="passwordInput">{{ $t('settings.privateDockerRegistryDialog.passwordToken') }}</label>
|
||||
<PasswordInput id="passwordInput" v-model="password" required />
|
||||
</FormGroup>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -4,103 +4,43 @@ import { useI18n } from 'vue-i18n';
|
||||
const i18n = useI18n();
|
||||
const t = i18n.t;
|
||||
|
||||
import { ref, useTemplateRef, onMounted, computed } from 'vue';
|
||||
import { Button, Dialog, FormGroup, PasswordInput, TextInput, SingleSelect } from 'pankow';
|
||||
import { isValidDomainOrURL } from 'pankow/utils';
|
||||
import { ref, onMounted, useTemplateRef, inject } from 'vue';
|
||||
import { Button, TableView } from 'pankow';
|
||||
import Section from '../components/Section.vue';
|
||||
import CloudronModel from '../models/CloudronModel.js';
|
||||
import DockerRegistryDialog from '../components/DockerRegistryDialog.vue';
|
||||
import DockerRegistriesModel from '../models/DockerRegistriesModel.js';
|
||||
|
||||
const cloudronModel = CloudronModel.create();
|
||||
const dockerRegistriesModel = DockerRegistriesModel.create();
|
||||
|
||||
const providers = [
|
||||
{ name: 'AWS', value: 'aws' },
|
||||
{ name: 'Cloudron', value: 'cloudron' },
|
||||
{ name: 'Digital Ocean', value: 'digitalocean' },
|
||||
{ name: 'DockerHub', value: 'dockerhub' },
|
||||
{ name: 'Google Cloud', value: 'google-cloud' },
|
||||
{ name: 'Linode', value: 'linode' },
|
||||
{ name: 'Quay', value: 'quay' },
|
||||
{ name: 'Treescale', value: 'treescale' },
|
||||
{ name: t('settings.registryConfig.providerOther') || 'Other', value: 'other' },
|
||||
{ name: t('settings.registryConfig.providerDisabled') || 'Disabled', value: 'noop' }
|
||||
];
|
||||
const registries = ref([]);
|
||||
|
||||
const provider = ref('');
|
||||
const serverAddress = ref('');
|
||||
const username = ref('');
|
||||
const email = ref('');
|
||||
const password = ref('');
|
||||
const columns = {
|
||||
serverAddress: {
|
||||
label: t('settings.privateDockerRegistry.server'),
|
||||
sort: true
|
||||
},
|
||||
username: {
|
||||
label: t('settings.privateDockerRegistry.username'),
|
||||
sort: true
|
||||
},
|
||||
actions: {}
|
||||
};
|
||||
|
||||
const features = inject('features');
|
||||
const subscriptionRequiredDialog = inject('subscriptionRequiredDialog');
|
||||
|
||||
// configure
|
||||
const dialog = useTemplateRef('dialog');
|
||||
const configureBusy = ref(false);
|
||||
const configureError = ref('');
|
||||
const configureProvider = ref('');
|
||||
const configureServerAddress = ref('');
|
||||
const configureUsername = ref('');
|
||||
const configureEmail = ref('');
|
||||
const configurePassword = ref('');
|
||||
const isValid = computed(() => {
|
||||
if (configureProvider.value === 'noop') return true;
|
||||
|
||||
if (!configureServerAddress.value) return false;
|
||||
if (!configureUsername.value) return false;
|
||||
if (!configurePassword.value) return false;
|
||||
if (!isValidDomainOrURL(configureServerAddress.value)) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
function onShowConfigure() {
|
||||
configureBusy.value = false;
|
||||
configureError.value = '';
|
||||
configureProvider.value = provider.value;
|
||||
configureServerAddress.value = serverAddress.value;
|
||||
configureUsername.value = username.value;
|
||||
configureEmail.value = email.value;
|
||||
configurePassword.value = password.value;
|
||||
|
||||
dialog.value.open();
|
||||
}
|
||||
|
||||
async function onSubmitConfigure() {
|
||||
if (!isValid.value) return;
|
||||
|
||||
configureBusy.value = true;
|
||||
configureError.value = '';
|
||||
|
||||
const data = {
|
||||
provider: configureProvider.value
|
||||
};
|
||||
|
||||
if (configureProvider.value !== 'noop') {
|
||||
data.serverAddress = configureServerAddress.value;
|
||||
data.username = configureUsername.value;
|
||||
data.email = configureEmail.value;
|
||||
data.password = configurePassword.value;
|
||||
};
|
||||
|
||||
const [error] = await cloudronModel.setRegistryConfig(data);
|
||||
if (error) {
|
||||
configureError.value = error.body ? error.body.message : 'Internal error';
|
||||
configureBusy.value = false;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
await refresh();
|
||||
dialog.value.close();
|
||||
configureBusy.value = false;
|
||||
function onEditOrAdd(registry = null) {
|
||||
if (registry || features.value.privateDockerRegistry) dialog.value.open(registry);
|
||||
else subscriptionRequiredDialog.value.open();
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const [error, result] = await cloudronModel.getRegistryConfig();
|
||||
const [error, result] = await dockerRegistriesModel.list();
|
||||
if (error) return console.error(error);
|
||||
|
||||
provider.value = result.provider;
|
||||
serverAddress.value = result.serverAddress;
|
||||
email.value = result.email;
|
||||
username.value = result.username;
|
||||
password.value = result.password;
|
||||
registries.value = result;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -111,60 +51,22 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<Section :title="$t('settings.privateDockerRegistry.title')">
|
||||
<Dialog ref="dialog"
|
||||
:title="$t('settings.privateDockerRegistryDialog.title')"
|
||||
:confirm-label="$t('main.dialog.save')"
|
||||
:confirm-busy="configureBusy"
|
||||
:confirm-active="!configureBusy && isValid"
|
||||
:reject-label="$t('main.dialog.cancel')"
|
||||
reject-style="secondary"
|
||||
@confirm="onSubmitConfigure()"
|
||||
>
|
||||
<p class="has-error text-center" v-show="configureError">{{ configureError }}</p>
|
||||
<DockerRegistryDialog ref="dialog" @success="refresh()"/>
|
||||
|
||||
<form novalidate @submit.prevent="onSubmitConfigure()" autocomplete="off">
|
||||
<fieldset :disabled="configureBusy">
|
||||
<input style="display: none" type="submit" :disabled="!isValid"/>
|
||||
|
||||
<FormGroup>
|
||||
<label for="providerInput">{{ $t('settings.registryConfig.provider') }} <sup><a href="https://docs.cloudron.io/settings/#private-docker-registry" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<SingleSelect id="providerInput" v-model="configureProvider" :options="providers" option-key="value" option-label="name" />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup v-if="configureProvider !== 'noop'">
|
||||
<label for="serverAddressInput">{{ $t('settings.privateDockerRegistry.server') }}</label>
|
||||
<TextInput id="serverAddressInput" v-model="configureServerAddress" placeholder="docker.io" required />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup v-if="configureProvider !== 'noop'">
|
||||
<label for="usernameInput">{{ $t('settings.privateDockerRegistry.username') }}</label>
|
||||
<TextInput id="usernameInput" v-model="configureUsername" required />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup v-if="configureProvider !== 'noop'">
|
||||
<label for="emailInput">{{ $t('settings.privateDockerRegistryDialog.email') }}</label>
|
||||
<TextInput id="emailInput" v-model="configureEmail" />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup v-if="configureProvider !== 'noop'">
|
||||
<label for="passwordInput">{{ $t('settings.privateDockerRegistryDialog.passwordToken') }}</label>
|
||||
<PasswordInput id="passwordInput" v-model="configurePassword" required />
|
||||
</FormGroup>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Dialog>
|
||||
<template #header-buttons>
|
||||
<!-- TODO translate -->
|
||||
<Button icon="fa-solid fa-plus" @click="onEditOrAdd()">Add</Button>
|
||||
</template>
|
||||
|
||||
<p v-html="$t('settings.privateDockerRegistry.description', { customAppsLink: 'https://docs.cloudron.io/custom-apps/tutorial/' })"></p>
|
||||
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('settings.privateDockerRegistry.server') }}</div>
|
||||
<div class="info-value">{{ serverAddress || $t('settings.privateDockerRegistry.serverNotSet') }}</div>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<div class="info-label">{{ $t('settings.privateDockerRegistry.username') }}</div>
|
||||
<div class="info-value">{{ username || email || $t('settings.privateDockerRegistry.usernameNotSet') }}</div>
|
||||
</div>
|
||||
|
||||
<Button @click="onShowConfigure()">{{ $t('settings.privateDockerRegistry.configureAction') }}</Button>
|
||||
<TableView :columns="columns" :model="registries">
|
||||
<template #actions="registry">
|
||||
<div class="table-actions">
|
||||
<Button small tool secondary @click="onEditOrAdd(registry)" icon="fa fa-pencil-alt" />
|
||||
<Button small tool danger @click="onRemove(registry)" icon="far fa-trash-alt" />
|
||||
</div>
|
||||
</template>
|
||||
</TableView>
|
||||
</Section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user