mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-02-16 19:35:14 +00:00
Implement visual display of queue statistics (#1657)
Light mode: ![attels](https://user-images.githubusercontent.com/165205/226354030-5769ddce-3727-441a-8ecb-c6cc3cf70529.png) Dark mode: ![attels](https://user-images.githubusercontent.com/165205/226353854-51b2da0a-b35a-4f61-bf28-e21179164a05.png)
This commit is contained in:
parent
2337f1854a
commit
7870c29f5f
7 changed files with 140 additions and 19 deletions
1
web/components.d.ts
vendored
1
web/components.d.ts
vendored
|
@ -12,6 +12,7 @@ declare module '@vue/runtime-core' {
|
||||||
ActionsTab: typeof import('./src/components/repo/settings/ActionsTab.vue')['default']
|
ActionsTab: typeof import('./src/components/repo/settings/ActionsTab.vue')['default']
|
||||||
ActivePipelines: typeof import('./src/components/layout/header/ActivePipelines.vue')['default']
|
ActivePipelines: typeof import('./src/components/layout/header/ActivePipelines.vue')['default']
|
||||||
AdminAgentsTab: typeof import('./src/components/admin/settings/AdminAgentsTab.vue')['default']
|
AdminAgentsTab: typeof import('./src/components/admin/settings/AdminAgentsTab.vue')['default']
|
||||||
|
AdminQueueStats: typeof import('./src/components/admin/settings/queue/AdminQueueStats.vue')['default']
|
||||||
AdminQueueTab: typeof import('./src/components/admin/settings/AdminQueueTab.vue')['default']
|
AdminQueueTab: typeof import('./src/components/admin/settings/AdminQueueTab.vue')['default']
|
||||||
AdminSecretsTab: typeof import('./src/components/admin/settings/AdminSecretsTab.vue')['default']
|
AdminSecretsTab: typeof import('./src/components/admin/settings/AdminSecretsTab.vue')['default']
|
||||||
AdminUsersTab: typeof import('./src/components/admin/settings/AdminUsersTab.vue')['default']
|
AdminUsersTab: typeof import('./src/components/admin/settings/AdminUsersTab.vue')['default']
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
"@vueuse/core": "^9.13.0",
|
"@vueuse/core": "^9.13.0",
|
||||||
"ansi_up": "^5.1.0",
|
"ansi_up": "^5.1.0",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"floating-vue": "2.0.0-beta.20",
|
"floating-vue": "^2.0.0-beta.20",
|
||||||
"fuse.js": "^6.6.2",
|
"fuse.js": "^6.6.2",
|
||||||
"humanize-duration": "^3.28.0",
|
"humanize-duration": "^3.28.0",
|
||||||
"javascript-time-ago": "^2.5.9",
|
"javascript-time-ago": "^2.5.9",
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||||
"eslint-plugin-vue": "^9.9.0",
|
"eslint-plugin-vue": "^9.9.0",
|
||||||
"eslint-plugin-vue-scoped-css": "^2.4.0",
|
"eslint-plugin-vue-scoped-css": "^2.4.0",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^2.8.5",
|
||||||
"typescript": "5.0.2",
|
"typescript": "5.0.2",
|
||||||
"unplugin-icons": "^0.15.3",
|
"unplugin-icons": "^0.15.3",
|
||||||
"unplugin-vue-components": "^0.24.1",
|
"unplugin-vue-components": "^0.24.1",
|
||||||
|
|
|
@ -27,14 +27,14 @@ specifiers:
|
||||||
eslint-plugin-simple-import-sort: ^10.0.0
|
eslint-plugin-simple-import-sort: ^10.0.0
|
||||||
eslint-plugin-vue: ^9.9.0
|
eslint-plugin-vue: ^9.9.0
|
||||||
eslint-plugin-vue-scoped-css: ^2.4.0
|
eslint-plugin-vue-scoped-css: ^2.4.0
|
||||||
floating-vue: 2.0.0-beta.20
|
floating-vue: ^2.0.0-beta.20
|
||||||
fuse.js: ^6.6.2
|
fuse.js: ^6.6.2
|
||||||
humanize-duration: ^3.28.0
|
humanize-duration: ^3.28.0
|
||||||
javascript-time-ago: ^2.5.9
|
javascript-time-ago: ^2.5.9
|
||||||
lodash: ^4.17.21
|
lodash: ^4.17.21
|
||||||
node-emoji: ^1.11.0
|
node-emoji: ^1.11.0
|
||||||
pinia: ^2.0.33
|
pinia: ^2.0.33
|
||||||
prettier: ^2.8.4
|
prettier: ^2.8.5
|
||||||
prismjs: ^1.29.0
|
prismjs: ^1.29.0
|
||||||
typescript: 5.0.2
|
typescript: 5.0.2
|
||||||
unplugin-icons: ^0.15.3
|
unplugin-icons: ^0.15.3
|
||||||
|
@ -85,12 +85,12 @@ devDependencies:
|
||||||
eslint-config-airbnb-typescript: 17.0.0_g4ufqspmwdmninnalnmypeb4gi
|
eslint-config-airbnb-typescript: 17.0.0_g4ufqspmwdmninnalnmypeb4gi
|
||||||
eslint-config-prettier: 8.7.0_eslint@8.36.0
|
eslint-config-prettier: 8.7.0_eslint@8.36.0
|
||||||
eslint-plugin-import: 2.27.5_a7er6olmtneep4uytpot6gt7wu
|
eslint-plugin-import: 2.27.5_a7er6olmtneep4uytpot6gt7wu
|
||||||
eslint-plugin-prettier: 4.2.1_eqzx3hpkgx5nnvxls3azrcc7dm
|
eslint-plugin-prettier: 4.2.1_e4jq4ivnnnwcsomlcyj2qoljy4
|
||||||
eslint-plugin-promise: 6.1.1_eslint@8.36.0
|
eslint-plugin-promise: 6.1.1_eslint@8.36.0
|
||||||
eslint-plugin-simple-import-sort: 10.0.0_eslint@8.36.0
|
eslint-plugin-simple-import-sort: 10.0.0_eslint@8.36.0
|
||||||
eslint-plugin-vue: 9.9.0_eslint@8.36.0
|
eslint-plugin-vue: 9.9.0_eslint@8.36.0
|
||||||
eslint-plugin-vue-scoped-css: 2.4.0_baumsyql7ym4rscil5ymcumriy
|
eslint-plugin-vue-scoped-css: 2.4.0_baumsyql7ym4rscil5ymcumriy
|
||||||
prettier: 2.8.4
|
prettier: 2.8.5
|
||||||
typescript: 5.0.2
|
typescript: 5.0.2
|
||||||
unplugin-icons: 0.15.3_@vue+compiler-sfc@3.2.47
|
unplugin-icons: 0.15.3_@vue+compiler-sfc@3.2.47
|
||||||
unplugin-vue-components: 0.24.1_vue@3.2.47
|
unplugin-vue-components: 0.24.1_vue@3.2.47
|
||||||
|
@ -1763,7 +1763,7 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/eslint-plugin-prettier/4.2.1_eqzx3hpkgx5nnvxls3azrcc7dm:
|
/eslint-plugin-prettier/4.2.1_e4jq4ivnnnwcsomlcyj2qoljy4:
|
||||||
resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==}
|
resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -1776,7 +1776,7 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 8.36.0
|
eslint: 8.36.0
|
||||||
eslint-config-prettier: 8.7.0_eslint@8.36.0
|
eslint-config-prettier: 8.7.0_eslint@8.36.0
|
||||||
prettier: 2.8.4
|
prettier: 2.8.5
|
||||||
prettier-linter-helpers: 1.0.0
|
prettier-linter-helpers: 1.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
@ -2842,8 +2842,8 @@ packages:
|
||||||
fast-diff: 1.2.0
|
fast-diff: 1.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/prettier/2.8.4:
|
/prettier/2.8.5:
|
||||||
resolution: {integrity: sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==}
|
resolution: {integrity: sha512-3gzuxrHbKUePRBB4ZeU08VNkUcqEHaUaouNt0m7LGP4Hti/NuB07C7PPTM/LkWqXoJYJn2McEo5+kxPNrtQkLQ==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
|
@ -376,7 +376,14 @@
|
||||||
"tasks": "Tasks",
|
"tasks": "Tasks",
|
||||||
"task_running": "Task is running",
|
"task_running": "Task is running",
|
||||||
"task_pending": "Task is pending",
|
"task_pending": "Task is pending",
|
||||||
"task_waiting_on_deps": "Task is waiting on dependencies"
|
"task_waiting_on_deps": "Task is waiting on dependencies",
|
||||||
|
"stats": {
|
||||||
|
"completed_count": "Completed Tasks",
|
||||||
|
"worker_count": "Free",
|
||||||
|
"running_count": "Running",
|
||||||
|
"pending_count": "Pending",
|
||||||
|
"waiting_on_deps_count": "Waiting on dependencies"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<pre>{{ queueInfo?.stats }}</pre>
|
<AdminQueueStats :stats="queueInfo?.stats" />
|
||||||
|
|
||||||
<div v-if="tasks.length > 0" class="flex flex-col">
|
<div v-if="tasks.length > 0" class="flex flex-col">
|
||||||
<p class="mt-6 mb-2 text-xl">{{ $t('admin.settings.queue.tasks') }}</p>
|
<p class="mt-6 mb-2 text-xl">{{ $t('admin.settings.queue.tasks') }}</p>
|
||||||
|
@ -85,6 +85,8 @@ import useApiClient from '~/compositions/useApiClient';
|
||||||
import useNotifications from '~/compositions/useNotifications';
|
import useNotifications from '~/compositions/useNotifications';
|
||||||
import { QueueInfo } from '~/lib/api/types/queue';
|
import { QueueInfo } from '~/lib/api/types/queue';
|
||||||
|
|
||||||
|
import AdminQueueStats from './queue/AdminQueueStats.vue';
|
||||||
|
|
||||||
const apiClient = useApiClient();
|
const apiClient = useApiClient();
|
||||||
const notifications = useNotifications();
|
const notifications = useNotifications();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
109
web/src/components/admin/settings/queue/AdminQueueStats.vue
Normal file
109
web/src/components/admin/settings/queue/AdminQueueStats.vue
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="stats" class="flex justify-center">
|
||||||
|
<div class="bg-gray-100 dark:bg-dark-gray-600 text-color dark:text-gray-400 rounded-md py-5 px-5 w-full">
|
||||||
|
<div class="flex w-full">
|
||||||
|
<h3 class="text-lg font-semibold leading-tight uppercase flex-1">
|
||||||
|
{{ $t('admin.settings.queue.stats.completed_count') }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="relative overflow-hidden transition-all duration-500">
|
||||||
|
<div>
|
||||||
|
<div class="pb-4 lg:pb-6">
|
||||||
|
<h4 class="text-2xl lg:text-3xl font-semibold leading-tight inline-block">
|
||||||
|
{{ stats.completed_count }}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="pb-4 lg:pb-6">
|
||||||
|
<div class="overflow-hidden rounded-full h-3 flex transition-all duration-500">
|
||||||
|
<div
|
||||||
|
v-for="item in data"
|
||||||
|
:key="item.key"
|
||||||
|
class="h-full"
|
||||||
|
:class="`${item.color}`"
|
||||||
|
:style="{ width: `${item.perc}%` }"
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex -mx-4 sm:flex-wrap">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in data"
|
||||||
|
:key="item.key"
|
||||||
|
class="px-4 md:w-1/4 sm:w-full"
|
||||||
|
:class="{ 'md:border-l border-gray-300 dark:border-gray-600': index !== 0 }"
|
||||||
|
>
|
||||||
|
<div class="text-sm whitespace-nowrap overflow-hidden text-ellipsis">
|
||||||
|
<span class="inline-block w-2 h-2 rounded-full mr-1 align-middle" :class="`${item.color}`"> </span>
|
||||||
|
<span class="align-middle">{{ item.label }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="font-medium text-lg">
|
||||||
|
{{ item.value }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
import { QueueStats } from '~/lib/api/types/queue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
stats?: QueueStats;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const total = computed(() => {
|
||||||
|
if (!props.stats) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
props.stats.worker_count + props.stats.running_count + props.stats.pending_count + props.stats.waiting_on_deps_count
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = computed(() => {
|
||||||
|
if (!props.stats) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'worker_count',
|
||||||
|
label: t('admin.settings.queue.stats.worker_count'),
|
||||||
|
value: props.stats.worker_count,
|
||||||
|
perc: total.value > 0 ? (props.stats.worker_count / total.value) * 100 : 0,
|
||||||
|
color: 'bg-green-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'running_count',
|
||||||
|
label: t('admin.settings.queue.stats.running_count'),
|
||||||
|
value: props.stats.running_count,
|
||||||
|
perc: total.value > 0 ? (props.stats.running_count / total.value) * 100 : 100,
|
||||||
|
color: 'bg-blue-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pending_count',
|
||||||
|
label: t('admin.settings.queue.stats.pending_count'),
|
||||||
|
value: props.stats.pending_count,
|
||||||
|
perc: total.value > 0 ? (props.stats.pending_count / total.value) * 100 : 0,
|
||||||
|
color: 'bg-red-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'waiting_on_deps_count',
|
||||||
|
label: t('admin.settings.queue.stats.waiting_on_deps_count'),
|
||||||
|
value: props.stats.waiting_on_deps_count,
|
||||||
|
perc: total.value > 0 ? (props.stats.waiting_on_deps_count / total.value) * 100 : 0,
|
||||||
|
color: 'bg-red-800',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -7,16 +7,18 @@ export type Task = {
|
||||||
run_on: string[];
|
run_on: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type QueueStats = {
|
||||||
|
worker_count: number;
|
||||||
|
pending_count: number;
|
||||||
|
waiting_on_deps_count: number;
|
||||||
|
running_count: number;
|
||||||
|
completed_count: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type QueueInfo = {
|
export type QueueInfo = {
|
||||||
pending: Task[];
|
pending: Task[];
|
||||||
waiting_on_deps: Task[];
|
waiting_on_deps: Task[];
|
||||||
running: Task[];
|
running: Task[];
|
||||||
stats: {
|
stats: QueueStats;
|
||||||
worker_count: number;
|
|
||||||
pending_count: number;
|
|
||||||
waiting_on_deps_count: number;
|
|
||||||
running_count: number;
|
|
||||||
completed_count: number;
|
|
||||||
};
|
|
||||||
paused: boolean;
|
paused: boolean;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue