Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user registries UI #3888

Merged
merged 6 commits into from
Jul 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions web/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ declare module 'vue' {
IMdiPlay: typeof import('~icons/mdi/play')['default']
IMdiRadioboxBlank: typeof import('~icons/mdi/radiobox-blank')['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']
IMdiSourceCommit: typeof import('~icons/mdi/source-commit')['default']
IMdiSourceMerge: typeof import('~icons/mdi/source-merge')['default']
IMdiSourcePull: typeof import('~icons/mdi/source-pull')['default']
IMdiStop: typeof import('~icons/mdi/stop')['default']
IMdiSync: typeof import('~icons/mdi/sync')['default']
IMdiTagOutline: typeof import('~icons/mdi/tag-outline')['default']
InputField: typeof import('./src/components/form/InputField.vue')['default']
IPhGitlabLogoSimpleFill: typeof import('~icons/ph/gitlab-logo-simple-fill')['default']
Expand Down Expand Up @@ -100,8 +100,8 @@ declare module 'vue' {
PipelineStepList: typeof import('./src/components/repo/pipeline/PipelineStepList.vue')['default']
Popup: typeof import('./src/components/layout/Popup.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']
RegistryEdit: typeof import('./src/components/registry/RegistryEdit.vue')['default']
RegistryList: typeof import('./src/components/registry/RegistryList.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Expand All @@ -116,6 +116,7 @@ declare module 'vue' {
TextField: typeof import('./src/components/form/TextField.vue')['default']
UserCLIAndAPITab: typeof import('./src/components/user/UserCLIAndAPITab.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']
Warning: typeof import('./src/components/atomic/Warning.vue')['default']
}
Expand Down
3 changes: 3 additions & 0 deletions web/src/assets/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,9 @@
"secrets": {
"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 & API",
"desc": "Personal Access Token, CLI and API usage",
Expand Down
114 changes: 114 additions & 0 deletions web/src/components/user/UserRegistriesTab.vue
Original file line number Diff line number Diff line change
@@ -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>
4 changes: 4 additions & 0 deletions web/src/views/User.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
<Tab id="secrets" :title="$t('secrets.secrets')">
<UserSecretsTab />
</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')">
<UserCLIAndAPITab />
</Tab>
Expand All @@ -19,6 +22,7 @@ import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue';
import UserCLIAndAPITab from '~/components/user/UserCLIAndAPITab.vue';
import UserGeneralTab from '~/components/user/UserGeneralTab.vue';
import UserRegistriesTab from '~/components/user/UserRegistriesTab.vue';
import UserSecretsTab from '~/components/user/UserSecretsTab.vue';
import useConfig from '~/compositions/useConfig';

Expand Down