mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-25 19:31:05 +00:00
Add user registries UI (#3888)
This commit is contained in:
parent
2d607a9ae4
commit
a26c7a475b
4 changed files with 124 additions and 2 deletions
5
web/components.d.ts
vendored
5
web/components.d.ts
vendored
|
@ -67,12 +67,12 @@ declare module 'vue' {
|
||||||
IMdiPlay: typeof import('~icons/mdi/play')['default']
|
IMdiPlay: typeof import('~icons/mdi/play')['default']
|
||||||
IMdiRadioboxBlank: typeof import('~icons/mdi/radiobox-blank')['default']
|
IMdiRadioboxBlank: typeof import('~icons/mdi/radiobox-blank')['default']
|
||||||
IMdiRadioboxIndeterminateVariant: typeof import('~icons/mdi/radiobox-indeterminate-variant')['default']
|
IMdiRadioboxIndeterminateVariant: typeof import('~icons/mdi/radiobox-indeterminate-variant')['default']
|
||||||
IMdiSync: typeof import('~icons/mdi/sync')['default']
|
|
||||||
IMdiSourceBranch: typeof import('~icons/mdi/source-branch')['default']
|
IMdiSourceBranch: typeof import('~icons/mdi/source-branch')['default']
|
||||||
IMdiSourceCommit: typeof import('~icons/mdi/source-commit')['default']
|
IMdiSourceCommit: typeof import('~icons/mdi/source-commit')['default']
|
||||||
IMdiSourceMerge: typeof import('~icons/mdi/source-merge')['default']
|
IMdiSourceMerge: typeof import('~icons/mdi/source-merge')['default']
|
||||||
IMdiSourcePull: typeof import('~icons/mdi/source-pull')['default']
|
IMdiSourcePull: typeof import('~icons/mdi/source-pull')['default']
|
||||||
IMdiStop: typeof import('~icons/mdi/stop')['default']
|
IMdiStop: typeof import('~icons/mdi/stop')['default']
|
||||||
|
IMdiSync: typeof import('~icons/mdi/sync')['default']
|
||||||
IMdiTagOutline: typeof import('~icons/mdi/tag-outline')['default']
|
IMdiTagOutline: typeof import('~icons/mdi/tag-outline')['default']
|
||||||
InputField: typeof import('./src/components/form/InputField.vue')['default']
|
InputField: typeof import('./src/components/form/InputField.vue')['default']
|
||||||
IPhGitlabLogoSimpleFill: typeof import('~icons/ph/gitlab-logo-simple-fill')['default']
|
IPhGitlabLogoSimpleFill: typeof import('~icons/ph/gitlab-logo-simple-fill')['default']
|
||||||
|
@ -100,8 +100,8 @@ declare module 'vue' {
|
||||||
PipelineStepList: typeof import('./src/components/repo/pipeline/PipelineStepList.vue')['default']
|
PipelineStepList: typeof import('./src/components/repo/pipeline/PipelineStepList.vue')['default']
|
||||||
Popup: typeof import('./src/components/layout/Popup.vue')['default']
|
Popup: typeof import('./src/components/layout/Popup.vue')['default']
|
||||||
RadioField: typeof import('./src/components/form/RadioField.vue')['default']
|
RadioField: typeof import('./src/components/form/RadioField.vue')['default']
|
||||||
RegistryEdit: typeof import('./src/components/registry/RegistryEdit.vue')['default']
|
|
||||||
RegistriesTab: typeof import('./src/components/repo/settings/RegistriesTab.vue')['default']
|
RegistriesTab: typeof import('./src/components/repo/settings/RegistriesTab.vue')['default']
|
||||||
|
RegistryEdit: typeof import('./src/components/registry/RegistryEdit.vue')['default']
|
||||||
RegistryList: typeof import('./src/components/registry/RegistryList.vue')['default']
|
RegistryList: typeof import('./src/components/registry/RegistryList.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
@ -116,6 +116,7 @@ declare module 'vue' {
|
||||||
TextField: typeof import('./src/components/form/TextField.vue')['default']
|
TextField: typeof import('./src/components/form/TextField.vue')['default']
|
||||||
UserCLIAndAPITab: typeof import('./src/components/user/UserCLIAndAPITab.vue')['default']
|
UserCLIAndAPITab: typeof import('./src/components/user/UserCLIAndAPITab.vue')['default']
|
||||||
UserGeneralTab: typeof import('./src/components/user/UserGeneralTab.vue')['default']
|
UserGeneralTab: typeof import('./src/components/user/UserGeneralTab.vue')['default']
|
||||||
|
UserRegistriesTab: typeof import('./src/components/user/UserRegistriesTab.vue')['default']
|
||||||
UserSecretsTab: typeof import('./src/components/user/UserSecretsTab.vue')['default']
|
UserSecretsTab: typeof import('./src/components/user/UserSecretsTab.vue')['default']
|
||||||
Warning: typeof import('./src/components/atomic/Warning.vue')['default']
|
Warning: typeof import('./src/components/atomic/Warning.vue')['default']
|
||||||
}
|
}
|
||||||
|
|
|
@ -388,6 +388,9 @@
|
||||||
"secrets": {
|
"secrets": {
|
||||||
"desc": "User secrets can be passed to all user's repository individual pipeline steps at runtime as environmental variables."
|
"desc": "User secrets can be passed to all user's repository individual pipeline steps at runtime as environmental variables."
|
||||||
},
|
},
|
||||||
|
"registries": {
|
||||||
|
"desc": "User registries credentials can be added to use private images for all individual pipelines."
|
||||||
|
},
|
||||||
"cli_and_api": {
|
"cli_and_api": {
|
||||||
"cli_and_api": "CLI & API",
|
"cli_and_api": "CLI & API",
|
||||||
"desc": "Personal Access Token, CLI and API usage",
|
"desc": "Personal Access Token, CLI and API usage",
|
||||||
|
|
114
web/src/components/user/UserRegistriesTab.vue
Normal file
114
web/src/components/user/UserRegistriesTab.vue
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
<template>
|
||||||
|
<Panel>
|
||||||
|
<div class="flex flex-row border-b mb-4 pb-4 items-center dark:border-wp-background-100">
|
||||||
|
<div class="ml-2">
|
||||||
|
<h1 class="text-xl text-wp-text-100">{{ $t('registries.registries') }}</h1>
|
||||||
|
<p class="text-sm text-wp-text-alt-100">
|
||||||
|
{{ $t('user.settings.registries.desc') }}
|
||||||
|
<DocsLink :topic="$t('registries.registries')" url="docs/usage/registries" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
v-if="selectedRegistry"
|
||||||
|
class="ml-auto"
|
||||||
|
:text="$t('registries.show')"
|
||||||
|
start-icon="back"
|
||||||
|
@click="selectedRegistry = undefined"
|
||||||
|
/>
|
||||||
|
<Button v-else class="ml-auto" :text="$t('registries.add')" start-icon="plus" @click="showAddRegistry" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RegistryList
|
||||||
|
v-if="!selectedRegistry"
|
||||||
|
v-model="registries"
|
||||||
|
:is-deleting="isDeleting"
|
||||||
|
@edit="editRegistry"
|
||||||
|
@delete="deleteRegistry"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RegistryEdit
|
||||||
|
v-else
|
||||||
|
v-model="selectedRegistry"
|
||||||
|
:is-saving="isSaving"
|
||||||
|
@save="createRegistry"
|
||||||
|
@cancel="selectedRegistry = undefined"
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import Button from '~/components/atomic/Button.vue';
|
||||||
|
import DocsLink from '~/components/atomic/DocsLink.vue';
|
||||||
|
import Panel from '~/components/layout/Panel.vue';
|
||||||
|
import RegistryEdit from '~/components/registry/RegistryEdit.vue';
|
||||||
|
import RegistryList from '~/components/registry/RegistryList.vue';
|
||||||
|
import useApiClient from '~/compositions/useApiClient';
|
||||||
|
import { useAsyncAction } from '~/compositions/useAsyncAction';
|
||||||
|
import useAuthentication from '~/compositions/useAuthentication';
|
||||||
|
import useNotifications from '~/compositions/useNotifications';
|
||||||
|
import { usePagination } from '~/compositions/usePaginate';
|
||||||
|
import type { Registry } from '~/lib/api/types';
|
||||||
|
|
||||||
|
const emptyRegistry: Partial<Registry> = {
|
||||||
|
address: '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const apiClient = useApiClient();
|
||||||
|
const notifications = useNotifications();
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const { user } = useAuthentication();
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('Unexpected: Unauthenticated');
|
||||||
|
}
|
||||||
|
const selectedRegistry = ref<Partial<Registry>>();
|
||||||
|
const isEditingRegistry = computed(() => !!selectedRegistry.value?.id);
|
||||||
|
|
||||||
|
async function loadRegistries(page: number): Promise<Registry[] | null> {
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('Unexpected: Unauthenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiClient.getOrgRegistryList(user.org_id, { page });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { resetPage, data: registries } = usePagination(loadRegistries, () => !selectedRegistry.value);
|
||||||
|
|
||||||
|
const { doSubmit: createRegistry, isLoading: isSaving } = useAsyncAction(async () => {
|
||||||
|
if (!selectedRegistry.value) {
|
||||||
|
throw new Error("Unexpected: Can't get registry");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditingRegistry.value) {
|
||||||
|
await apiClient.updateOrgRegistry(user.org_id, selectedRegistry.value);
|
||||||
|
} else {
|
||||||
|
await apiClient.createOrgRegistry(user.org_id, selectedRegistry.value);
|
||||||
|
}
|
||||||
|
notifications.notify({
|
||||||
|
title: isEditingRegistry.value ? i18n.t('registries.saved') : i18n.t('registries.created'),
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
selectedRegistry.value = undefined;
|
||||||
|
resetPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
const { doSubmit: deleteRegistry, isLoading: isDeleting } = useAsyncAction(async (_registry: Registry) => {
|
||||||
|
await apiClient.deleteOrgRegistry(user.org_id, _registry.address);
|
||||||
|
notifications.notify({ title: i18n.t('registries.deleted'), type: 'success' });
|
||||||
|
resetPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
function editRegistry(registry: Registry) {
|
||||||
|
selectedRegistry.value = cloneDeep(registry);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAddRegistry() {
|
||||||
|
selectedRegistry.value = cloneDeep(emptyRegistry);
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -8,6 +8,9 @@
|
||||||
<Tab id="secrets" :title="$t('secrets.secrets')">
|
<Tab id="secrets" :title="$t('secrets.secrets')">
|
||||||
<UserSecretsTab />
|
<UserSecretsTab />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab id="registries" :title="$t('registries.registries')">
|
||||||
|
<UserRegistriesTab />
|
||||||
|
</Tab>
|
||||||
<Tab id="cli-and-api" :title="$t('user.settings.cli_and_api.cli_and_api')">
|
<Tab id="cli-and-api" :title="$t('user.settings.cli_and_api.cli_and_api')">
|
||||||
<UserCLIAndAPITab />
|
<UserCLIAndAPITab />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
@ -19,6 +22,7 @@ import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
|
||||||
import Tab from '~/components/layout/scaffold/Tab.vue';
|
import Tab from '~/components/layout/scaffold/Tab.vue';
|
||||||
import UserCLIAndAPITab from '~/components/user/UserCLIAndAPITab.vue';
|
import UserCLIAndAPITab from '~/components/user/UserCLIAndAPITab.vue';
|
||||||
import UserGeneralTab from '~/components/user/UserGeneralTab.vue';
|
import UserGeneralTab from '~/components/user/UserGeneralTab.vue';
|
||||||
|
import UserRegistriesTab from '~/components/user/UserRegistriesTab.vue';
|
||||||
import UserSecretsTab from '~/components/user/UserSecretsTab.vue';
|
import UserSecretsTab from '~/components/user/UserSecretsTab.vue';
|
||||||
import useConfig from '~/compositions/useConfig';
|
import useConfig from '~/compositions/useConfig';
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue