mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-22 09:51:01 +00:00
Header and Tabs UI Improvements (#1290)
Some improvements to the Page Header and Tab UI. Original | New :--------:|:-------: ![image](https://user-images.githubusercontent.com/62170586/197360886-046f1016-ca39-4b69-8134-99ba88e3a0c2.png) | ![image](https://user-images.githubusercontent.com/62170586/197360819-7efd0d82-1412-465d-aefa-039164f97465.png) ![image](https://user-images.githubusercontent.com/62170586/197360872-f2ece5fd-7c0b-4e2c-8629-31524a412af5.png) | ![image](https://user-images.githubusercontent.com/62170586/197360830-49f09e0d-619e-4fa9-8e38-8d05d9404185.png) ![image](https://user-images.githubusercontent.com/62170586/197281776-e3de6441-9417-4614-8b25-1aaef0b8da61.png) | ![image](https://user-images.githubusercontent.com/62170586/197281698-40c66d34-76f3-4fd5-97e3-1c422b74844c.png) ![image](https://user-images.githubusercontent.com/62170586/196609248-ff150c6e-2995-4bcc-8573-49ffaf388446.png) | ![image](https://user-images.githubusercontent.com/62170586/197323734-7c1a1b79-0f41-4bf2-96a3-dd38df9e1415.png) ![image](https://user-images.githubusercontent.com/62170586/196609329-b7a6f37e-e8c2-4004-a98b-73f837122ff8.png) | ![image](https://user-images.githubusercontent.com/62170586/197323882-10141ffd-7411-4493-8291-b8000adc3cc5.png) What? - Create a new Scaffold component, which includes the header and tabs required for a page. - Use this component to wrap all the views that have a header. - Ensures consistency in headers between different pages. - [x] Add support to use custom html/component in place of title (for repo page, pipeline page, etc) - [x] Add support of right icon buttons (for repo page, pipeline page, etc) - [x] Refactor tabs handling using compositions (useTabsProvider, useTabsClient) - [x] Make new header ui resposive
This commit is contained in:
parent
8b7ca464cf
commit
e2ab8a46ed
22 changed files with 469 additions and 355 deletions
6
web/components.d.ts
vendored
6
web/components.d.ts
vendored
|
@ -18,6 +18,7 @@ declare module '@vue/runtime-core' {
|
|||
DocsLink: typeof import('./src/components/atomic/DocsLink.vue')['default']
|
||||
FluidContainer: typeof import('./src/components/layout/FluidContainer.vue')['default']
|
||||
GeneralTab: typeof import('./src/components/repo/settings/GeneralTab.vue')['default']
|
||||
Header: typeof import('./src/components/layout/scaffold/Header.vue')['default']
|
||||
IBxBxPowerOff: typeof import('~icons/bx/bx-power-off')['default']
|
||||
ICarbonCloseOutline: typeof import('~icons/carbon/close-outline')['default']
|
||||
IClarityDeployLine: typeof import('~icons/clarity/deploy-line')['default']
|
||||
|
@ -84,12 +85,13 @@ declare module '@vue/runtime-core' {
|
|||
RegistriesTab: typeof import('./src/components/repo/settings/RegistriesTab.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Scaffold: typeof import('./src/components/layout/scaffold/Scaffold.vue')['default']
|
||||
SecretEdit: typeof import('./src/components/secrets/SecretEdit.vue')['default']
|
||||
SecretList: typeof import('./src/components/secrets/SecretList.vue')['default']
|
||||
SecretsTab: typeof import('./src/components/repo/settings/SecretsTab.vue')['default']
|
||||
SelectField: typeof import('./src/components/form/SelectField.vue')['default']
|
||||
Tab: typeof import('./src/components/tabs/Tab.vue')['default']
|
||||
Tabs: typeof import('./src/components/tabs/Tabs.vue')['default']
|
||||
Tab: typeof import('./src/components/layout/scaffold/Tab.vue')['default']
|
||||
Tabs: typeof import('./src/components/layout/scaffold/Tabs.vue')['default']
|
||||
TextField: typeof import('./src/components/form/TextField.vue')['default']
|
||||
Warning: typeof import('./src/components/atomic/Warning.vue')['default']
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
type="button"
|
||||
class="relative flex items-center py-1 px-2 rounded-md border shadow-sm cursor-pointer transition-all duration-150 focus:outline-none overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:class="{
|
||||
'bg-white hover:bg-gray-200 border-gray-300 text-color dark:bg-dark-gray-700 dark:border-dark-400 dark:hover:bg-dark-gray-800':
|
||||
'bg-white hover:bg-gray-200 border-gray-300 text-color dark:bg-dark-gray-600 dark:border-dark-400 dark:hover:bg-dark-gray-800':
|
||||
color === 'gray',
|
||||
'bg-lime-600 hover:bg-lime-700 border-lime-800 text-white dark:text-gray-400 dark:bg-lime-900 dark:hover:bg-lime-800':
|
||||
color === 'green',
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
v-if="lines === 1"
|
||||
v-model="innerValue"
|
||||
class="w-full bg-transparent text-color focus:outline-none focus:border-blue-400"
|
||||
:class="inputClass"
|
||||
:disabled="disabled"
|
||||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
|
@ -15,6 +16,7 @@
|
|||
v-else
|
||||
v-model="innerValue"
|
||||
class="w-full bg-transparent text-color focus:outline-none focus:border-blue-400"
|
||||
:class="inputClass"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
:rows="lines"
|
||||
|
@ -52,6 +54,11 @@ export default defineComponent({
|
|||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
inputClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
emits: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<!-- Navbar -->
|
||||
<div class="flex shadow-lg dark:shadow-sm bg-lime-600 text-neutral-content p-4 dark:bg-dark-gray-900">
|
||||
<div class="flex bg-lime-600 text-neutral-content p-4 dark:bg-dark-gray-800 dark:border-b dark:border-gray-700">
|
||||
<!-- Left Links Box -->
|
||||
<div class="flex text-white dark:text-gray-400 items-center space-x-2">
|
||||
<!-- Logo -->
|
||||
|
|
64
web/src/components/layout/scaffold/Header.vue
Normal file
64
web/src/components/layout/scaffold/Header.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div class="bg-white dark:bg-dark-gray-900 border-b dark:border-gray-700">
|
||||
<FluidContainer class="!py-0">
|
||||
<div class="flex flex-wrap items-center justify-between py-4 <md:flex-row <md:gap-y-4">
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-start <md:w-full <md:justify-center"
|
||||
:class="{
|
||||
'md:flex-1': searchBoxPresent,
|
||||
}"
|
||||
>
|
||||
<IconButton v-if="goBack" icon="back" :title="$t('back')" class="mr-2 <md:hidden" @click="goBack" />
|
||||
<h1 class="flex flex-wrap text-xl text-color items-center gap-x-2">
|
||||
<slot name="title" />
|
||||
</h1>
|
||||
</div>
|
||||
<TextField
|
||||
v-if="searchBoxPresent"
|
||||
class="w-auto !bg-gray-100 !dark:bg-dark-gray-600 <md:w-full <md:order-3"
|
||||
input-class="!placeholder-gray-500"
|
||||
:placeholder="$t('search')"
|
||||
:model-value="search"
|
||||
@update:model-value="(value: string) => $emit('update:search', value)"
|
||||
/>
|
||||
<div
|
||||
v-if="$slots.titleActions"
|
||||
class="flex flex-wrap items-center justify-end gap-x-2 <md:w-full <md:justify-center"
|
||||
:class="{
|
||||
'md:flex-1': searchBoxPresent,
|
||||
}"
|
||||
>
|
||||
<slot name="titleActions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="enableTabs" class="flex flex-wrap justify-between">
|
||||
<Tabs class="<md:order-2" />
|
||||
<div
|
||||
v-if="$slots.titleActions"
|
||||
class="flex items-center justify-end gap-x-2 md:mb-2 <md:w-full <md:justify-center <md:order-1"
|
||||
>
|
||||
<slot name="tabActions" />
|
||||
</div>
|
||||
</div>
|
||||
</FluidContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TextField from '~/components/form/TextField.vue';
|
||||
import FluidContainer from '~/components/layout/FluidContainer.vue';
|
||||
|
||||
import Tabs from './Tabs.vue';
|
||||
|
||||
export interface Props {
|
||||
goBack?: () => void;
|
||||
enableTabs?: boolean;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
defineEmits(['update:search']);
|
||||
|
||||
const searchBoxPresent = props.search !== undefined;
|
||||
</script>
|
56
web/src/components/layout/scaffold/Scaffold.vue
Normal file
56
web/src/components/layout/scaffold/Scaffold.vue
Normal file
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<Header
|
||||
:go-back="goBack"
|
||||
:enable-tabs="enableTabs"
|
||||
:search="search"
|
||||
@update:search="(value) => $emit('update:search', value)"
|
||||
>
|
||||
<template #title><slot name="title" /></template>
|
||||
<template v-if="$slots.titleActions" #titleActions><slot name="titleActions" /></template>
|
||||
<template v-if="$slots.tabActions" #tabActions><slot name="tabActions" /></template>
|
||||
</Header>
|
||||
|
||||
<FluidContainer>
|
||||
<slot />
|
||||
</FluidContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { toRef } from 'vue';
|
||||
|
||||
import FluidContainer from '~/components/layout/FluidContainer.vue';
|
||||
import { useTabsProvider } from '~/compositions/useTabs';
|
||||
|
||||
import Header from './Header.vue';
|
||||
|
||||
export interface Props {
|
||||
// Header
|
||||
goBack?: () => void;
|
||||
search?: string;
|
||||
|
||||
// Tabs
|
||||
enableTabs?: boolean;
|
||||
disableHashMode?: boolean;
|
||||
activeTab: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
goBack: undefined,
|
||||
search: undefined,
|
||||
// eslint-disable-next-line vue/no-boolean-default
|
||||
disableHashMode: false,
|
||||
// eslint-disable-next-line vue/no-boolean-default
|
||||
enableTabs: false,
|
||||
activeTab: '',
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:activeTab', 'update:search']);
|
||||
|
||||
if (props.enableTabs) {
|
||||
useTabsProvider({
|
||||
activeTabProp: toRef(props, 'activeTab'),
|
||||
disableHashMode: toRef(props, 'disableHashMode'),
|
||||
updateActiveTabProp: (value) => emit('update:activeTab', value),
|
||||
});
|
||||
}
|
||||
</script>
|
31
web/src/components/layout/scaffold/Tab.vue
Normal file
31
web/src/components/layout/scaffold/Tab.vue
Normal file
|
@ -0,0 +1,31 @@
|
|||
<template>
|
||||
<div v-if="$slots.default" v-show="isActive" :aria-hidden="!isActive">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { Tab, useTabsClient } from '~/compositions/useTabs';
|
||||
|
||||
export interface Props {
|
||||
id?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const { tabs, activeTab } = useTabsClient();
|
||||
const tab = ref<Tab>();
|
||||
|
||||
onMounted(() => {
|
||||
tab.value = {
|
||||
id: props.id || props.title.toLocaleLowerCase().replace(' ', '-') || tabs.value.length.toString(),
|
||||
title: props.title,
|
||||
};
|
||||
tabs.value.push(tab.value);
|
||||
});
|
||||
|
||||
const isActive = computed(() => tab.value && tab.value.id === activeTab.value);
|
||||
</script>
|
41
web/src/components/layout/scaffold/Tabs.vue
Normal file
41
web/src/components/layout/scaffold/Tabs.vue
Normal file
|
@ -0,0 +1,41 @@
|
|||
<template>
|
||||
<div class="flex flex-wrap">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
class="w-full py-2 md:w-auto md:py-2 md:px-8 flex cursor-pointer md:border-b-2 text-color hover:text-gray-700 dark:hover:text-gray-400 items-center"
|
||||
:class="{
|
||||
'border-gray-400 dark:border-gray-600': activeTab === tab.id,
|
||||
'border-transparent': activeTab !== tab.id,
|
||||
}"
|
||||
type="button"
|
||||
@click="selectTab(tab)"
|
||||
>
|
||||
<Icon v-if="activeTab === tab.id" name="chevron-right" class="md:hidden" />
|
||||
<Icon v-else name="blank" class="md:hidden" />
|
||||
<span>{{ tab.title }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { Tab, useTabsClient } from '~/compositions/useTabs';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const { activeTab, tabs, disableHashMode } = useTabsClient();
|
||||
|
||||
async function selectTab(tab: Tab) {
|
||||
if (tab.id === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeTab.value = tab.id;
|
||||
if (!disableHashMode.value) {
|
||||
await router.replace({ params: route.params, hash: `#${tab.id}` });
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,49 +0,0 @@
|
|||
<template>
|
||||
<div v-if="$slots.default" v-show="isActive" :aria-hidden="!isActive" class="mt-4">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, inject, onMounted, Ref, ref } from 'vue';
|
||||
|
||||
import { Tab } from './types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Tab',
|
||||
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const activeTab = inject<Ref<string>>('active-tab');
|
||||
const tabs = inject<Ref<Tab[]>>('tabs');
|
||||
if (activeTab === undefined || tabs === undefined) {
|
||||
throw new Error('Please wrap this "Tab"-component inside a "Tabs" list.');
|
||||
}
|
||||
|
||||
const tab = ref<Tab>();
|
||||
|
||||
onMounted(() => {
|
||||
tab.value = {
|
||||
id: props.id || props.title.toLocaleLowerCase().replace(' ', '-') || tabs.value.length.toString(),
|
||||
title: props.title,
|
||||
};
|
||||
tabs.value.push(tab.value);
|
||||
});
|
||||
|
||||
const isActive = computed(() => tab.value && tab.value.id === activeTab.value);
|
||||
|
||||
return { isActive };
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -1,91 +0,0 @@
|
|||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex w-full md:pt-4 flex-wrap">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
class="w-full py-2 md:w-auto md:pt-0 md:pb-2 md:px-8 flex cursor-pointer md:border-b-2 text-color hover:text-gray-700 dark:hover:text-gray-400 items-center"
|
||||
:class="{
|
||||
'border-gray-400 dark:border-gray-600': activeTab === tab.id,
|
||||
'border-transparent': activeTab !== tab.id,
|
||||
}"
|
||||
type="button"
|
||||
@click="selectTab(tab)"
|
||||
>
|
||||
<Icon v-if="activeTab === tab.id" name="chevron-right" class="md:hidden" />
|
||||
<Icon v-else name="blank" class="md:hidden" />
|
||||
<span>{{ tab.title }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, provide, ref, toRef } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import Icon from '~/components/atomic/Icon.vue';
|
||||
|
||||
import { Tab } from './types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Tabs',
|
||||
components: { Icon },
|
||||
props: {
|
||||
disableHashMode: {
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
emits: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
'update:modelValue': (_value: string): boolean => true,
|
||||
},
|
||||
|
||||
setup(props, { emit }) {
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const disableHashMode = toRef(props, 'disableHashMode');
|
||||
const modelValue = toRef(props, 'modelValue');
|
||||
const tabs = ref<Tab[]>([]);
|
||||
const activeTab = ref();
|
||||
provide('tabs', tabs);
|
||||
provide(
|
||||
'active-tab',
|
||||
computed(() => activeTab.value),
|
||||
);
|
||||
async function selectTab(tab: Tab) {
|
||||
if (tab.id === undefined) {
|
||||
return;
|
||||
}
|
||||
activeTab.value = tab.id;
|
||||
emit('update:modelValue', activeTab.value);
|
||||
if (!disableHashMode.value) {
|
||||
await router.replace({ params: route.params, hash: `#${tab.id}` });
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
if (modelValue.value) {
|
||||
activeTab.value = modelValue.value;
|
||||
return;
|
||||
}
|
||||
const hashTab = route.hash.replace(/^#/, '');
|
||||
if (hashTab) {
|
||||
activeTab.value = hashTab;
|
||||
return;
|
||||
}
|
||||
activeTab.value = tabs.value[0].id;
|
||||
});
|
||||
return { tabs, activeTab, selectTab };
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -1,4 +0,0 @@
|
|||
export type Tab = {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
64
web/src/compositions/useTabs.ts
Normal file
64
web/src/compositions/useTabs.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { computed, inject, onMounted, provide, Ref, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
export type Tab = {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export function useTabsProvider({
|
||||
activeTabProp,
|
||||
disableHashMode,
|
||||
updateActiveTabProp,
|
||||
}: {
|
||||
activeTabProp: Ref<string>;
|
||||
updateActiveTabProp: (tab: string) => void;
|
||||
disableHashMode: Ref<boolean>;
|
||||
}) {
|
||||
const route = useRoute();
|
||||
|
||||
const tabs = ref<Tab[]>([]);
|
||||
const activeTab = ref<string>('');
|
||||
|
||||
provide('tabs', tabs);
|
||||
provide(
|
||||
'disable-hash-mode',
|
||||
computed(() => disableHashMode.value),
|
||||
);
|
||||
provide(
|
||||
'active-tab',
|
||||
computed({
|
||||
get: () => activeTab.value,
|
||||
set: (value) => {
|
||||
activeTab.value = value;
|
||||
updateActiveTabProp(value);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (activeTabProp.value) {
|
||||
activeTab.value = activeTabProp.value;
|
||||
return;
|
||||
}
|
||||
|
||||
const hashTab = route.hash.replace(/^#/, '');
|
||||
if (hashTab) {
|
||||
activeTab.value = hashTab;
|
||||
return;
|
||||
}
|
||||
activeTab.value = tabs.value[0].id;
|
||||
});
|
||||
}
|
||||
|
||||
export function useTabsClient() {
|
||||
const tabs = inject<Ref<Tab[]>>('tabs');
|
||||
const disableHashMode = inject<Ref<boolean>>('disable-hash-mode');
|
||||
const activeTab = inject<Ref<string>>('active-tab');
|
||||
|
||||
if (activeTab === undefined || tabs === undefined || disableHashMode === undefined) {
|
||||
throw new Error('Please use this "useTabsClient" composition inside a compoent running "useTabsProvider".');
|
||||
}
|
||||
|
||||
return { activeTab, tabs, disableHashMode };
|
||||
}
|
|
@ -1,17 +1,12 @@
|
|||
<template>
|
||||
<FluidContainer class="flex flex-col">
|
||||
<div class="flex flex-row border-b mb-4 pb-4 items-center dark:border-dark-200">
|
||||
<IconButton :to="{ name: 'repos' }" :title="$t('back')" icon="back" />
|
||||
<h1 class="text-xl ml-2 text-color">{{ $t('repo.add') }}</h1>
|
||||
<TextField v-model="search" class="w-auto ml-auto" :placeholder="$t('search')" />
|
||||
<Button
|
||||
class="ml-auto"
|
||||
start-icon="sync"
|
||||
:text="$t('repo.enable.reload')"
|
||||
:is-loading="isReloadingRepos"
|
||||
@click="reloadRepos"
|
||||
/>
|
||||
</div>
|
||||
<Scaffold v-model:search="search" :go-back="goBack">
|
||||
<template #title>
|
||||
{{ $t('repo.add') }}
|
||||
</template>
|
||||
|
||||
<template #titleActions>
|
||||
<Button start-icon="sync" :text="$t('repo.enable.reload')" :is-loading="isReloadingRepos" @click="reloadRepos" />
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<ListItem
|
||||
|
@ -32,7 +27,7 @@
|
|||
/>
|
||||
</ListItem>
|
||||
</div>
|
||||
</FluidContainer>
|
||||
</Scaffold>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -41,14 +36,13 @@ import { useI18n } from 'vue-i18n';
|
|||
import { useRouter } from 'vue-router';
|
||||
|
||||
import Button from '~/components/atomic/Button.vue';
|
||||
import IconButton from '~/components/atomic/IconButton.vue';
|
||||
import ListItem from '~/components/atomic/ListItem.vue';
|
||||
import TextField from '~/components/form/TextField.vue';
|
||||
import FluidContainer from '~/components/layout/FluidContainer.vue';
|
||||
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
|
||||
import useApiClient from '~/compositions/useApiClient';
|
||||
import { useAsyncAction } from '~/compositions/useAsyncAction';
|
||||
import useNotifications from '~/compositions/useNotifications';
|
||||
import { useRepoSearch } from '~/compositions/useRepoSearch';
|
||||
import { useRouteBackOrDefault } from '~/compositions/useRouteBackOrDefault';
|
||||
import { Repo } from '~/lib/api/types';
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -56,10 +50,8 @@ export default defineComponent({
|
|||
|
||||
components: {
|
||||
Button,
|
||||
FluidContainer,
|
||||
ListItem,
|
||||
IconButton,
|
||||
TextField,
|
||||
Scaffold,
|
||||
},
|
||||
|
||||
setup() {
|
||||
|
@ -91,10 +83,13 @@ export default defineComponent({
|
|||
await router.push({ name: 'repo', params: { repoName: repo.name, repoOwner: repo.owner } });
|
||||
});
|
||||
|
||||
const goBack = useRouteBackOrDefault({ name: 'repos' });
|
||||
|
||||
return {
|
||||
isReloadingRepos,
|
||||
isActivatingRepo,
|
||||
repoToActivate,
|
||||
goBack,
|
||||
reloadRepos,
|
||||
activateRepo,
|
||||
searchedRepos,
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
<template>
|
||||
<FluidContainer class="flex flex-col">
|
||||
<div class="flex flex-row flex-wrap md:grid md:grid-cols-3 border-b pb-4 mb-4 dark:border-dark-200">
|
||||
<h1 class="text-xl text-color">{{ $t('repositories') }}</h1>
|
||||
<TextField v-model="search" class="w-auto md:ml-auto md:mr-auto" :placeholder="$t('search')" />
|
||||
<Button class="md:ml-auto" :to="{ name: 'repo-add' }" start-icon="plus" :text="$t('repo.add')" />
|
||||
</div>
|
||||
<Scaffold v-model:search="search">
|
||||
<template #title>
|
||||
{{ $t('repositories') }}
|
||||
</template>
|
||||
|
||||
<template #titleActions>
|
||||
<Button :to="{ name: 'repo-add' }" start-icon="plus" :text="$t('repo.add')" />
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<ListItem
|
||||
|
@ -16,7 +18,7 @@
|
|||
<span class="text-color">{{ `${repo.owner} / ${repo.name}` }}</span>
|
||||
</ListItem>
|
||||
</div>
|
||||
</FluidContainer>
|
||||
</Scaffold>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -24,8 +26,7 @@ import { computed, defineComponent, onMounted, ref } from 'vue';
|
|||
|
||||
import Button from '~/components/atomic/Button.vue';
|
||||
import ListItem from '~/components/atomic/ListItem.vue';
|
||||
import TextField from '~/components/form/TextField.vue';
|
||||
import FluidContainer from '~/components/layout/FluidContainer.vue';
|
||||
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
|
||||
import { useRepoSearch } from '~/compositions/useRepoSearch';
|
||||
import RepoStore from '~/store/repos';
|
||||
|
||||
|
@ -34,9 +35,8 @@ export default defineComponent({
|
|||
|
||||
components: {
|
||||
Button,
|
||||
FluidContainer,
|
||||
ListItem,
|
||||
TextField,
|
||||
Scaffold,
|
||||
},
|
||||
|
||||
setup() {
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
<template>
|
||||
<FluidContainer class="flex flex-col">
|
||||
<div class="flex flex-row flex-wrap md:grid md:grid-cols-3 border-b pb-4 mb-4 dark:border-dark-200">
|
||||
<h1 class="text-xl text-color">{{ repoOwner }}</h1>
|
||||
<TextField v-model="search" class="w-auto md:ml-auto md:mr-auto" :placeholder="$t('search')" />
|
||||
<Scaffold v-model:search="search">
|
||||
<template #title>
|
||||
{{ repoOwner }}
|
||||
</template>
|
||||
|
||||
<template #titleActions>
|
||||
<IconButton
|
||||
v-if="orgPermissions.admin"
|
||||
icon="settings"
|
||||
:to="{ name: 'org-settings' }"
|
||||
:title="$t('org.settings.settings')"
|
||||
class="ml-auto"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-4">
|
||||
<ListItem
|
||||
|
@ -25,7 +26,7 @@
|
|||
<div v-if="(searchedRepos || []).length <= 0" class="text-center">
|
||||
<span class="text-color m-auto">{{ $t('repo.user_none') }}</span>
|
||||
</div>
|
||||
</FluidContainer>
|
||||
</Scaffold>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -33,8 +34,7 @@ import { computed, defineComponent, onMounted, ref } from 'vue';
|
|||
|
||||
import IconButton from '~/components/atomic/IconButton.vue';
|
||||
import ListItem from '~/components/atomic/ListItem.vue';
|
||||
import TextField from '~/components/form/TextField.vue';
|
||||
import FluidContainer from '~/components/layout/FluidContainer.vue';
|
||||
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
|
||||
import useApiClient from '~/compositions/useApiClient';
|
||||
import { useRepoSearch } from '~/compositions/useRepoSearch';
|
||||
import { OrgPermissions } from '~/lib/api/types';
|
||||
|
@ -44,10 +44,9 @@ export default defineComponent({
|
|||
name: 'ReposOwner',
|
||||
|
||||
components: {
|
||||
FluidContainer,
|
||||
ListItem,
|
||||
TextField,
|
||||
IconButton,
|
||||
Scaffold,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
|
@ -1,32 +1,34 @@
|
|||
<template>
|
||||
<FluidContainer class="space-y-4 flex flex-col my-0">
|
||||
<Button class="ml-auto" :text="$t('logout')" :to="`${address}/logout`" />
|
||||
<Scaffold>
|
||||
<template #title>{{ $t('user.settings') }}</template>
|
||||
<template #titleActions><Button :text="$t('logout')" :to="`${address}/logout`" /></template>
|
||||
<div class="space-y-4 flex flex-col">
|
||||
<SelectField v-model="selectedLocale" :options="localeOptions" />
|
||||
|
||||
<SelectField v-model="selectedLocale" :options="localeOptions" />
|
||||
|
||||
<div>
|
||||
<h2 class="text-lg text-color">{{ $t('user.token') }}</h2>
|
||||
<pre class="cli-box">{{ token }}</pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-lg text-color">{{ $t('user.shell_setup') }}</h2>
|
||||
<pre class="cli-box">{{ usageWithShell }}</pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-lg text-color">{{ $t('user.api_usage') }}</h2>
|
||||
<pre class="cli-box">{{ usageWithCurl }}</pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<h2 class="text-lg text-color">{{ $t('user.cli_usage') }}</h2>
|
||||
<a :href="cliDownload" target="_blank" class="ml-4 text-link">{{ $t('user.dl_cli') }}</a>
|
||||
<div>
|
||||
<h2 class="text-lg text-color">{{ $t('user.token') }}</h2>
|
||||
<pre class="cli-box">{{ token }}</pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-lg text-color">{{ $t('user.shell_setup') }}</h2>
|
||||
<pre class="cli-box">{{ usageWithShell }}</pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="text-lg text-color">{{ $t('user.api_usage') }}</h2>
|
||||
<pre class="cli-box">{{ usageWithCurl }}</pre>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<h2 class="text-lg text-color">{{ $t('user.cli_usage') }}</h2>
|
||||
<a :href="cliDownload" target="_blank" class="ml-4 text-link">{{ $t('user.dl_cli') }}</a>
|
||||
</div>
|
||||
<pre class="cli-box">{{ usageWithCli }}</pre>
|
||||
</div>
|
||||
<pre class="cli-box">{{ usageWithCli }}</pre>
|
||||
</div>
|
||||
</FluidContainer>
|
||||
</Scaffold>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -38,7 +40,7 @@ import { useI18n } from 'vue-i18n';
|
|||
|
||||
import Button from '~/components/atomic/Button.vue';
|
||||
import SelectField from '~/components/form/SelectField.vue';
|
||||
import FluidContainer from '~/components/layout/FluidContainer.vue';
|
||||
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
|
||||
import useApiClient from '~/compositions/useApiClient';
|
||||
|
||||
const { t, availableLocales, locale } = useI18n();
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
<template>
|
||||
<FluidContainer>
|
||||
<div class="flex border-b items-center pb-4 mb-4 dark:border-gray-600">
|
||||
<IconButton icon="back" :title="$t('back')" @click="goBack" />
|
||||
<h1 class="text-xl ml-2 text-color">{{ $t('admin.settings.settings') }}</h1>
|
||||
</div>
|
||||
|
||||
<Tabs>
|
||||
<Tab id="secrets" :title="$t('admin.settings.secrets.secrets')">
|
||||
<AdminSecretsTab />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</FluidContainer>
|
||||
<Scaffold enable-tabs>
|
||||
<template #title>
|
||||
{{ $t('repo.settings.settings') }}
|
||||
</template>
|
||||
<Tab id="secrets" :title="$t('admin.settings.secrets.secrets')">
|
||||
<AdminSecretsTab />
|
||||
</Tab>
|
||||
</Scaffold>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -19,23 +15,18 @@ import { useI18n } from 'vue-i18n';
|
|||
import { useRouter } from 'vue-router';
|
||||
|
||||
import AdminSecretsTab from '~/components/admin/settings/AdminSecretsTab.vue';
|
||||
import IconButton from '~/components/atomic/IconButton.vue';
|
||||
import FluidContainer from '~/components/layout/FluidContainer.vue';
|
||||
import Tab from '~/components/tabs/Tab.vue';
|
||||
import Tabs from '~/components/tabs/Tabs.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';
|
||||
import { useRouteBackOrDefault } from '~/compositions/useRouteBackOrDefault';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AdminSettings',
|
||||
|
||||
components: {
|
||||
FluidContainer,
|
||||
IconButton,
|
||||
Tabs,
|
||||
Tab,
|
||||
AdminSecretsTab,
|
||||
Scaffold,
|
||||
},
|
||||
|
||||
setup() {
|
||||
|
@ -50,10 +41,6 @@ export default defineComponent({
|
|||
await router.replace({ name: 'home' });
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
goBack: useRouteBackOrDefault({ name: 'home' }),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
<template>
|
||||
<FluidContainer>
|
||||
<div class="flex border-b items-center pb-4 mb-4 dark:border-gray-600">
|
||||
<IconButton icon="back" :title="$t('back')" @click="goBack" />
|
||||
<h1 class="text-xl ml-2 text-color">{{ $t('org.settings.settings') }}</h1>
|
||||
</div>
|
||||
<Scaffold enable-tabs :go-back="goBack">
|
||||
<template #title>
|
||||
<span>
|
||||
<router-link :to="{ name: 'repos-owner', params: { repoOwner: org.name } }" class="hover:underline">
|
||||
{{ org.name }}
|
||||
</router-link>
|
||||
/
|
||||
{{ $t('org.settings.settings') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<Tabs>
|
||||
<Tab id="secrets" :title="$t('org.settings.secrets.secrets')">
|
||||
<OrgSecretsTab />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</FluidContainer>
|
||||
<Tab id="secrets" :title="$t('org.settings.secrets.secrets')">
|
||||
<OrgSecretsTab />
|
||||
</Tab>
|
||||
</Scaffold>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -18,22 +21,16 @@ import { defineComponent, inject, onMounted, Ref } from 'vue';
|
|||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import IconButton from '~/components/atomic/IconButton.vue';
|
||||
import FluidContainer from '~/components/layout/FluidContainer.vue';
|
||||
import Tab from '~/components/layout/scaffold/Tab.vue';
|
||||
import OrgSecretsTab from '~/components/org/settings/OrgSecretsTab.vue';
|
||||
import Tab from '~/components/tabs/Tab.vue';
|
||||
import Tabs from '~/components/tabs/Tabs.vue';
|
||||
import useNotifications from '~/compositions/useNotifications';
|
||||
import { useRouteBackOrDefault } from '~/compositions/useRouteBackOrDefault';
|
||||
import { OrgPermissions } from '~/lib/api/types';
|
||||
import { Org, OrgPermissions } from '~/lib/api/types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OrgSettings',
|
||||
|
||||
components: {
|
||||
FluidContainer,
|
||||
IconButton,
|
||||
Tabs,
|
||||
Tab,
|
||||
OrgSecretsTab,
|
||||
},
|
||||
|
@ -48,6 +45,11 @@ export default defineComponent({
|
|||
throw new Error('Unexpected: "orgPermissions" should be provided at this place');
|
||||
}
|
||||
|
||||
const org = inject<Ref<Org>>('org');
|
||||
if (!org) {
|
||||
throw new Error('Unexpected: "org" should be provided at this place');
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!orgPermissions.value.admin) {
|
||||
notifications.notify({ type: 'error', title: i18n.t('org.settings.not_allowed') });
|
||||
|
@ -56,6 +58,7 @@ export default defineComponent({
|
|||
});
|
||||
|
||||
return {
|
||||
org,
|
||||
goBack: useRouteBackOrDefault({ name: 'repos-owner' }),
|
||||
};
|
||||
},
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
<template>
|
||||
<FluidContainer v-if="org && orgPermissions && $route.meta.orgHeader">
|
||||
<div class="flex flex-wrap border-b items-center pb-4 mb-4 dark:border-gray-600 justify-center">
|
||||
<h1 class="text-xl text-color w-full md:w-auto text-center mb-4 md:mb-0">
|
||||
{{ org.name }}
|
||||
</h1>
|
||||
<Scaffold v-if="org && orgPermissions && $route.meta.orgHeader">
|
||||
<template #title>
|
||||
{{ org.name }}
|
||||
</template>
|
||||
|
||||
<template #titleActions>
|
||||
<IconButton
|
||||
v-if="orgPermissions.admin"
|
||||
class="ml-2"
|
||||
:to="{ name: 'repo-settings' }"
|
||||
:title="$t('org.settings.settings')"
|
||||
icon="settings"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<router-view />
|
||||
</FluidContainer>
|
||||
</Scaffold>
|
||||
<router-view v-else-if="org && orgPermissions" />
|
||||
</template>
|
||||
|
||||
|
@ -22,14 +22,14 @@
|
|||
import { computed, defineComponent, onMounted, provide, ref, toRef, watch } from 'vue';
|
||||
|
||||
import IconButton from '~/components/atomic/IconButton.vue';
|
||||
import FluidContainer from '~/components/layout/FluidContainer.vue';
|
||||
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
|
||||
import useApiClient from '~/compositions/useApiClient';
|
||||
import { Org, OrgPermissions } from '~/lib/api/types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OrgWrapper',
|
||||
|
||||
components: { FluidContainer, IconButton },
|
||||
components: { IconButton, Scaffold },
|
||||
|
||||
props: {
|
||||
repoOwner: {
|
||||
|
|
|
@ -1,31 +1,41 @@
|
|||
<template>
|
||||
<FluidContainer>
|
||||
<div class="flex border-b items-center pb-4 mb-4 dark:border-gray-600">
|
||||
<IconButton icon="back" :title="$t('back')" @click="goBack" />
|
||||
<h1 class="text-xl ml-2 text-color">{{ $t('repo.settings.settings') }}</h1>
|
||||
</div>
|
||||
<Scaffold enable-tabs :go-back="goBack">
|
||||
<template #title>
|
||||
<span>
|
||||
<router-link :to="{ name: 'repos-owner', params: { repoOwner: repo.owner } }" class="hover:underline">
|
||||
{{ repo.owner }}
|
||||
</router-link>
|
||||
/
|
||||
<router-link
|
||||
:to="{ name: 'repo', params: { repoOwner: repo.owner, repoName: repo.name } }"
|
||||
class="hover:underline"
|
||||
>
|
||||
{{ repo.name }}
|
||||
</router-link>
|
||||
/
|
||||
{{ $t('repo.settings.settings') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<Tabs>
|
||||
<Tab id="general" :title="$t('repo.settings.general.general')">
|
||||
<GeneralTab />
|
||||
</Tab>
|
||||
<Tab id="secrets" :title="$t('repo.settings.secrets.secrets')">
|
||||
<SecretsTab />
|
||||
</Tab>
|
||||
<Tab id="registries" :title="$t('repo.settings.registries.registries')">
|
||||
<RegistriesTab />
|
||||
</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>
|
||||
</Tabs>
|
||||
</FluidContainer>
|
||||
<Tab id="general" :title="$t('repo.settings.general.general')">
|
||||
<GeneralTab />
|
||||
</Tab>
|
||||
<Tab id="secrets" :title="$t('repo.settings.secrets.secrets')">
|
||||
<SecretsTab />
|
||||
</Tab>
|
||||
<Tab id="registries" :title="$t('repo.settings.registries.registries')">
|
||||
<RegistriesTab />
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -33,19 +43,17 @@ import { inject, onMounted, Ref } from 'vue';
|
|||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import IconButton from '~/components/atomic/IconButton.vue';
|
||||
import FluidContainer from '~/components/layout/FluidContainer.vue';
|
||||
import Scaffold from '~/components/layout/scaffold/Scaffold.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 Tab from '~/components/tabs/Tab.vue';
|
||||
import Tabs from '~/components/tabs/Tabs.vue';
|
||||
import useNotifications from '~/compositions/useNotifications';
|
||||
import { useRouteBackOrDefault } from '~/compositions/useRouteBackOrDefault';
|
||||
import { RepoPermissions } from '~/lib/api/types';
|
||||
import { Repo, RepoPermissions } from '~/lib/api/types';
|
||||
|
||||
const notifications = useNotifications();
|
||||
const router = useRouter();
|
||||
|
@ -56,6 +64,11 @@ if (!repoPermissions) {
|
|||
throw new Error('Unexpected: "repoPermissions" should be provided at this place');
|
||||
}
|
||||
|
||||
const repo = inject<Ref<Repo>>('repo');
|
||||
if (!repo) {
|
||||
throw new Error('Unexpected: "repo" should be provided at this place');
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!repoPermissions.value.admin) {
|
||||
notifications.notify({ type: 'error', title: i18n.t('repo.settings.not_allowed') });
|
||||
|
|
|
@ -1,19 +1,26 @@
|
|||
<template>
|
||||
<FluidContainer v-if="repo && repoPermissions && $route.meta.repoHeader">
|
||||
<div class="flex flex-wrap border-b items-center pb-4 mb-4 dark:border-gray-600 justify-center">
|
||||
<h1 class="text-xl text-color w-full md:w-auto text-center mb-4 md:mb-0">
|
||||
<Scaffold
|
||||
v-if="repo && repoPermissions && $route.meta.repoHeader"
|
||||
v-model:activeTab="activeTab"
|
||||
enable-tabs
|
||||
disable-hash-mode
|
||||
>
|
||||
<template #title>
|
||||
<span class="flex">
|
||||
<router-link :to="{ name: 'repos-owner', params: { repoOwner } }" class="hover:underline">{{
|
||||
repoOwner
|
||||
}}</router-link>
|
||||
{{ ` / ${repo.name}` }}
|
||||
</h1>
|
||||
<a v-if="badgeUrl" :href="badgeUrl" target="_blank" class="md:ml-auto">
|
||||
{{ ` / ${repo.name}` }}
|
||||
</span>
|
||||
</template>
|
||||
<template #titleActions>
|
||||
<a v-if="badgeUrl" :href="badgeUrl" target="_blank" class="ml-2">
|
||||
<img :src="badgeUrl" />
|
||||
</a>
|
||||
<a
|
||||
:href="repo.link_url"
|
||||
target="_blank"
|
||||
class="flex ml-4 p-1 rounded-full text-color hover:bg-gray-200 hover:text-gray-700 dark:hover:bg-gray-600"
|
||||
class="flex p-1 rounded-full text-color hover:bg-gray-200 hover:text-gray-700 dark:hover:bg-gray-600"
|
||||
>
|
||||
<Icon v-if="forge === 'github'" name="github" />
|
||||
<Icon v-else-if="forge === 'gitea'" name="gitea" />
|
||||
|
@ -23,28 +30,26 @@
|
|||
</a>
|
||||
<IconButton
|
||||
v-if="repoPermissions.admin"
|
||||
class="ml-2"
|
||||
:to="{ name: 'repo-settings' }"
|
||||
:title="$t('repo.settings.settings')"
|
||||
icon="settings"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-y-2 items-center justify-between">
|
||||
<Tabs v-model="activeTab" disable-hash-mode class="mb-4">
|
||||
<Tab id="activity" :title="$t('repo.activity')" />
|
||||
<Tab id="branches" :title="$t('repo.branches')" />
|
||||
</Tabs>
|
||||
</template>
|
||||
|
||||
<template #tabActions>
|
||||
<Button
|
||||
v-if="repoPermissions.push"
|
||||
:text="$t('repo.manual_pipeline.trigger')"
|
||||
class="ml-auto"
|
||||
@click="showManualPipelinePopup = true"
|
||||
/>
|
||||
<ManualPipelinePopup :open="showManualPipelinePopup" @close="showManualPipelinePopup = false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Tab id="activity" :title="$t('repo.activity')" />
|
||||
<Tab id="branches" :title="$t('repo.branches')" />
|
||||
|
||||
<router-view />
|
||||
</FluidContainer>
|
||||
</Scaffold>
|
||||
<router-view v-else-if="repo && repoPermissions" />
|
||||
</template>
|
||||
|
||||
|
@ -55,10 +60,9 @@ import { useRoute, useRouter } from 'vue-router';
|
|||
|
||||
import Icon from '~/components/atomic/Icon.vue';
|
||||
import IconButton from '~/components/atomic/IconButton.vue';
|
||||
import FluidContainer from '~/components/layout/FluidContainer.vue';
|
||||
import ManualPipelinePopup from '~/components/layout/popups/ManualPipelinePopup.vue';
|
||||
import Tab from '~/components/tabs/Tab.vue';
|
||||
import Tabs from '~/components/tabs/Tabs.vue';
|
||||
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
|
||||
import Tab from '~/components/layout/scaffold/Tab.vue';
|
||||
import useApiClient from '~/compositions/useApiClient';
|
||||
import useAuthentication from '~/compositions/useAuthentication';
|
||||
import useConfig from '~/compositions/useConfig';
|
||||
|
|
|
@ -1,48 +1,34 @@
|
|||
<template>
|
||||
<template v-if="pipeline && repo">
|
||||
<FluidContainer class="flex flex-col min-w-0 dark:border-gray-600">
|
||||
<div class="flex mb-2 items-center <md:flex-wrap">
|
||||
<IconButton icon="back" :title="$t('back')" class="flex-shrink-0" @click="goBack" />
|
||||
<Scaffold v-model:activeTab="activeTab" enable-tabs disable-hash-mode :go-back="goBack">
|
||||
<template #title>
|
||||
<span class="w-full md:w-auto text-center">{{ $t('repo.pipeline.pipeline', { pipelineId }) }}</span>
|
||||
<span class="<md:hidden">-</span>
|
||||
<span class="w-full md:w-auto text-center truncate">{{ message }}</span>
|
||||
</template>
|
||||
|
||||
<h1
|
||||
class="order-3 w-full <md:flex-wrap md:order-none md:w-auto md:ml-2 flex text-center text-xl text-color whitespace-nowrap overflow-hidden overflow-ellipsis"
|
||||
>
|
||||
<span class="w-full md:w-auto text-center">{{ $t('repo.pipeline.pipeline', { pipelineId }) }}</span>
|
||||
<span class="<md:hidden mx-2">-</span>
|
||||
<span class="w-full md:w-auto text-center truncate">{{ message }}</span>
|
||||
</h1>
|
||||
|
||||
<PipelineStatusIcon :pipeline="pipeline" class="flex flex-shrink-0 ml-auto" />
|
||||
<template #titleActions>
|
||||
<PipelineStatusIcon :pipeline="pipeline" class="flex flex-shrink-0" />
|
||||
|
||||
<template v-if="repoPermissions.push">
|
||||
<Button
|
||||
v-if="pipeline.status === 'pending' || pipeline.status === 'running'"
|
||||
class="ml-4 flex-shrink-0"
|
||||
class="flex-shrink-0"
|
||||
:text="$t('repo.pipeline.actions.cancel')"
|
||||
:is-loading="isCancelingPipeline"
|
||||
@click="cancelPipeline"
|
||||
/>
|
||||
<Button
|
||||
v-else-if="pipeline.status !== 'blocked' && pipeline.status !== 'declined'"
|
||||
class="ml-4 flex-shrink-0"
|
||||
class="flex-shrink-0"
|
||||
:text="$t('repo.pipeline.actions.restart')"
|
||||
:is-loading="isRestartingPipeline"
|
||||
@click="restartPipeline"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-y-2 items-center justify-between">
|
||||
<Tabs v-model="activeTab" disable-hash-mode class="order-2 md:order-none">
|
||||
<Tab id="tasks" :title="$t('repo.pipeline.tasks')" />
|
||||
<Tab id="config" :title="$t('repo.pipeline.config')" />
|
||||
<Tab
|
||||
v-if="pipeline.event === 'push' || pipeline.event === 'pull_request'"
|
||||
id="changed-files"
|
||||
:title="$t('repo.pipeline.files', { files: pipeline.changed_files?.length || 0 })"
|
||||
/>
|
||||
</Tabs>
|
||||
</template>
|
||||
|
||||
<template #tabActions>
|
||||
<div class="flex justify-between gap-x-4 text-color flex-shrink-0 pb-2 md:p-0 mx-auto md:mr-0">
|
||||
<div class="flex space-x-1 items-center flex-shrink-0">
|
||||
<Icon name="since" />
|
||||
|
@ -58,8 +44,16 @@
|
|||
<span>{{ duration }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FluidContainer>
|
||||
</template>
|
||||
|
||||
<Tab id="tasks" :title="$t('repo.pipeline.tasks')" />
|
||||
<Tab id="config" :title="$t('repo.pipeline.config')" />
|
||||
<Tab
|
||||
v-if="pipeline.event === 'push' || pipeline.event === 'pull_request'"
|
||||
id="changed-files"
|
||||
:title="$t('repo.pipeline.files', { files: pipeline.changed_files?.length || 0 })"
|
||||
/>
|
||||
</Scaffold>
|
||||
|
||||
<router-view />
|
||||
</template>
|
||||
|
@ -72,11 +66,9 @@ import { useI18n } from 'vue-i18n';
|
|||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import Button from '~/components/atomic/Button.vue';
|
||||
import IconButton from '~/components/atomic/IconButton.vue';
|
||||
import FluidContainer from '~/components/layout/FluidContainer.vue';
|
||||
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
|
||||
import Tab from '~/components/layout/scaffold/Tab.vue';
|
||||
import PipelineStatusIcon from '~/components/repo/pipeline/PipelineStatusIcon.vue';
|
||||
import Tab from '~/components/tabs/Tab.vue';
|
||||
import Tabs from '~/components/tabs/Tabs.vue';
|
||||
import useApiClient from '~/compositions/useApiClient';
|
||||
import { useAsyncAction } from '~/compositions/useAsyncAction';
|
||||
import { useFavicon } from '~/compositions/useFavicon';
|
||||
|
@ -90,13 +82,11 @@ export default defineComponent({
|
|||
name: 'PipelineWrapper',
|
||||
|
||||
components: {
|
||||
FluidContainer,
|
||||
Button,
|
||||
PipelineStatusIcon,
|
||||
IconButton,
|
||||
Tabs,
|
||||
Tab,
|
||||
Tooltip,
|
||||
Scaffold,
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
Loading…
Reference in a new issue