Add user registries UI (#3888)

This commit is contained in:
Lauris BH 2024-07-13 06:32:10 +03:00 committed by GitHub
parent 2d607a9ae4
commit a26c7a475b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 124 additions and 2 deletions

5
web/components.d.ts vendored
View file

@ -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']
} }

View file

@ -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",

View 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>

View file

@ -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';