UI cleanups and improvements (#2548)

This commit is contained in:
Anbraten 2023-10-08 17:49:13 +02:00 committed by GitHub
parent 5bad63556a
commit 284fb99194
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 106 additions and 157 deletions

1
web/components.d.ts vendored
View file

@ -21,6 +21,7 @@ declare module 'vue' {
Button: typeof import('./src/components/atomic/Button.vue')['default'] Button: typeof import('./src/components/atomic/Button.vue')['default']
Checkbox: typeof import('./src/components/form/Checkbox.vue')['default'] Checkbox: typeof import('./src/components/form/Checkbox.vue')['default']
CheckboxesField: typeof import('./src/components/form/CheckboxesField.vue')['default'] CheckboxesField: typeof import('./src/components/form/CheckboxesField.vue')['default']
Container: typeof import('./src/components/layout/Container.vue')['default']
CronTab: typeof import('./src/components/repo/settings/CronTab.vue')['default'] CronTab: typeof import('./src/components/repo/settings/CronTab.vue')['default']
DeployPipelinePopup: typeof import('./src/components/layout/popups/DeployPipelinePopup.vue')['default'] DeployPipelinePopup: typeof import('./src/components/layout/popups/DeployPipelinePopup.vue')['default']
DocsLink: typeof import('./src/components/atomic/DocsLink.vue')['default'] DocsLink: typeof import('./src/components/atomic/DocsLink.vue')['default']

View file

@ -489,5 +489,6 @@
"oauth_error": "Error while authenticating against OAuth provider", "oauth_error": "Error while authenticating against OAuth provider",
"internal_error": "Some internal error occurred", "internal_error": "Some internal error occurred",
"access_denied": "You are not allowed to login" "access_denied": "You are not allowed to login"
} },
"default": "default"
} }

View file

@ -1,6 +1,6 @@
<template> <template>
<component <component
:is="to === null ? 'button' : httpLink ? 'a' : 'router-link'" :is="to === undefined ? 'button' : httpLink ? 'a' : 'router-link'"
v-bind="btnAttrs" v-bind="btnAttrs"
class="relative flex items-center py-1 px-2 rounded-md border shadow-sm cursor-pointer transition-all duration-150 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed" class="relative flex items-center py-1 px-2 rounded-md border shadow-sm cursor-pointer transition-all duration-150 overflow-hidden disabled:opacity-50 disabled:cursor-not-allowed"
:class="{ :class="{
@ -19,10 +19,9 @@
<span :class="{ invisible: isLoading }">{{ text }}</span> <span :class="{ invisible: isLoading }">{{ text }}</span>
<Icon v-if="endIcon" :name="endIcon" class="ml-2 w-6 h-6" :class="{ invisible: isLoading }" /> <Icon v-if="endIcon" :name="endIcon" class="ml-2 w-6 h-6" :class="{ invisible: isLoading }" />
<div <div
v-if="isLoading"
class="absolute left-0 top-0 right-0 bottom-0 flex items-center justify-center" class="absolute left-0 top-0 right-0 bottom-0 flex items-center justify-center"
:class="{ :class="{
'opacity-100': isLoading,
'opacity-0': !isLoading,
'bg-wp-control-neutral-200': color === 'gray', 'bg-wp-control-neutral-200': color === 'gray',
'bg-wp-control-ok-200': color === 'green', 'bg-wp-control-ok-200': color === 'green',
'bg-wp-control-info-200': color === 'blue', 'bg-wp-control-info-200': color === 'blue',
@ -43,22 +42,22 @@ import Icon, { IconNames } from '~/components/atomic/Icon.vue';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
text: string; text?: string;
title?: string; title?: string;
disabled?: boolean; disabled?: boolean;
to: RouteLocationRaw | null; to?: RouteLocationRaw;
color: 'blue' | 'green' | 'red' | 'gray'; color?: 'blue' | 'green' | 'red' | 'gray';
startIcon: IconNames | null; startIcon?: IconNames;
endIcon: IconNames | null; endIcon?: IconNames;
isLoading?: boolean; isLoading?: boolean;
}>(), }>(),
{ {
text: '', text: undefined,
title: undefined, title: undefined,
to: null, to: undefined,
color: 'gray', color: 'gray',
startIcon: null, startIcon: undefined,
endIcon: null, endIcon: undefined,
}, },
); );

View file

@ -32,22 +32,14 @@ import { RouteLocationRaw } from 'vue-router';
import Icon, { IconNames } from '~/components/atomic/Icon.vue'; import Icon, { IconNames } from '~/components/atomic/Icon.vue';
withDefaults( defineProps<{
defineProps<{ icon?: IconNames;
icon: IconNames | null; disabled?: boolean;
disabled?: boolean; to?: RouteLocationRaw;
to: RouteLocationRaw | null; isLoading?: boolean;
isLoading?: boolean; title?: string;
title: string; href?: string;
href?: string; }>();
}>(),
{
icon: null,
to: null,
title: undefined,
href: '',
},
);
</script> </script>
<style scoped> <style scoped>

View file

@ -16,24 +16,17 @@ import { computed, toRef } from 'vue';
import Checkbox from './Checkbox.vue'; import Checkbox from './Checkbox.vue';
import { CheckboxOption } from './form.types'; import { CheckboxOption } from './form.types';
const props = withDefaults( const props = defineProps<{
defineProps<{ modelValue?: CheckboxOption['value'][];
modelValue: CheckboxOption['value'][]; options?: CheckboxOption[];
options: CheckboxOption[]; }>();
}>(),
{
modelValue: () => [],
options: undefined,
},
);
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'update:modelValue', value: CheckboxOption['value'][]): void; (event: 'update:modelValue', value: CheckboxOption['value'][]): void;
}>(); }>();
const modelValue = toRef(props, 'modelValue'); const modelValue = toRef(props, 'modelValue');
const innerValue = computed({ const innerValue = computed({
get: () => modelValue.value, get: () => modelValue.value || [],
set: (value) => { set: (value) => {
emit('update:modelValue', value); emit('update:modelValue', value);
}, },

View file

@ -15,17 +15,11 @@ import { computed, toRef } from 'vue';
import { SelectOption } from './form.types'; import { SelectOption } from './form.types';
const props = withDefaults( const props = defineProps<{
defineProps<{ modelValue: string;
modelValue: string; placeholder?: string;
placeholder: string; options: SelectOption[];
options: SelectOption[]; }>();
}>(),
{
placeholder: '',
options: undefined,
},
);
const emit = defineEmits<{ const emit = defineEmits<{
(event: 'update:modelValue', value: string): void; (event: 'update:modelValue', value: string): void;

View file

@ -24,10 +24,10 @@ import { computed, toRef } from 'vue';
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
modelValue: string; modelValue?: string;
placeholder: string; placeholder?: string;
type: string; type?: string;
lines: number; lines?: number;
disabled?: boolean; disabled?: boolean;
}>(), }>(),
{ {

View file

@ -5,9 +5,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
export interface Props { defineProps<{
fullWidth?: boolean; fullWidth?: boolean;
} }>();
defineProps<Props>();
</script> </script>

View file

@ -36,15 +36,10 @@ import { computed, ref } from 'vue';
import Icon from '~/components/atomic/Icon.vue'; import Icon from '~/components/atomic/Icon.vue';
const props = withDefaults( const props = defineProps<{
defineProps<{ title?: string;
title?: string; collapsable?: boolean;
collapsable?: boolean; }>();
}>(),
{
title: '',
},
);
/** /**
* _collapsed is used to store the internal state of the panel, but is * _collapsed is used to store the internal state of the panel, but is

View file

@ -3,7 +3,7 @@
class="bg-wp-background-100 border-b-1 border-wp-background-400 dark:border-wp-background-100 dark:bg-wp-background-300 text-wp-text-100" class="bg-wp-background-100 border-b-1 border-wp-background-400 dark:border-wp-background-100 dark:bg-wp-background-300 text-wp-text-100"
:class="{ 'md:px-4': fullWidth }" :class="{ 'md:px-4': fullWidth }"
> >
<FluidContainer :full-width="fullWidth" class="!py-0"> <Container :full-width="fullWidth" class="!py-0">
<div class="flex w-full md:items-center flex-col py-3 gap-2 md:gap-10 md:flex-row md:justify-between"> <div class="flex w-full md:items-center flex-col py-3 gap-2 md:gap-10 md:flex-row md:justify-between">
<div <div
class="flex items-center content-start" class="flex items-center content-start"
@ -46,13 +46,13 @@
<slot name="tabActions" /> <slot name="tabActions" />
</div> </div>
</div> </div>
</FluidContainer> </Container>
</header> </header>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import TextField from '~/components/form/TextField.vue'; import TextField from '~/components/form/TextField.vue';
import FluidContainer from '~/components/layout/FluidContainer.vue'; import Container from '~/components/layout/Container.vue';
import Tabs from './Tabs.vue'; import Tabs from './Tabs.vue';

View file

@ -3,7 +3,7 @@
:go-back="goBack" :go-back="goBack"
:enable-tabs="enableTabs" :enable-tabs="enableTabs"
:search="search" :search="search"
:full-width="fullWidth" :full-width="fullWidthHeader"
@update:search="(value) => $emit('update:search', value)" @update:search="(value) => $emit('update:search', value)"
> >
<template #title><slot name="title" /></template> <template #title><slot name="title" /></template>
@ -11,48 +11,39 @@
<template v-if="$slots.tabActions" #tabActions><slot name="tabActions" /></template> <template v-if="$slots.tabActions" #tabActions><slot name="tabActions" /></template>
</Header> </Header>
<FluidContainer v-if="fluidContent"> <slot v-if="fluidContent" />
<Container v-else>
<slot /> <slot />
</FluidContainer> </Container>
<slot v-else />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { toRef } from 'vue'; import { toRef } from 'vue';
import FluidContainer from '~/components/layout/FluidContainer.vue'; import Container from '~/components/layout/Container.vue';
import { useTabsProvider } from '~/compositions/useTabs'; import { useTabsProvider } from '~/compositions/useTabs';
import Header from './Header.vue'; import Header from './Header.vue';
export interface Props { const props = defineProps<{
// Header // Header
goBack?: () => void; goBack?: () => void;
search?: string; search?: string;
fullWidthHeader?: boolean;
// Tabs // Tabs
enableTabs?: boolean; enableTabs?: boolean;
disableHashMode?: boolean; disableHashMode?: boolean;
activeTab: string; activeTab?: string;
// Content // Content
fluidContent?: boolean; fluidContent?: boolean;
fullWidth?: boolean; }>();
}
const props = withDefaults(defineProps<Props>(), { const emit = defineEmits<{
goBack: undefined, (event: 'update:activeTab', value: string): void;
search: undefined, (event: 'update:search', value: string): void;
// eslint-disable-next-line vue/no-boolean-default }>();
disableHashMode: false,
// eslint-disable-next-line vue/no-boolean-default
enableTabs: false,
activeTab: '',
// eslint-disable-next-line vue/no-boolean-default
fluidContent: true,
});
const emit = defineEmits(['update:activeTab', 'update:search']);
if (props.enableTabs) { if (props.enableTabs) {
useTabsProvider({ useTabsProvider({

View file

@ -275,7 +275,7 @@ async function loadLogs() {
if (loadedStepSlug.value === stepSlug.value) { if (loadedStepSlug.value === stepSlug.value) {
return; return;
} }
loadedStepSlug.value = stepSlug.value;
log.value = undefined; log.value = undefined;
logBuffer.value = []; logBuffer.value = [];
ansiUp.value = new AnsiUp(); ansiUp.value = new AnsiUp();
@ -294,12 +294,12 @@ async function loadLogs() {
} }
if (isStepFinished(step.value)) { if (isStepFinished(step.value)) {
loadedStepSlug.value = stepSlug.value;
const logs = await apiClient.getLogs(repo.value.id, pipeline.value.number, step.value.id); const logs = await apiClient.getLogs(repo.value.id, pipeline.value.number, step.value.id);
logs?.forEach((line) => writeLog({ index: line.line, text: b64DecodeUnicode(line.data), time: line.time })); logs?.forEach((line) => writeLog({ index: line.line, text: b64DecodeUnicode(line.data), time: line.time }));
flushLogs(false); flushLogs(false);
} } else if (isStepRunning(step.value)) {
loadedStepSlug.value = stepSlug.value;
if (isStepRunning(step.value)) {
stream.value = apiClient.streamLogs(repo.value.id, pipeline.value.number, step.value.id, (line) => { stream.value = apiClient.streamLogs(repo.value.id, pipeline.value.number, step.value.id, (line) => {
writeLog({ index: line.line, text: b64DecodeUnicode(line.data), time: line.time }); writeLog({ index: line.line, text: b64DecodeUnicode(line.data), time: line.time });
flushLogs(true); flushLogs(true);
@ -308,18 +308,22 @@ async function loadLogs() {
} }
onMounted(async () => { onMounted(async () => {
loadLogs(); await loadLogs();
}); });
watch(stepSlug, () => { watch(stepSlug, async () => {
loadLogs(); await loadLogs();
}); });
watch(step, (oldStep, newStep) => { watch(step, async (newStep, oldStep) => {
if (oldStep && oldStep.name === newStep?.name && oldStep?.end_time !== newStep?.end_time) { if (oldStep?.name === newStep?.name) {
if (autoScroll.value) { if (oldStep?.end_time !== newStep?.end_time && autoScroll.value) {
scrollDown(); scrollDown();
} }
if (oldStep?.state !== newStep?.state) {
await loadLogs();
}
} }
}); });
</script> </script>

View file

@ -11,7 +11,7 @@ export function useTabsProvider({
disableHashMode, disableHashMode,
updateActiveTabProp, updateActiveTabProp,
}: { }: {
activeTabProp: Ref<string>; activeTabProp: Ref<string | undefined>;
updateActiveTabProp: (tab: string) => void; updateActiveTabProp: (tab: string) => void;
disableHashMode: Ref<boolean>; disableHashMode: Ref<boolean>;
}) { }) {

View file

@ -39,53 +39,33 @@ export default class ApiClient {
this.csrf = csrf; this.csrf = csrf;
} }
private _request(method: string, path: string, data: unknown): Promise<unknown> { private async _request(method: string, path: string, data: unknown): Promise<unknown> {
const endpoint = `${this.server}${path}`; const res = await fetch(`${this.server}${path}`, {
const xhr = new XMLHttpRequest(); method,
xhr.open(method, endpoint, true); headers: {
...(method !== 'GET' && this.csrf ? { 'X-CSRF-TOKEN': this.csrf } : {}),
if (this.token) { ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
xhr.setRequestHeader('Authorization', `Bearer ${this.token}`); },
} body: data ? JSON.stringify(data) : undefined,
if (method !== 'GET' && this.csrf) {
xhr.setRequestHeader('X-CSRF-TOKEN', this.csrf);
}
return new Promise((resolve, reject) => {
xhr.onload = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 300) {
const error: ApiError = {
status: xhr.status,
message: xhr.response,
};
if (this.onerror) {
this.onerror(error);
}
reject(error);
return;
}
const contentType = xhr.getResponseHeader('Content-Type');
if (contentType && contentType.startsWith('application/json')) {
resolve(JSON.parse(xhr.response));
} else {
resolve(xhr.response);
}
}
};
xhr.onerror = (e) => {
reject(e);
};
if (data) {
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
} else {
xhr.send();
}
}); });
if (!res.ok) {
const error: ApiError = {
status: res.status,
message: res.statusText,
};
if (this.onerror) {
this.onerror(error);
}
throw new Error(res.statusText);
}
const contentType = res.headers.get('Content-Type');
if (contentType && contentType.startsWith('application/json')) {
return res.json();
}
return res.text();
} }
_get(path: string) { _get(path: string) {

View file

@ -7,6 +7,7 @@
:to="{ name: 'repo-branch', params: { branch } }" :to="{ name: 'repo-branch', params: { branch } }"
> >
{{ branch }} {{ branch }}
<Badge v-if="branch === repo?.default_branch" :label="$t('default')" class="ml-auto" />
</ListItem> </ListItem>
</div> </div>
</template> </template>
@ -14,6 +15,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { inject, Ref, watch } from 'vue'; import { inject, Ref, watch } from 'vue';
import Badge from '~/components/atomic/Badge.vue';
import ListItem from '~/components/atomic/ListItem.vue'; import ListItem from '~/components/atomic/ListItem.vue';
import useApiClient from '~/compositions/useApiClient'; import useApiClient from '~/compositions/useApiClient';
import { usePagination } from '~/compositions/usePaginate'; import { usePagination } from '~/compositions/usePaginate';

View file

@ -1,5 +1,5 @@
<template> <template>
<FluidContainer full-width class="flex flex-col flex-grow md:min-h-xs"> <Container full-width class="flex flex-col flex-grow md:min-h-xs">
<div class="flex w-full min-h-0 flex-grow"> <div class="flex w-full min-h-0 flex-grow">
<PipelineStepList <PipelineStepList
v-if="pipeline?.workflows?.length || 0 > 0" v-if="pipeline?.workflows?.length || 0 > 0"
@ -25,7 +25,6 @@
color="blue" color="blue"
:start-icon="forge ?? 'repo'" :start-icon="forge ?? 'repo'"
:text="$t('repo.pipeline.protected.review')" :text="$t('repo.pipeline.protected.review')"
:is-loading="isApprovingPipeline"
:to="pipeline.link_url" :to="pipeline.link_url"
:title="message" :title="message"
/> />
@ -57,7 +56,7 @@
/> />
</div> </div>
</div> </div>
</FluidContainer> </Container>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -67,7 +66,7 @@ import { useRoute, useRouter } from 'vue-router';
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';
import FluidContainer from '~/components/layout/FluidContainer.vue'; import Container from '~/components/layout/Container.vue';
import PipelineLog from '~/components/repo/pipeline/PipelineLog.vue'; import PipelineLog from '~/components/repo/pipeline/PipelineLog.vue';
import PipelineStepList from '~/components/repo/pipeline/PipelineStepList.vue'; import PipelineStepList from '~/components/repo/pipeline/PipelineStepList.vue';
import useApiClient from '~/compositions/useApiClient'; import useApiClient from '~/compositions/useApiClient';

View file

@ -5,8 +5,8 @@
enable-tabs enable-tabs
disable-hash-mode disable-hash-mode
:go-back="goBack" :go-back="goBack"
:fluid-content="activeTab !== 'tasks'" :fluid-content="activeTab === 'tasks'"
:full-width="true" full-width-header
> >
<template #title>{{ repo.full_name }}</template> <template #title>{{ repo.full_name }}</template>