Use separate routes instead of anchors (#4285)

Co-authored-by: Anbraten <anton@ju60.de>
This commit is contained in:
qwerty287 2024-11-19 15:04:50 +02:00 committed by GitHub
parent 5c2204716c
commit 95e464b7cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 339 additions and 369 deletions

View file

@ -18,8 +18,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue';
import Container from '~/components/layout/Container.vue'; import Container from '~/components/layout/Container.vue';
import { useTabsProvider } from '~/compositions/useTabs'; import { useTabsProvider } from '~/compositions/useTabs';
@ -33,37 +31,16 @@ const props = defineProps<{
// Tabs // Tabs
enableTabs?: boolean; enableTabs?: boolean;
disableTabUrlHashMode?: boolean;
activeTab?: string;
// Content // Content
fluidContent?: boolean; fluidContent?: boolean;
}>(); }>();
const emit = defineEmits<{ defineEmits<{
(event: 'update:activeTab', value: string | undefined): void;
(event: 'update:search', value: string): void; (event: 'update:search', value: string): void;
}>(); }>();
if (props.enableTabs) { if (props.enableTabs) {
const internalActiveTab = ref(props.activeTab); useTabsProvider();
watch(
() => props.activeTab,
(activeTab) => {
internalActiveTab.value = activeTab;
},
);
useTabsProvider({
activeTab: computed({
get: () => internalActiveTab.value,
set: (value) => {
internalActiveTab.value = value;
emit('update:activeTab', value);
},
}),
disableUrlHashMode: computed(() => props.disableTabUrlHashMode || false),
});
} }
</script> </script>

View file

@ -1,38 +1,40 @@
<template> <template><span /></template>
<div v-if="$slots.default" v-show="isActive" :aria-hidden="!isActive">
<slot />
</div>
</template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue'; import { onMounted } from 'vue';
import type { RouteLocationRaw } from 'vue-router';
import type { IconNames } from '~/components/atomic/Icon.vue'; import type { IconNames } from '~/components/atomic/Icon.vue';
import { useTabsClient, type Tab } from '~/compositions/useTabs'; import { useTabsClient } from '~/compositions/useTabs';
const props = defineProps<{ const props = defineProps<{
id?: string; to: RouteLocationRaw;
title: string; title: string;
icon?: IconNames; icon?: IconNames;
iconClass?: string; iconClass?: string;
matchChildren?: boolean;
}>(); }>();
const { tabs, activeTab } = useTabsClient(); const { tabs } = useTabsClient();
const tab = ref<Tab>();
// TODO: find a better way to compare routes like
// https://github.com/vuejs/router/blob/0eaaeb9697acd40ad524d913d0348748e9797acb/packages/router/src/utils/index.ts#L17
function isSameRoute(a: RouteLocationRaw, b: RouteLocationRaw): boolean {
return JSON.stringify(a) === JSON.stringify(b);
}
onMounted(() => { onMounted(() => {
tab.value = { // don't add tab if tab id is already present
id: props.id || props.title.toLocaleLowerCase().replace(' ', '-') || tabs.value.length.toString(), if (tabs.value.find(({ to }) => isSameRoute(to, props.to))) {
return;
}
tabs.value.push({
to: props.to,
title: props.title, title: props.title,
icon: props.icon, icon: props.icon,
iconClass: props.iconClass, iconClass: props.iconClass,
}; matchChildren: props.matchChildren,
});
// don't add tab if tab id is already present
if (!tabs.value.find(({ id }) => id === props.id)) {
tabs.value.push(tab.value);
}
}); });
const isActive = computed(() => tab.value && tab.value.id === activeTab.value);
</script> </script>

View file

@ -1,46 +1,27 @@
<template> <template>
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<button <router-link
v-for="tab in tabs" v-for="tab in tabs"
:key="tab.id" :key="tab.title"
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" v-slot="{ isActive, isExactActive }"
:class="{ :to="tab.to"
'border-wp-text-100': activeTab === tab.id, class="border-transparent 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"
'border-transparent': activeTab !== tab.id, :active-class="tab.matchChildren ? '!border-wp-text-100' : ''"
}" :exact-active-class="tab.matchChildren ? '' : '!border-wp-text-100'"
type="button"
@click="selectTab(tab)"
> >
<Icon v-if="activeTab === tab.id" name="chevron-right" class="md:hidden" /> <Icon v-if="isExactActive || (isActive && tab.matchChildren)" name="chevron-right" class="md:hidden" />
<Icon v-else name="blank" class="md:hidden" /> <Icon v-else name="blank" class="md:hidden" />
<span class="flex gap-2 items-center flex-row-reverse md:flex-row"> <span class="flex gap-2 items-center flex-row-reverse md:flex-row">
<Icon v-if="tab.icon" :name="tab.icon" :class="tab.iconClass" /> <Icon v-if="tab.icon" :name="tab.icon" :class="tab.iconClass" />
<span>{{ tab.title }}</span> <span>{{ tab.title }}</span>
</span> </span>
</button> </router-link>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import Icon from '~/components/atomic/Icon.vue'; import Icon from '~/components/atomic/Icon.vue';
import { useTabsClient, type Tab } from '~/compositions/useTabs'; import { useTabsClient } from '~/compositions/useTabs';
const router = useRouter(); const { tabs } = useTabsClient();
const route = useRoute();
const { activeTab, tabs, disableUrlHashMode } = useTabsClient();
async function selectTab(tab: Tab) {
if (tab.id === undefined) {
return;
}
activeTab.value = tab.id;
if (!disableUrlHashMode.value) {
await router.replace({ params: route.params, hash: `#${tab.id}` });
}
}
</script> </script>

View file

@ -1,12 +1,17 @@
import type { InjectionKey, Ref } from 'vue'; import type { InjectionKey, Ref } from 'vue';
import { inject as vueInject, provide as vueProvide } from 'vue'; import { inject as vueInject, provide as vueProvide } from 'vue';
import type { Org, OrgPermissions, Repo } from '~/lib/api/types'; import type { Org, OrgPermissions, Pipeline, PipelineConfig, Repo } from '~/lib/api/types';
import type { Tab } from './useTabs';
export interface InjectKeys { export interface InjectKeys {
repo: Ref<Repo>; repo: Ref<Repo>;
org: Ref<Org | undefined>; org: Ref<Org | undefined>;
'org-permissions': Ref<OrgPermissions | undefined>; 'org-permissions': Ref<OrgPermissions | undefined>;
pipeline: Ref<Pipeline | undefined>;
'pipeline-configs': Ref<PipelineConfig[] | undefined>;
tabs: Ref<Tab[]>;
} }
export function inject<T extends keyof InjectKeys>(key: T): InjectKeys[T] { export function inject<T extends keyof InjectKeys>(key: T): InjectKeys[T] {

View file

@ -1,49 +1,24 @@
import { inject, onMounted, provide, ref, type Ref } from 'vue'; import { ref } from 'vue';
import { useRoute } from 'vue-router'; import type { RouteLocationRaw } from 'vue-router';
import type { IconNames } from '~/components/atomic/Icon.vue'; import type { IconNames } from '~/components/atomic/Icon.vue';
import { inject, provide } from './useInjectProvide';
export interface Tab { export interface Tab {
id: string; to: RouteLocationRaw;
title: string; title: string;
icon?: IconNames; icon?: IconNames;
iconClass?: string; iconClass?: string;
matchChildren?: boolean;
} }
export function useTabsProvider({ export function useTabsProvider() {
activeTab,
disableUrlHashMode,
}: {
activeTab: Ref<string | undefined>;
disableUrlHashMode: Ref<boolean>;
}) {
const route = useRoute();
const tabs = ref<Tab[]>([]); const tabs = ref<Tab[]>([]);
provide('tabs', tabs); provide('tabs', tabs);
provide('disable-url-hash-mode', disableUrlHashMode);
provide('active-tab', activeTab);
onMounted(() => {
if (activeTab.value !== undefined) {
return;
}
const hashTab = route.hash.replace(/^#/, '');
activeTab.value = hashTab || tabs.value[0].id;
});
} }
export function useTabsClient() { export function useTabsClient() {
const tabs = inject<Ref<Tab[]>>('tabs'); const tabs = inject('tabs');
const disableUrlHashMode = inject<Ref<boolean>>('disable-url-hash-mode'); return { tabs };
const activeTab = inject<Ref<string>>('active-tab');
if (activeTab === undefined || tabs === undefined || disableUrlHashMode === undefined) {
throw new Error('Please use this "useTabsClient" composition inside a component running "useTabsProvider".');
}
return { activeTab, tabs, disableUrlHashMode };
} }

View file

@ -42,30 +42,39 @@ const routes: RouteRecordRaw[] = [
}, },
{ {
path: 'branches', path: 'branches',
meta: { repoHeader: true },
children: [
{
path: '',
name: 'repo-branches', name: 'repo-branches',
component: (): Component => import('~/views/repo/RepoBranches.vue'), component: (): Component => import('~/views/repo/RepoBranches.vue'),
meta: { repoHeader: true },
}, },
{ {
path: 'branches/:branch', path: ':branch',
name: 'repo-branch', name: 'repo-branch',
component: (): Component => import('~/views/repo/RepoBranch.vue'), component: (): Component => import('~/views/repo/RepoBranch.vue'),
meta: { repoHeader: true },
props: (route) => ({ branch: route.params.branch }), props: (route) => ({ branch: route.params.branch }),
}, },
],
},
{ {
path: 'pull-requests', path: 'pull-requests',
meta: { repoHeader: true },
children: [
{
path: '',
name: 'repo-pull-requests', name: 'repo-pull-requests',
component: (): Component => import('~/views/repo/RepoPullRequests.vue'), component: (): Component => import('~/views/repo/RepoPullRequests.vue'),
meta: { repoHeader: true },
}, },
{ {
path: 'pull-requests/:pullRequest', path: ':pullRequest',
name: 'repo-pull-request', name: 'repo-pull-request',
component: (): Component => import('~/views/repo/RepoPullRequest.vue'), component: (): Component => import('~/views/repo/RepoPullRequest.vue'),
meta: { repoHeader: true },
props: (route) => ({ pullRequest: route.params.pullRequest }), props: (route) => ({ pullRequest: route.params.pullRequest }),
}, },
],
},
{ {
path: 'pipeline/:pipelineId', path: 'pipeline/:pipelineId',
component: (): Component => import('~/views/repo/pipeline/PipelineWrapper.vue'), component: (): Component => import('~/views/repo/pipeline/PipelineWrapper.vue'),
@ -98,15 +107,53 @@ const routes: RouteRecordRaw[] = [
path: 'debug', path: 'debug',
name: 'repo-pipeline-debug', name: 'repo-pipeline-debug',
component: (): Component => import('~/views/repo/pipeline/PipelineDebug.vue'), component: (): Component => import('~/views/repo/pipeline/PipelineDebug.vue'),
props: true,
}, },
], ],
}, },
{ {
path: 'settings', path: 'settings',
name: 'repo-settings', component: (): Component => import('~/views/repo/settings/RepoSettings.vue'),
component: (): Component => import('~/views/repo/RepoSettings.vue'),
meta: { authentication: 'required' }, meta: { authentication: 'required' },
props: true, props: true,
children: [
{
path: '',
name: 'repo-settings',
component: (): Component => import('~/views/repo/settings/General.vue'),
props: true,
},
{
path: 'secrets',
name: 'repo-settings-secrets',
component: (): Component => import('~/views/repo/settings/Secrets.vue'),
props: true,
},
{
path: 'registries',
name: 'repo-settings-registries',
component: (): Component => import('~/views/repo/settings/Registries.vue'),
props: true,
},
{
path: 'crons',
name: 'repo-settings-crons',
component: (): Component => import('~/views/repo/settings/Crons.vue'),
props: true,
},
{
path: 'badge',
name: 'repo-settings-badge',
component: (): Component => import('~/views/repo/settings/Badge.vue'),
props: true,
},
{
path: 'actions',
name: 'repo-settings-actions',
component: (): Component => import('~/views/repo/settings/Actions.vue'),
props: true,
},
],
}, },
{ {
path: 'manual', path: 'manual',
@ -137,9 +184,29 @@ const routes: RouteRecordRaw[] = [
{ {
path: 'settings', path: 'settings',
name: 'org-settings', name: 'org-settings',
component: (): Component => import('~/views/org/OrgSettings.vue'), component: (): Component => import('~/views/org/settings/OrgSettingsWrapper.vue'),
meta: { authentication: 'required' }, meta: { authentication: 'required' },
props: true, props: true,
children: [
{
path: 'secrets',
name: 'org-settings-secrets',
component: (): Component => import('~/views/org/settings/OrgSecrets.vue'),
props: true,
},
{
path: 'registries',
name: 'org-settings-registries',
component: (): Component => import('~/views/org/settings/OrgRegistries.vue'),
props: true,
},
{
path: 'agents',
name: 'org-settings-agents',
component: (): Component => import('~/views/org/settings/OrgAgents.vue'),
props: true,
},
],
}, },
], ],
}, },
@ -150,18 +217,98 @@ const routes: RouteRecordRaw[] = [
}, },
{ {
path: `${rootPath}/admin`, path: `${rootPath}/admin`,
name: 'admin-settings', component: (): Component => import('~/views/admin/AdminSettingsWrapper.vue'),
component: (): Component => import('~/views/admin/AdminSettings.vue'),
props: true, props: true,
meta: { authentication: 'required' }, meta: { authentication: 'required' },
children: [
{
path: '',
name: 'admin-settings',
component: (): Component => import('~/views/admin/AdminInfo.vue'),
props: true,
},
{
path: 'secrets',
name: 'admin-settings-secrets',
component: (): Component => import('~/views/admin/AdminSecrets.vue'),
props: true,
},
{
path: 'registries',
name: 'admin-settings-registries',
component: (): Component => import('~/views/admin/AdminRegistries.vue'),
props: true,
},
{
path: 'repos',
name: 'admin-settings-repos',
component: (): Component => import('~/views/admin/AdminRepos.vue'),
props: true,
},
{
path: 'users',
name: 'admin-settings-users',
component: (): Component => import('~/views/admin/AdminUsers.vue'),
props: true,
},
{
path: 'orgs',
name: 'admin-settings-orgs',
component: (): Component => import('~/views/admin/AdminOrgs.vue'),
props: true,
},
{
path: 'agents',
name: 'admin-settings-agents',
component: (): Component => import('~/views/admin/AdminAgents.vue'),
props: true,
},
{
path: 'queue',
name: 'admin-settings-queue',
component: (): Component => import('~/views/admin/AdminQueue.vue'),
props: true,
},
],
}, },
{ {
path: `${rootPath}/user`, path: `${rootPath}/user`,
name: 'user', component: (): Component => import('~/views/user/UserWrapper.vue'),
component: (): Component => import('~/views/User.vue'),
meta: { authentication: 'required' }, meta: { authentication: 'required' },
props: true, props: true,
children: [
{
path: '',
name: 'user',
component: (): Component => import('~/views/user/UserGeneral.vue'),
props: true,
},
{
path: 'secrets',
name: 'user-secrets',
component: (): Component => import('~/views/user/UserSecrets.vue'),
props: true,
},
{
path: 'registries',
name: 'user-registries',
component: (): Component => import('~/views/user/UserRegistries.vue'),
props: true,
},
{
path: 'cli-and-api',
name: 'user-cli-and-api',
component: (): Component => import('~/views/user/UserCLIAndAPI.vue'),
props: true,
},
{
path: 'agents',
name: 'user-agents',
component: (): Component => import('~/views/user/UserAgents.vue'),
props: true,
},
],
}, },
{ {
path: `${rootPath}/login`, path: `${rootPath}/login`,

View file

@ -1,35 +0,0 @@
<template>
<Scaffold enable-tabs>
<template #title>{{ $t('user.settings.settings') }}</template>
<template #headerActions><Button :text="$t('logout')" :to="`${address}/logout`" /></template>
<Tab id="general" :title="$t('user.settings.general.general')">
<UserGeneralTab />
</Tab>
<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>
<Tab v-if="useConfig().userRegisteredAgents" id="agents" :title="$t('admin.settings.agents.agents')">
<UserAgentsTab />
</Tab>
</Scaffold>
</template>
<script lang="ts" setup>
import Button from '~/components/atomic/Button.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue';
import UserAgentsTab from '~/components/user/UserAgentsTab.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';
const address = `${window.location.protocol}//${window.location.host}${useConfig().rootPath}`; // port is included in location.host
</script>

View file

@ -78,6 +78,7 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'; import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import AdminQueueStats from '~/components/admin/settings/queue/AdminQueueStats.vue';
import Badge from '~/components/atomic/Badge.vue'; import Badge from '~/components/atomic/Badge.vue';
import Button from '~/components/atomic/Button.vue'; import Button from '~/components/atomic/Button.vue';
import Icon from '~/components/atomic/Icon.vue'; import Icon from '~/components/atomic/Icon.vue';
@ -87,8 +88,6 @@ import useApiClient from '~/compositions/useApiClient';
import useNotifications from '~/compositions/useNotifications'; import useNotifications from '~/compositions/useNotifications';
import type { QueueInfo } from '~/lib/api/types'; import type { QueueInfo } from '~/lib/api/types';
import AdminQueueStats from './queue/AdminQueueStats.vue';
const apiClient = useApiClient(); const apiClient = useApiClient();
const notifications = useNotifications(); const notifications = useNotifications();
const { t } = useI18n(); const { t } = useI18n();

View file

@ -1,67 +0,0 @@
<template>
<Scaffold enable-tabs>
<template #title>
{{ $t('settings') }}
</template>
<Tab id="info" :title="$t('info')">
<AdminInfoTab />
</Tab>
<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>
<Tab id="users" :title="$t('admin.settings.users.users')">
<AdminUsersTab />
</Tab>
<Tab id="orgs" :title="$t('admin.settings.orgs.orgs')">
<AdminOrgsTab />
</Tab>
<Tab id="agents" :title="$t('admin.settings.agents.agents')">
<AdminAgentsTab />
</Tab>
<Tab id="queue" :title="$t('admin.settings.queue.queue')">
<AdminQueueTab />
</Tab>
</Scaffold>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
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';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue';
import useAuthentication from '~/compositions/useAuthentication';
import useNotifications from '~/compositions/useNotifications';
const notifications = useNotifications();
const router = useRouter();
const i18n = useI18n();
const { user } = useAuthentication();
onMounted(async () => {
if (!user?.admin) {
notifications.notify({ type: 'error', title: i18n.t('admin.settings.not_allowed') });
await router.replace({ name: 'home' });
}
if (!user?.admin) {
notifications.notify({ type: 'error', title: i18n.t('admin.settings.not_allowed') });
await router.replace({ name: 'home' });
}
});
</script>

View file

@ -0,0 +1,40 @@
<template>
<Scaffold enable-tabs>
<template #title>
{{ $t('settings') }}
</template>
<Tab :to="{ name: 'admin-settings' }" :title="$t('info')" />
<Tab :to="{ name: 'admin-settings-secrets' }" :title="$t('secrets.secrets')" />
<Tab :to="{ name: 'admin-settings-registries' }" :title="$t('registries.registries')" />
<Tab :to="{ name: 'admin-settings-repos' }" :title="$t('admin.settings.repos.repos')" />
<Tab :to="{ name: 'admin-settings-users' }" :title="$t('admin.settings.users.users')" />
<Tab :to="{ name: 'admin-settings-orgs' }" :title="$t('admin.settings.orgs.orgs')" />
<Tab :to="{ name: 'admin-settings-agents' }" :title="$t('admin.settings.agents.agents')" />
<Tab :to="{ name: 'admin-settings-queue' }" :title="$t('admin.settings.queue.queue')" />
<router-view />
</Scaffold>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue';
import useAuthentication from '~/compositions/useAuthentication';
import useNotifications from '~/compositions/useNotifications';
const notifications = useNotifications();
const router = useRouter();
const i18n = useI18n();
const { user } = useAuthentication();
onMounted(async () => {
if (!user?.admin) {
notifications.notify({ type: 'error', title: i18n.t('admin.settings.not_allowed') });
await router.replace({ name: 'home' });
}
});
</script>

View file

@ -8,7 +8,7 @@
<IconButton <IconButton
v-if="orgPermissions.admin" v-if="orgPermissions.admin"
icon="settings" icon="settings"
:to="{ name: org.is_user ? 'user' : 'org-settings' }" :to="{ name: org.is_user ? 'user' : 'org-settings-secrets' }"
:title="$t('settings')" :title="$t('settings')"
/> />
</template> </template>

View file

@ -7,7 +7,7 @@
<template #headerActions> <template #headerActions>
<IconButton <IconButton
v-if="orgPermissions.admin" v-if="orgPermissions.admin"
:to="{ name: org.is_user ? 'user' : 'repo-settings' }" :to="{ name: org.is_user ? 'user' : 'org-settings-secrets' }"
:title="$t('settings')" :title="$t('settings')"
icon="settings" icon="settings"
/> />

View file

@ -11,17 +11,15 @@
</span> </span>
</template> </template>
<Tab id="secrets" :title="$t('secrets.secrets')"> <Tab :to="{ name: 'org-settings-secrets' }" :title="$t('secrets.secrets')" />
<OrgSecretsTab /> <Tab :to="{ name: 'org-settings-registries' }" :title="$t('registries.registries')" />
</Tab> <Tab
v-if="useConfig().userRegisteredAgents"
:to="{ name: 'org-settings-agents' }"
:title="$t('admin.settings.agents.agents')"
/>
<Tab id="registries" :title="$t('registries.registries')"> <router-view />
<OrgRegistriesTab />
</Tab>
<Tab v-if="useConfig().userRegisteredAgents" id="agents" :title="$t('admin.settings.agents.agents')">
<OrgAgentsTab />
</Tab>
</Scaffold> </Scaffold>
</template> </template>
@ -32,9 +30,6 @@ import { useRouter } from 'vue-router';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue'; 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 OrgAgentsTab from '~/components/org/settings/OrgAgentsTab.vue';
import OrgRegistriesTab from '~/components/org/settings/OrgRegistriesTab.vue';
import OrgSecretsTab from '~/components/org/settings/OrgSecretsTab.vue';
import useConfig from '~/compositions/useConfig'; import useConfig from '~/compositions/useConfig';
import { inject } from '~/compositions/useInjectProvide'; import { inject } from '~/compositions/useInjectProvide';
import useNotifications from '~/compositions/useNotifications'; import useNotifications from '~/compositions/useNotifications';

View file

@ -1,10 +1,5 @@
<template> <template>
<Scaffold <Scaffold v-if="repo && repoPermissions && route.meta.repoHeader" enable-tabs>
v-if="repo && repoPermissions && route.meta.repoHeader"
v-model:active-tab="activeTab"
enable-tabs
disable-tab-url-hash-mode
>
<template #title> <template #title>
<span class="flex"> <span class="flex">
<router-link :to="{ name: 'org', params: { orgId: repo.org_id } }" class="hover:underline">{{ <router-link :to="{ name: 'org', params: { orgId: repo.org_id } }" class="hover:underline">{{
@ -43,9 +38,14 @@
/> />
</template> </template>
<Tab id="activity" :title="$t('repo.activity')" /> <Tab :to="{ name: 'repo' }" :title="$t('repo.activity')" />
<Tab id="branches" :title="$t('repo.branches')" /> <Tab :to="{ name: 'repo-branches' }" match-children :title="$t('repo.branches')" />
<Tab v-if="repo.pr_enabled && repo.allow_pr" id="pull_requests" :title="$t('repo.pull_requests')" /> <Tab
v-if="repo.pr_enabled && repo.allow_pr"
:to="{ name: 'repo-pull-requests' }"
match-children
:title="$t('repo.pull_requests')"
/>
<router-view /> <router-view />
</Scaffold> </Scaffold>
@ -132,25 +132,4 @@ watch([repositoryId], () => {
}); });
const badgeUrl = computed(() => repo.value && `${config.rootPath}/api/badges/${repo.value.id}/status.svg`); const badgeUrl = computed(() => repo.value && `${config.rootPath}/api/badges/${repo.value.id}/status.svg`);
const activeTab = computed({
get() {
if (route.name === 'repo-branches' || route.name === 'repo-branch') {
return 'branches';
}
if (route.name === 'repo-pull-requests' || route.name === 'repo-pull-request') {
return 'pull_requests';
}
return 'activity';
},
set(tab: string) {
if (tab === 'branches') {
router.push({ name: 'repo-branches' });
} else if (tab === 'pull_requests') {
router.push({ name: 'repo-pull-requests' });
} else {
router.push({ name: 'repo' });
}
},
});
</script> </script>

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="flex flex-col gap-y-6"> <div class="flex flex-col gap-y-6">
<Panel <Panel
v-for="pipelineConfig in pipelineConfigsDecoded || []" v-for="pipelineConfig in pipelineConfigsDecoded"
:key="pipelineConfig.hash" :key="pipelineConfig.hash"
:collapsable="pipelineConfigsDecoded && pipelineConfigsDecoded.length > 1" :collapsable="pipelineConfigsDecoded && pipelineConfigsDecoded.length > 1"
collapsed-by-default collapsed-by-default
@ -14,21 +14,22 @@
<script lang="ts" setup> <script lang="ts" setup>
import { decode } from 'js-base64'; import { decode } from 'js-base64';
import { computed, inject, type Ref } from 'vue'; import { computed } from 'vue';
import SyntaxHighlight from '~/components/atomic/SyntaxHighlight'; import SyntaxHighlight from '~/components/atomic/SyntaxHighlight';
import Panel from '~/components/layout/Panel.vue'; import Panel from '~/components/layout/Panel.vue';
import type { PipelineConfig } from '~/lib/api/types'; import { inject } from '~/compositions/useInjectProvide';
const pipelineConfigs = inject<Ref<PipelineConfig[]>>('pipeline-configs'); const pipelineConfigs = inject('pipeline-configs');
if (!pipelineConfigs) { if (!pipelineConfigs) {
throw new Error('Unexpected: "pipelineConfigs" should be provided at this place'); throw new Error('Unexpected: "pipelineConfigs" should be provided at this place');
} }
const pipelineConfigsDecoded = computed(() => const pipelineConfigsDecoded = computed(
pipelineConfigs.value.map((i) => ({ () =>
pipelineConfigs.value?.map((i) => ({
...i, ...i,
data: decode(i.data), data: decode(i.data),
})), })) ?? [],
); );
</script> </script>

View file

@ -1,11 +1,9 @@
<template> <template>
<Scaffold <Scaffold
v-if="pipeline && repo" v-if="pipeline && repo"
v-model:active-tab="activeTab"
enable-tabs enable-tabs
disable-tab-url-hash-mode
:go-back="goBack" :go-back="goBack"
:fluid-content="activeTab === 'tasks'" :fluid-content="route.name === 'repo-pipeline'"
full-width-header full-width-header
> >
<template #title> <template #title>
@ -75,10 +73,10 @@
</div> </div>
</template> </template>
<Tab id="tasks" :title="$t('repo.pipeline.tasks')" /> <Tab :to="{ name: 'repo-pipeline' }" :title="$t('repo.pipeline.tasks')" />
<Tab <Tab
v-if="pipeline.errors && pipeline.errors.length > 0" v-if="pipeline.errors && pipeline.errors.length > 0"
id="errors" :to="{ name: 'repo-pipeline-errors' }"
icon="attention" icon="attention"
:title=" :title="
pipeline.errors.some((e) => !e.is_warning) pipeline.errors.some((e) => !e.is_warning)
@ -87,20 +85,24 @@
" "
:icon-class="pipeline.errors.some((e) => !e.is_warning) ? 'text-wp-state-error-100' : 'text-wp-state-warn-100'" :icon-class="pipeline.errors.some((e) => !e.is_warning) ? 'text-wp-state-error-100' : 'text-wp-state-warn-100'"
/> />
<Tab id="config" :title="$t('repo.pipeline.config')" /> <Tab :to="{ name: 'repo-pipeline-config' }" :title="$t('repo.pipeline.config')" />
<Tab <Tab
v-if="pipeline.changed_files && pipeline.changed_files.length > 0" v-if="pipeline.changed_files && pipeline.changed_files.length > 0"
id="changed-files" :to="{ name: 'repo-pipeline-changed-files' }"
:title="$t('repo.pipeline.files', { files: pipeline.changed_files?.length })" :title="$t('repo.pipeline.files', { files: pipeline.changed_files?.length })"
/> />
<Tab v-if="repoPermissions && repoPermissions.push" id="debug" :title="$t('repo.pipeline.debug.title')" /> <Tab
v-if="repoPermissions && repoPermissions.push"
:to="{ name: 'repo-pipeline-debug' }"
:title="$t('repo.pipeline.debug.title')"
/>
<router-view /> <router-view />
</Scaffold> </Scaffold>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onBeforeUnmount, onMounted, provide, ref, toRef, watch, type Ref } from 'vue'; import { computed, inject, onBeforeUnmount, onMounted, ref, toRef, watch, type Ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
@ -113,6 +115,7 @@ import PipelineStatusIcon from '~/components/repo/pipeline/PipelineStatusIcon.vu
import useApiClient from '~/compositions/useApiClient'; import useApiClient from '~/compositions/useApiClient';
import { useAsyncAction } from '~/compositions/useAsyncAction'; import { useAsyncAction } from '~/compositions/useAsyncAction';
import { useFavicon } from '~/compositions/useFavicon'; import { useFavicon } from '~/compositions/useFavicon';
import { provide } from '~/compositions/useInjectProvide';
import useNotifications from '~/compositions/useNotifications'; import useNotifications from '~/compositions/useNotifications';
import usePipeline from '~/compositions/usePipeline'; import usePipeline from '~/compositions/usePipeline';
import { useRouteBack } from '~/compositions/useRouteBack'; import { useRouteBack } from '~/compositions/useRouteBack';
@ -206,48 +209,5 @@ onBeforeUnmount(() => {
favicon.updateStatus('default'); favicon.updateStatus('default');
}); });
const activeTab = computed({
get() {
if (route.name === 'repo-pipeline-changed-files') {
return 'changed-files';
}
if (route.name === 'repo-pipeline-config') {
return 'config';
}
if (route.name === 'repo-pipeline-errors') {
return 'errors';
}
if (route.name === 'repo-pipeline-debug' && repoPermissions.value?.push) {
return 'debug';
}
return 'tasks';
},
set(tab: string) {
if (tab === 'tasks') {
router.replace({ name: 'repo-pipeline' });
}
if (tab === 'changed-files') {
router.replace({ name: 'repo-pipeline-changed-files' });
}
if (tab === 'config') {
router.replace({ name: 'repo-pipeline-config' });
}
if (tab === 'errors') {
router.replace({ name: 'repo-pipeline-errors' });
}
if (tab === 'debug' && repoPermissions.value?.push) {
router.replace({ name: 'repo-pipeline-debug' });
}
},
});
const goBack = useRouteBack({ name: 'repo' }); const goBack = useRouteBack({ name: 'repo' });
</script> </script>

View file

@ -16,24 +16,14 @@
</span> </span>
</template> </template>
<Tab id="general" :title="$t('repo.settings.general.general')"> <Tab :to="{ name: 'repo-settings' }" :title="$t('repo.settings.general.general')" />
<GeneralTab /> <Tab :to="{ name: 'repo-settings-secrets' }" :title="$t('secrets.secrets')" />
</Tab> <Tab :to="{ name: 'repo-settings-registries' }" :title="$t('registries.registries')" />
<Tab id="secrets" :title="$t('secrets.secrets')"> <Tab :to="{ name: 'repo-settings-crons' }" :title="$t('repo.settings.crons.crons')" />
<SecretsTab /> <Tab :to="{ name: 'repo-settings-badge' }" :title="$t('repo.settings.badge.badge')" />
</Tab> <Tab :to="{ name: 'repo-settings-actions' }" :title="$t('repo.settings.actions.actions')" />
<Tab id="registries" :title="$t('registries.registries')">
<RegistriesTab /> <router-view />
</Tab>
<Tab id="crons" :title="$t('repo.settings.crons.crons')">
<CronTab />
</Tab>
<Tab id="badge" :title="$t('repo.settings.badge.badge')">
<BadgeTab />
</Tab>
<Tab id="actions" :title="$t('repo.settings.actions.actions')">
<ActionsTab />
</Tab>
</Scaffold> </Scaffold>
</template> </template>
@ -44,12 +34,6 @@ import { useRouter } from 'vue-router';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue'; 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 ActionsTab from '~/components/repo/settings/ActionsTab.vue';
import BadgeTab from '~/components/repo/settings/BadgeTab.vue';
import CronTab from '~/components/repo/settings/CronTab.vue';
import GeneralTab from '~/components/repo/settings/GeneralTab.vue';
import RegistriesTab from '~/components/repo/settings/RegistriesTab.vue';
import SecretsTab from '~/components/repo/settings/SecretsTab.vue';
import useNotifications from '~/compositions/useNotifications'; import useNotifications from '~/compositions/useNotifications';
import { useRouteBack } from '~/compositions/useRouteBack'; import { useRouteBack } from '~/compositions/useRouteBack';
import type { Repo, RepoPermissions } from '~/lib/api/types'; import type { Repo, RepoPermissions } from '~/lib/api/types';

View file

@ -0,0 +1,27 @@
<template>
<Scaffold enable-tabs>
<template #title>{{ $t('user.settings.settings') }}</template>
<template #headerActions><Button :text="$t('logout')" :to="`${address}/logout`" /></template>
<Tab :to="{ name: 'user' }" :title="$t('user.settings.general.general')" />
<Tab :to="{ name: 'user-secrets' }" :title="$t('secrets.secrets')" />
<Tab :to="{ name: 'user-registries' }" :title="$t('registries.registries')" />
<Tab :to="{ name: 'user-cli-and-api' }" :title="$t('user.settings.cli_and_api.cli_and_api')" />
<Tab
v-if="useConfig().userRegisteredAgents"
:to="{ name: 'user-agents' }"
:title="$t('admin.settings.agents.agents')"
/>
<router-view />
</Scaffold>
</template>
<script lang="ts" setup>
import Button from '~/components/atomic/Button.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
import Tab from '~/components/layout/scaffold/Tab.vue';
import useConfig from '~/compositions/useConfig';
const address = `${window.location.protocol}//${window.location.host}${useConfig().rootPath}`; // port is included in location.host
</script>