Global and organization registries (#1672)

Co-authored-by: Anbraten <6918444+anbraten@users.noreply.github.com>
This commit is contained in:
Lauris BH
2024-07-03 16:33:11 +03:00
committed by GitHub
parent e5f3e67bf2
commit 28e982fffb
65 changed files with 3260 additions and 269 deletions

6
web/components.d.ts vendored
View File

@@ -14,6 +14,7 @@ declare module 'vue' {
AdminOrgsTab: typeof import('./src/components/admin/settings/AdminOrgsTab.vue')['default']
AdminQueueStats: typeof import('./src/components/admin/settings/queue/AdminQueueStats.vue')['default']
AdminQueueTab: typeof import('./src/components/admin/settings/AdminQueueTab.vue')['default']
AdminRegistriesTab: typeof import('./src/components/admin/settings/AdminRegistriesTab.vue')['default']
AdminReposTab: typeof import('./src/components/admin/settings/AdminReposTab.vue')['default']
AdminSecretsTab: typeof import('./src/components/admin/settings/AdminSecretsTab.vue')['default']
AdminUsersTab: typeof import('./src/components/admin/settings/AdminUsersTab.vue')['default']
@@ -66,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']
@@ -85,6 +86,7 @@ declare module 'vue' {
ManualPipelinePopup: typeof import('./src/components/layout/popups/ManualPipelinePopup.vue')['default']
Navbar: typeof import('./src/components/layout/header/Navbar.vue')['default']
NumberField: typeof import('./src/components/form/NumberField.vue')['default']
OrgRegistriesTab: typeof import('./src/components/org/settings/OrgRegistriesTab.vue')['default']
OrgSecretsTab: typeof import('./src/components/org/settings/OrgSecretsTab.vue')['default']
Panel: typeof import('./src/components/layout/Panel.vue')['default']
PipelineFeedItem: typeof import('./src/components/pipeline-feed/PipelineFeedItem.vue')['default']
@@ -98,7 +100,9 @@ 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']
RegistryList: typeof import('./src/components/registry/RegistryList.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Scaffold: typeof import('./src/components/layout/scaffold/Scaffold.vue')['default']

View File

@@ -122,24 +122,6 @@
"desc": "Enable to cancel pending and running pipelines of the same event and context before starting the newly triggered one."
}
},
"registries": {
"registries": "Registries",
"credentials": "Registry credentials",
"desc": "Registries credentials can be added to use private images for your pipeline.",
"show": "Show registries",
"add": "Add registry",
"none": "There are no registry credentials yet.",
"save": "Save registry",
"created": "Registry credentials created",
"saved": "Registry credentials saved",
"deleted": "Registry credentials deleted",
"address": {
"address": "Address",
"placeholder": "Registry Address (e.g. docker.io)"
},
"edit": "Edit registry",
"delete": "Delete registry"
},
"crons": {
"crons": "Crons",
"desc": "Cron jobs can be used to trigger pipelines on a regular basis.",
@@ -266,6 +248,9 @@
"not_allowed": "You are not allowed to access this organization's settings",
"secrets": {
"desc": "Organization secrets can be passed to all organization's repository individual pipeline steps at runtime as environmental variables."
},
"registries": {
"desc": "Organization registry credentials can be added to use private images for all organization's pipelines."
}
}
},
@@ -276,6 +261,10 @@
"desc": "Global secrets can be passed to all repositories individual pipeline steps at runtime as environmental variables.",
"warning": "These secrets will be available for all server users."
},
"registries": {
"desc": "Global registry credentials can be added to use private images for all server's pipelines.",
"warning": "These registry creditentials will be available for all server users."
},
"agents": {
"agents": "Agents",
"desc": "Agents registered for this server",
@@ -435,6 +424,26 @@
"edit": "Edit secret",
"delete": "Delete secret"
},
"registries": {
"registries": "Registries",
"credentials": "Registry credentials",
"desc": "Registries credentials can be added to use private images for your pipeline.",
"none": "There are no registry credentials yet.",
"address": {
"address": "Address",
"desc": "Registry Address (e.g. docker.io)"
},
"show": "Show registries",
"save": "Save registry",
"add": "Add registry",
"view": "View registry",
"edit": "Edit registry",
"delete": "Delete registry",
"delete_confirm": "Do you really want to delete this registry?",
"created": "Registry credentials created",
"saved": "Registry credentials saved",
"deleted": "Registry credentials deleted"
},
"default": "default",
"info": "Info",
"running_version": "You are running Woodpecker {0}",

View File

@@ -0,0 +1,101 @@
<template>
<Settings
:title="$t('registries.registries')"
:desc="$t('admin.settings.registries.desc')"
docs-url="docs/usage/registries"
:warning="$t('admin.settings.registries.warning')"
>
<template #titleActions>
<Button
v-if="selectedRegistry"
:text="$t('registries.show')"
start-icon="back"
@click="selectedRegistry = undefined"
/>
<Button v-else :text="$t('registries.add')" start-icon="plus" @click="showAddRegistry" />
</template>
<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"
/>
</Settings>
</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 Settings from '~/components/layout/Settings.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 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 selectedRegistry = ref<Partial<Registry>>();
const isEditingRegistry = computed(() => !!selectedRegistry.value?.id);
async function loadRegistries(page: number): Promise<Registry[] | null> {
return apiClient.getGlobalRegistryList({ 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.updateGlobalRegistry(selectedRegistry.value);
} else {
await apiClient.createGlobalRegistry(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.deleteGlobalRegistry(_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

@@ -3,7 +3,7 @@
<button
v-for="tab in tabs"
:key="tab.id"
class="w-full py-1 md:py-2 md:w-auto md:px-8 flex cursor-pointer md:border-b-2 text-wp-text-100 hover:text-wp-text-200 items-center"
class="w-full py-1 md:py-2 md:w-auto md:px-6 flex cursor-pointer md:border-b-2 text-wp-text-100 hover:text-wp-text-200 items-center"
:class="{
'border-wp-text-100': activeTab === tab.id,
'border-transparent': activeTab !== tab.id,

View File

@@ -0,0 +1,113 @@
<template>
<Settings
:title="$t('registries.registries')"
:desc="$t('org.settings.registries.desc')"
docs-url="docs/usage/registries"
>
<template #titleActions>
<Button
v-if="selectedRegistry"
:text="$t('registries.show')"
start-icon="back"
@click="selectedRegistry = undefined"
/>
<Button v-else :text="$t('registries.add')" start-icon="plus" @click="showAddRegistry" />
</template>
<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"
/>
</Settings>
</template>
<script lang="ts" setup>
import { cloneDeep } from 'lodash';
import { computed, inject, ref, type Ref } from 'vue';
import { useI18n } from 'vue-i18n';
import Button from '~/components/atomic/Button.vue';
import Settings from '~/components/layout/Settings.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 useNotifications from '~/compositions/useNotifications';
import { usePagination } from '~/compositions/usePaginate';
import type { Org, Registry } from '~/lib/api/types';
const emptyRegistry: Partial<Registry> = {
address: '',
username: '',
password: '',
};
const apiClient = useApiClient();
const notifications = useNotifications();
const i18n = useI18n();
const org = inject<Ref<Org>>('org');
const selectedRegistry = ref<Partial<Registry>>();
const isEditing = computed(() => !!selectedRegistry.value?.id);
async function loadRegistries(page: number): Promise<Registry[] | null> {
if (!org?.value) {
throw new Error("Unexpected: Can't load org");
}
return apiClient.getOrgRegistryList(org.value.id, { page });
}
const { resetPage, data: registries } = usePagination(loadRegistries, () => !selectedRegistry.value);
const { doSubmit: createRegistry, isLoading: isSaving } = useAsyncAction(async () => {
if (!org?.value) {
throw new Error("Unexpected: Can't load org");
}
if (!selectedRegistry.value) {
throw new Error("Unexpected: Can't get registry");
}
if (isEditing.value) {
await apiClient.updateOrgRegistry(org.value.id, selectedRegistry.value);
} else {
await apiClient.createOrgRegistry(org.value.id, selectedRegistry.value);
}
notifications.notify({
title: isEditing.value ? i18n.t('registries.saved') : i18n.t('registries.created'),
type: 'success',
});
selectedRegistry.value = undefined;
resetPage();
});
const { doSubmit: deleteRegistry, isLoading: isDeleting } = useAsyncAction(async (_registry: Registry) => {
if (!org?.value) {
throw new Error("Unexpected: Can't load org");
}
await apiClient.deleteOrgRegistry(org.value.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

@@ -0,0 +1,78 @@
<template>
<div v-if="innerValue" class="space-y-4">
<form @submit.prevent="save">
<InputField v-slot="{ id }" :label="$t('registries.address.address')">
<!-- TODO: check input field Address is a valid address -->
<TextField
:id="id"
v-model="innerValue.address"
:placeholder="$t('registries.address.desc')"
required
:disabled="isEditing || isReadOnly"
/>
</InputField>
<InputField v-slot="{ id }" :label="$t('username')">
<TextField
:id="id"
v-model="innerValue.username"
:placeholder="$t('username')"
required
:disabled="isReadOnly"
/>
</InputField>
<InputField v-if="!isReadOnly" v-slot="{ id }" :label="$t('password')">
<TextField :id="id" v-model="innerValue.password" :placeholder="$t('password')" :required="!isEditing" />
</InputField>
<div v-if="!isReadOnly" class="flex gap-2">
<Button type="button" color="gray" :text="$t('cancel')" @click="$emit('cancel')" />
<Button
type="submit"
color="green"
:is-loading="isSaving"
:text="isEditing ? $t('registries.save') : $t('registries.add')"
/>
</div>
</form>
</div>
</template>
<script lang="ts" setup>
import { computed, toRef } from 'vue';
import Button from '~/components/atomic/Button.vue';
import InputField from '~/components/form/InputField.vue';
import TextField from '~/components/form/TextField.vue';
import type { Registry } from '~/lib/api/types';
const props = defineProps<{
modelValue: Partial<Registry>;
isSaving: boolean;
}>();
const emit = defineEmits<{
(event: 'update:modelValue', value: Partial<Registry> | undefined): void;
(event: 'save', value: Partial<Registry>): void;
(event: 'cancel'): void;
}>();
const modelValue = toRef(props, 'modelValue');
const innerValue = computed({
get: () => modelValue.value,
set: (value) => {
emit('update:modelValue', value);
},
});
const isEditing = computed(() => !!innerValue.value?.id);
const isReadOnly = computed(() => !!innerValue.value?.readonly);
function save() {
if (!innerValue.value) {
return;
}
emit('save', innerValue.value);
}
</script>

View File

@@ -0,0 +1,63 @@
<template>
<div class="space-y-4 text-wp-text-100">
<ListItem
v-for="registry in registries"
:key="registry.id"
class="items-center !bg-wp-background-200 !dark:bg-wp-background-100"
>
<span>{{ registry.address }}</span>
<IconButton
:icon="registry.readonly ? 'chevron-right' : 'edit'"
class="ml-auto w-8 h-8"
:title="registry.readonly ? $t('registries.view') : $t('registries.edit')"
@click="editRegistry(registry)"
/>
<IconButton
v-if="!registry.readonly"
icon="trash"
class="w-8 h-8 hover:text-wp-control-error-100"
:is-loading="isDeleting"
:title="$t('registries.delete')"
@click="deleteRegistry(registry)"
/>
</ListItem>
<div v-if="registries?.length === 0" class="ml-2">{{ $t('registries.none') }}</div>
</div>
</template>
<script lang="ts" setup>
import { toRef } from 'vue';
import { useI18n } from 'vue-i18n';
import IconButton from '~/components/atomic/IconButton.vue';
import ListItem from '~/components/atomic/ListItem.vue';
import type { Registry } from '~/lib/api/types';
const props = defineProps<{
modelValue: (Registry & { edit?: boolean })[];
isDeleting: boolean;
}>();
const emit = defineEmits<{
(event: 'edit', registry: Registry): void;
(event: 'delete', registry: Registry): void;
}>();
const i18n = useI18n();
const registries = toRef(props, 'modelValue');
function editRegistry(registry: Registry) {
emit('edit', registry);
}
function deleteRegistry(registry: Registry) {
// TODO: use proper dialog
// eslint-disable-next-line no-alert
if (!confirm(i18n.t('registries.delete_confirm'))) {
return;
}
emit('delete', registry);
}
</script>

View File

@@ -1,95 +1,54 @@
<template>
<Settings
:title="$t('repo.settings.registries.credentials')"
:desc="$t('repo.settings.registries.desc')"
docs-url="docs/usage/registries"
>
<Settings :title="$t('registries.credentials')" :desc="$t('registries.desc')" docs-url="docs/usage/registries">
<template #titleActions>
<Button
v-if="selectedRegistry"
:text="$t('registries.show')"
start-icon="back"
:text="$t('repo.settings.registries.show')"
@click="selectedRegistry = undefined"
/>
<Button v-else start-icon="plus" :text="$t('repo.settings.registries.add')" @click="selectedRegistry = {}" />
<Button v-else :text="$t('registries.add')" start-icon="plus" @click="showAddRegistry" />
</template>
<div v-if="!selectedRegistry" class="space-y-4 text-wp-text-100">
<ListItem
v-for="registry in registries"
:key="registry.id"
class="items-center !bg-wp-background-200 !dark:bg-wp-background-100"
>
<span>{{ registry.address }}</span>
<IconButton
icon="edit"
class="ml-auto w-8 h-8"
:title="$t('repo.settings.registries.edit')"
@click="selectedRegistry = registry"
/>
<IconButton
icon="trash"
class="w-8 h-8 hover:text-wp-control-error-100"
:is-loading="isDeleting"
:title="$t('repo.settings.registries.delete')"
@click="deleteRegistry(registry)"
/>
</ListItem>
<RegistryList
v-if="!selectedRegistry"
v-model="registries"
:is-deleting="isDeleting"
@edit="editRegistry"
@delete="deleteRegistry"
/>
<div v-if="registries?.length === 0" class="ml-2">{{ $t('repo.settings.registries.none') }}</div>
</div>
<div v-else class="space-y-4">
<form @submit.prevent="createRegistry">
<InputField v-slot="{ id }" :label="$t('repo.settings.registries.address.address')">
<!-- TODO: check input field Address is a valid address -->
<TextField
:id="id"
v-model="selectedRegistry.address"
:placeholder="$t('repo.settings.registries.address.placeholder')"
required
:disabled="isEditingRegistry"
/>
</InputField>
<InputField v-slot="{ id }" :label="$t('username')">
<TextField :id="id" v-model="selectedRegistry.username" :placeholder="$t('username')" required />
</InputField>
<InputField v-slot="{ id }" :label="$t('password')">
<TextField :id="id" v-model="selectedRegistry.password" :placeholder="$t('password')" required />
</InputField>
<div class="flex gap-2">
<Button type="button" color="gray" :text="$t('cancel')" @click="selectedRegistry = undefined" />
<Button
type="submit"
color="green"
:is-loading="isSaving"
:text="isEditingRegistry ? $t('repo.settings.registries.save') : $t('repo.settings.registries.add')"
/>
</div>
</form>
</div>
<RegistryEdit
v-else
v-model="selectedRegistry"
:is-saving="isSaving"
@save="createRegistry"
@cancel="selectedRegistry = undefined"
/>
</Settings>
</template>
<script lang="ts" setup>
import { cloneDeep } from 'lodash';
import { computed, inject, ref, type Ref } from 'vue';
import { useI18n } from 'vue-i18n';
import Button from '~/components/atomic/Button.vue';
import IconButton from '~/components/atomic/IconButton.vue';
import ListItem from '~/components/atomic/ListItem.vue';
import InputField from '~/components/form/InputField.vue';
import TextField from '~/components/form/TextField.vue';
import Settings from '~/components/layout/Settings.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 useNotifications from '~/compositions/useNotifications';
import { usePagination } from '~/compositions/usePaginate';
import type { Registry, Repo } from '~/lib/api/types';
const emptyRegistry: Partial<Registry> = {
address: '',
username: '',
password: '',
};
const apiClient = useApiClient();
const notifications = useNotifications();
const i18n = useI18n();
@@ -123,9 +82,7 @@ const { doSubmit: createRegistry, isLoading: isSaving } = useAsyncAction(async (
await apiClient.createRegistry(repo.value.id, selectedRegistry.value);
}
notifications.notify({
title: isEditingRegistry.value
? i18n.t('repo.settings.registries.saved')
: i18n.t('repo.settings.registries.created'),
title: isEditingRegistry.value ? i18n.t('registries.saved') : i18n.t('registries.created'),
type: 'success',
});
selectedRegistry.value = undefined;
@@ -139,7 +96,15 @@ const { doSubmit: deleteRegistry, isLoading: isDeleting } = useAsyncAction(async
const registryAddress = encodeURIComponent(_registry.address);
await apiClient.deleteRegistry(repo.value.id, registryAddress);
notifications.notify({ title: i18n.t('repo.settings.registries.deleted'), type: 'success' });
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

@@ -170,19 +170,53 @@ export default class WoodpeckerClient extends ApiClient {
getRegistryList(repoId: number, opts?: PaginationOptions): Promise<Registry[] | null> {
const query = encodeQueryString(opts);
return this._get(`/api/repos/${repoId}/registry?${query}`) as Promise<Registry[] | null>;
return this._get(`/api/repos/${repoId}/registries?${query}`) as Promise<Registry[] | null>;
}
createRegistry(repoId: number, registry: Partial<Registry>): Promise<unknown> {
return this._post(`/api/repos/${repoId}/registry`, registry);
return this._post(`/api/repos/${repoId}/registries`, registry);
}
updateRegistry(repoId: number, registry: Partial<Registry>): Promise<unknown> {
return this._patch(`/api/repos/${repoId}/registry/${registry.address}`, registry);
return this._patch(`/api/repos/${repoId}/registries/${registry.address}`, registry);
}
deleteRegistry(repoId: number, registryAddress: string): Promise<unknown> {
return this._delete(`/api/repos/${repoId}/registry/${registryAddress}`);
return this._delete(`/api/repos/${repoId}/registries/${registryAddress}`);
}
getOrgRegistryList(orgId: number, opts?: PaginationOptions): Promise<Registry[] | null> {
const query = encodeQueryString(opts);
return this._get(`/api/orgs/${orgId}/registries?${query}`) as Promise<Registry[] | null>;
}
createOrgRegistry(orgId: number, registry: Partial<Registry>): Promise<unknown> {
return this._post(`/api/orgs/${orgId}/registries`, registry);
}
updateOrgRegistry(orgId: number, registry: Partial<Registry>): Promise<unknown> {
return this._patch(`/api/orgs/${orgId}/registries/${registry.address}`, registry);
}
deleteOrgRegistry(orgId: number, registryAddress: string): Promise<unknown> {
return this._delete(`/api/orgs/${orgId}/registries/${registryAddress}`);
}
getGlobalRegistryList(opts?: PaginationOptions): Promise<Registry[] | null> {
const query = encodeQueryString(opts);
return this._get(`/api/registries?${query}`) as Promise<Registry[] | null>;
}
createGlobalRegistry(registry: Partial<Registry>): Promise<unknown> {
return this._post(`/api/registries`, registry);
}
updateGlobalRegistry(registry: Partial<Registry>): Promise<unknown> {
return this._patch(`/api/registries/${registry.address}`, registry);
}
deleteGlobalRegistry(registryAddress: string): Promise<unknown> {
return this._delete(`/api/registries/${registryAddress}`);
}
getCronList(repoId: number, opts?: PaginationOptions): Promise<Cron[] | null> {

View File

@@ -1,6 +1,9 @@
export interface Registry {
id: string;
repo_id: number;
org_id: number;
address: string;
username: string;
password: string;
readonly: boolean;
}

View File

@@ -9,6 +9,9 @@
<Tab id="secrets" :title="$t('secrets.secrets')">
<AdminSecretsTab />
</Tab>
<Tab id="registries" :title="$t('registries.registries')">
<AdminRegistriesTab />
</Tab>
<Tab id="repos" :title="$t('admin.settings.repos.repos')">
<AdminReposTab />
</Tab>
@@ -36,6 +39,7 @@ import AdminAgentsTab from '~/components/admin/settings/AdminAgentsTab.vue';
import AdminInfoTab from '~/components/admin/settings/AdminInfoTab.vue';
import AdminOrgsTab from '~/components/admin/settings/AdminOrgsTab.vue';
import AdminQueueTab from '~/components/admin/settings/AdminQueueTab.vue';
import AdminRegistriesTab from '~/components/admin/settings/AdminRegistriesTab.vue';
import AdminReposTab from '~/components/admin/settings/AdminReposTab.vue';
import AdminSecretsTab from '~/components/admin/settings/AdminSecretsTab.vue';
import AdminUsersTab from '~/components/admin/settings/AdminUsersTab.vue';

View File

@@ -14,6 +14,10 @@
<Tab id="secrets" :title="$t('secrets.secrets')">
<OrgSecretsTab />
</Tab>
<Tab id="registries" :title="$t('registries.registries')">
<OrgRegistriesTab />
</Tab>
</Scaffold>
</template>
@@ -23,6 +27,7 @@ import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import Tab from '~/components/layout/scaffold/Tab.vue';
import OrgRegistriesTab from '~/components/org/settings/OrgRegistriesTab.vue';
import OrgSecretsTab from '~/components/org/settings/OrgSecretsTab.vue';
import { inject } from '~/compositions/useInjectProvide';
import useNotifications from '~/compositions/useNotifications';

View File

@@ -2,15 +2,15 @@
<Scaffold enable-tabs :go-back="goBack">
<template #title>
<span>
<router-link :to="{ name: 'org', params: { orgId: repo!.org_id } }" class="hover:underline">
{{ repo!.owner }}
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
</router-link>
<router-link :to="{ name: 'org', params: { orgId: repo!.org_id } }" class="hover:underline">{{
repo!.owner
/* eslint-disable-next-line @intlify/vue-i18n/no-raw-text */
}}</router-link>
/
<router-link :to="{ name: 'repo' }" class="hover:underline">
{{ repo!.name }}
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
</router-link>
<router-link :to="{ name: 'repo' }" class="hover:underline">{{
repo!.name
/* eslint-disable-next-line @intlify/vue-i18n/no-raw-text */
}}</router-link>
/
{{ $t('settings') }}
</span>
@@ -22,7 +22,7 @@
<Tab id="secrets" :title="$t('secrets.secrets')">
<SecretsTab />
</Tab>
<Tab id="registries" :title="$t('repo.settings.registries.registries')">
<Tab id="registries" :title="$t('registries.registries')">
<RegistriesTab />
</Tab>
<Tab id="crons" :title="$t('repo.settings.crons.crons')">

View File

@@ -9,8 +9,10 @@
<span class="flex">
<router-link :to="{ name: 'org', params: { orgId: repo.org_id } }" class="hover:underline">{{
repo.owner
/* eslint-disable-next-line @intlify/vue-i18n/no-raw-text */
}}</router-link>
{{ `&nbsp;/&nbsp;${repo.name}` }}
&nbsp;/
{{ repo.name }}
</span>
</template>
<template #titleActions>

View File

@@ -10,14 +10,12 @@
>
<template #title>
<span>
<router-link :to="{ name: 'org', params: { orgId: repo.org_id } }" class="hover:underline">
{{ repo.owner }}
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
</router-link>
<router-link :to="{ name: 'org', params: { orgId: repo.org_id } }" class="hover:underline">{{
repo.owner
/* eslint-disable-next-line @intlify/vue-i18n/no-raw-text */
}}</router-link>
/
<router-link :to="{ name: 'repo' }" class="hover:underline">
{{ repo.name }}
</router-link>
<router-link :to="{ name: 'repo' }" class="hover:underline">{{ repo.name }}</router-link>
</span>
</template>