mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
Compare commits
5 Commits
1667ed2a16
...
create-tas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d62433148b | ||
|
|
d44364d4d5 | ||
|
|
9118dc0835 | ||
|
|
97bf035a9f | ||
|
|
1e7ac62c51 |
@@ -0,0 +1,66 @@
|
||||
# Example
|
||||
|
||||
```vue-template
|
||||
<div class="ui-task-item">
|
||||
<div v-if="task.name" v-tooltip class="text-ellipsis typo-body-regular">
|
||||
{{ task.name }}
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<UiTag v-if="task.tag" accent="neutral" variant="primary" size="medium">
|
||||
{{ task.tag }}
|
||||
</UiTag>
|
||||
<div v-if="task.subtasks" class="subtasks">
|
||||
<VtsIcon accent="current" class="icon" :icon="faCircleNotch" />
|
||||
<p class="typo-form-info text-ellipsis">
|
||||
{{ t('tasks.n-subtasks', { n: subtasks }) }}
|
||||
</p>
|
||||
</div>
|
||||
<UiInfo v-for="(item, index) in messageTypes" :key="index" v-tooltip :accent="item.type as any">
|
||||
{{ item.message }}
|
||||
</UiInfo>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="user typo-body-regular-small">
|
||||
<div v-if="user" v-tooltip class="user-name text-ellipsis">
|
||||
<span>{{ t('by') }}</span>
|
||||
<UiUserLink :username="user" />
|
||||
</div>
|
||||
<span v-if="taskIsComplete" class="text-ellipsis">
|
||||
{{ $t('task.finished') }}
|
||||
<RelativeTime v-if="task.end" :date="task.end" />
|
||||
</span>
|
||||
<span v-else class="text-ellipsis">
|
||||
{{ $t('task.started') }}
|
||||
<RelativeTime v-if="task.start" :date="task.start" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="progress-circle-bar">
|
||||
<UiCircleProgressBar
|
||||
v-if="circleProgress"
|
||||
:accent="getEffectiveStatus"
|
||||
size="small"
|
||||
:value="circleProgress"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```vue-script
|
||||
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
||||
import RelativeTime from '@core/components/relative-time/RelativeTime.vue'
|
||||
import UiCircleProgressBar, {
|
||||
type CircleProgressBarAccent,
|
||||
} from '@core/components/ui/circle-progress-bar/UiCircleProgressBar.vue'
|
||||
import UiInfo from '@core/components/ui/info/UiInfo.vue'
|
||||
import UiTag from '@core/components/ui/tag/UiTag.vue'
|
||||
import UiUserLink from '@core/components/ui/user-link/UiUserLink.vue'
|
||||
import { useMapper } from '@core/composables/mapper.composable.ts'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive'
|
||||
import type { Message, Task, TaskStatus } from '@core/types/task.type.ts'
|
||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[prop('task').required(), prop('user').str().preset('Username').widget()]"
|
||||
>
|
||||
<UiTaskItem v-bind="properties" :task />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { prop } from '@/libs/story/story-param.ts'
|
||||
import UiTaskItem from '@core/components/ui/task-item/UiTaskItem.vue'
|
||||
|
||||
const task = {
|
||||
id: '0m96rljcx',
|
||||
status: 'pending',
|
||||
name: 'Task name',
|
||||
tag: 'xo:task:action',
|
||||
start: 1744011802689,
|
||||
end: 1745395694000,
|
||||
infos: [
|
||||
{
|
||||
message: 'Started',
|
||||
},
|
||||
{
|
||||
message: 'info bar',
|
||||
},
|
||||
],
|
||||
errors: [
|
||||
{
|
||||
message: 'error foo',
|
||||
},
|
||||
{
|
||||
message: 'error bar',
|
||||
},
|
||||
],
|
||||
warnings: [
|
||||
{
|
||||
message: 'info bar',
|
||||
},
|
||||
],
|
||||
progress: 25,
|
||||
}
|
||||
</script>
|
||||
@@ -79,7 +79,8 @@ export class VmController extends XapiXoController<XoVm> {
|
||||
property: 'resident_on',
|
||||
})
|
||||
) {
|
||||
/* throw */ invalidParameters(`VM ${id} is halted or host could not be found.`, error)
|
||||
/* throw */
|
||||
invalidParameters(`VM ${id} is halted or host could not be found.`, error)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<span :title="date.toLocaleString()">{{ relativeTime }}</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useRelativeTime from '@core/composables/relative-time.composable'
|
||||
import { parseDateTime } from '@core/utils/time.util.ts'
|
||||
import { useNow } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
date: Date | number | string
|
||||
}>()
|
||||
|
||||
const date = computed(() => new Date(parseDateTime(props.date)))
|
||||
const now = useNow({ interval: 1000 })
|
||||
const relativeTime = useRelativeTime(date, now)
|
||||
</script>
|
||||
@@ -46,7 +46,7 @@ const {
|
||||
|
||||
const { n } = useI18n()
|
||||
|
||||
type CircleProgressBarAccent = 'info' | 'warning' | 'danger'
|
||||
export type CircleProgressBarAccent = 'info' | 'warning' | 'danger'
|
||||
type CircleProgressBarSize = 'extra-small' | 'small' | 'medium' | 'large'
|
||||
|
||||
const circleSizeMap = {
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
<!-- v3 -->
|
||||
<template>
|
||||
<div class="ui-task-item">
|
||||
<div v-if="task.name" v-tooltip class="text-ellipsis typo-body-regular">
|
||||
{{ task.name }}
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<UiTag v-if="task.tag" accent="neutral" variant="primary" size="medium">
|
||||
{{ task.tag }}
|
||||
</UiTag>
|
||||
<div v-if="task.subtasks" class="subtasks">
|
||||
<VtsIcon accent="current" class="icon" :icon="faCircleNotch" />
|
||||
<p class="typo-form-info text-ellipsis">
|
||||
{{ t('tasks.n-subtasks', { n: subtasks }) }}
|
||||
</p>
|
||||
</div>
|
||||
<UiInfo v-for="(item, index) in messageTypes" :key="index" v-tooltip :accent="item.type as any">
|
||||
{{ item.message }}
|
||||
</UiInfo>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="user typo-body-regular-small">
|
||||
<div v-if="user" v-tooltip class="user-name text-ellipsis">
|
||||
<span>{{ t('by') }}</span>
|
||||
<UiUserLink :username="user" />
|
||||
</div>
|
||||
<span v-if="taskIsComplete" class="text-ellipsis">
|
||||
{{ $t('task.finished') }}
|
||||
<VtsRelativeTime v-if="task.end" :date="task.end" />
|
||||
</span>
|
||||
<span v-else class="text-ellipsis">
|
||||
{{ $t('task.started') }}
|
||||
<VtsRelativeTime v-if="task.start" :date="task.start" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="progress-circle-bar">
|
||||
<UiCircleProgressBar
|
||||
v-if="circleProgress"
|
||||
:accent="getEffectiveStatus"
|
||||
size="small"
|
||||
:value="circleProgress"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
||||
import VtsRelativeTime from '@core/components/relative-time/VtsRelativeTime.vue'
|
||||
import UiCircleProgressBar, {
|
||||
type CircleProgressBarAccent,
|
||||
} from '@core/components/ui/circle-progress-bar/UiCircleProgressBar.vue'
|
||||
import UiInfo from '@core/components/ui/info/UiInfo.vue'
|
||||
import UiTag from '@core/components/ui/tag/UiTag.vue'
|
||||
import UiUserLink from '@core/components/ui/user-link/UiUserLink.vue'
|
||||
import { useMapper } from '@core/composables/mapper.composable.ts'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive'
|
||||
import type { Message, Task, TaskStatus } from '@core/types/task.type.ts'
|
||||
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { task, user } = defineProps<{
|
||||
task: Task
|
||||
user?: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const taskIsComplete = computed(() => {
|
||||
if (!task.end || !task.start) {
|
||||
return
|
||||
}
|
||||
|
||||
return task.end >= task.start || task.progress === 100
|
||||
})
|
||||
|
||||
const countSubtasks = (task: Task): number => {
|
||||
if (!task.subtasks || task.subtasks.length === 0) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return task.subtasks.length + task.subtasks.reduce((sum: number, subtask: any) => sum + countSubtasks(subtask), 0)
|
||||
}
|
||||
|
||||
const subtasks = computed(() => countSubtasks(task))
|
||||
|
||||
const circleProgress = computed(() => {
|
||||
if (['success', 'failure'].includes(task.status)) {
|
||||
return 100
|
||||
} else {
|
||||
return task.progress
|
||||
}
|
||||
})
|
||||
|
||||
const getEffectiveStatus = computed<CircleProgressBarAccent>(() => {
|
||||
const evaluate = (task: Task) => {
|
||||
const subtasks = task.subtasks || []
|
||||
|
||||
if (subtasks.length === 0) {
|
||||
return useMapper<TaskStatus, CircleProgressBarAccent>(
|
||||
() => task.status,
|
||||
{
|
||||
pending: 'warning',
|
||||
interrupted: 'warning',
|
||||
success: 'info',
|
||||
failure: 'danger',
|
||||
},
|
||||
() => 'info'
|
||||
).value
|
||||
}
|
||||
|
||||
const subStatuses = subtasks.map(evaluate)
|
||||
const allFailure = subStatuses.every(status => status === 'danger')
|
||||
const someFailure = subStatuses.some(status => status === 'danger')
|
||||
const allSuccess = subStatuses.every(status => status === 'info')
|
||||
|
||||
if (allFailure) {
|
||||
return 'danger'
|
||||
}
|
||||
if (someFailure) {
|
||||
return 'warning'
|
||||
}
|
||||
if (allSuccess) {
|
||||
return 'info'
|
||||
}
|
||||
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
return evaluate(task)
|
||||
})
|
||||
|
||||
const generateMessages = (task: Task) => {
|
||||
const results = {
|
||||
info: { count: 0, messages: [] as Message[] },
|
||||
danger: { count: 0, messages: [] as Message[] },
|
||||
warning: { count: 0, messages: [] as Message[] },
|
||||
}
|
||||
|
||||
const accumulate = (task: Task) => {
|
||||
const infos = task.infos || []
|
||||
const errors = task.errors || []
|
||||
const warnings = task.warnings || []
|
||||
|
||||
results.info.count += infos.length
|
||||
results.info.messages.push(...infos)
|
||||
|
||||
results.danger.count += errors.length
|
||||
results.danger.messages.push(...errors)
|
||||
|
||||
results.warning.count += warnings.length
|
||||
results.warning.messages.push(...warnings)
|
||||
|
||||
task.subtasks?.forEach(accumulate)
|
||||
}
|
||||
|
||||
accumulate(task)
|
||||
|
||||
return [
|
||||
...(results.info.count > 0
|
||||
? [{ type: 'info', message: t('information', { n: results.info.count }), messages: results.info.messages }]
|
||||
: []),
|
||||
...(results.danger.count > 0
|
||||
? [{ type: 'danger', message: t('error', { n: results.danger.count }), messages: results.danger.messages }]
|
||||
: []),
|
||||
...(results.warning.count > 0
|
||||
? [{ type: 'warning', message: t('warning', { n: results.warning.count }), messages: results.warning.messages }]
|
||||
: []),
|
||||
]
|
||||
}
|
||||
const messageTypes = computed(() => generateMessages(task))
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.ui-task-item {
|
||||
padding: 0.2rem;
|
||||
margin-right: 1.6rem;
|
||||
display: flex;
|
||||
height: 4.8rem;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
gap: 1.6rem;
|
||||
padding-inline: 0.8rem;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
gap: 1.6rem;
|
||||
align-items: center;
|
||||
|
||||
/*keep the same space even iif circleProgress is undefined*/
|
||||
.progress-circle-bar {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
.subtasks {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 1rem;
|
||||
color: var(--color-neutral-txt-secondary);
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
> span::before {
|
||||
content: '•';
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
span:first-of-type::before {
|
||||
content: '';
|
||||
}
|
||||
|
||||
&:has(.user-name) > span::before {
|
||||
content: '•';
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.user:has(.user-name) > div > span:first-child {
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -54,7 +54,7 @@ export default function useRelativeTime(fromDate: MaybeRef<Date>, toDate: MaybeR
|
||||
parts.push(t('relative-time.minute', { n: minutes }))
|
||||
}
|
||||
|
||||
if (days === 0 && seconds > 0) {
|
||||
if (minutes === 0 && seconds > 0) {
|
||||
parts.push(t('relative-time.second', { n: seconds }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"boot-firmware-bios": "The template already contains the BIOS strings",
|
||||
"boot-firmware-uefi": "The boot firmware is UEFI",
|
||||
"boot-vm": "Boot VM",
|
||||
"by": "By",
|
||||
"bytes.gi": "GiB",
|
||||
"bytes.ki": "KiB",
|
||||
"bytes.mi": "MiB",
|
||||
@@ -151,6 +152,7 @@
|
||||
"enabled": "Enabled",
|
||||
"end-of-life": "End of life",
|
||||
"eol": "EOL",
|
||||
"error": "{n} error | {n} errors",
|
||||
"error-no-data": "Error, can't collect data.",
|
||||
"error-occurred": "An error has occurred",
|
||||
"exit-fullscreen": "Exit fullscreen",
|
||||
@@ -206,6 +208,7 @@
|
||||
"hosts-status.unknown.tooltip": "Hosts metrics are unavailable",
|
||||
"id": "Id",
|
||||
"in-last-three-jobs": "In their last three jobs",
|
||||
"information": "{n} information | {n} informations",
|
||||
"install-settings": "Install settings",
|
||||
"interfaces": "Interface | Interface | Interfaces",
|
||||
"invalid-field": "Invalid field",
|
||||
@@ -430,6 +433,8 @@
|
||||
"table-actions": "Table actions",
|
||||
"tags": "Tags",
|
||||
"task.estimated-end": "Estimated end",
|
||||
"task.estimated-end-in": "Estimated end in {time}",
|
||||
"task.finished": "Finished",
|
||||
"task.progress": "Progress",
|
||||
"task.started": "Started",
|
||||
"tasks": "Tasks",
|
||||
@@ -491,6 +496,7 @@
|
||||
"vms-status.suspended": "Suspended",
|
||||
"vms-status.unknown": "Unknown",
|
||||
"vms-status.unknown.tooltip": "For which XO has lost connection to the pool",
|
||||
"warning": "{n} warning | {n} warnings",
|
||||
"xo-backups": "XO backups",
|
||||
"xo-lite-under-construction": "XOLite is under construction",
|
||||
"xo-replications": "XO replications",
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"boot-firmware-bios": "Le modèle contient déjà les BIOS strings",
|
||||
"boot-firmware-uefi": "Le boot firmware est UEFI",
|
||||
"boot-vm": "Démarrer la VM",
|
||||
"by": "Par",
|
||||
"bytes.gi": "Gio",
|
||||
"bytes.ki": "Kio",
|
||||
"bytes.mi": "Mio",
|
||||
@@ -151,6 +152,7 @@
|
||||
"enabled": "Activé",
|
||||
"end-of-life": "Fin de vie",
|
||||
"eol": "EOL",
|
||||
"error": "{n} erreur | {n} erreurs",
|
||||
"error-no-data": "Erreur, impossible de collecter les données.",
|
||||
"error-occurred": "Une erreur est survenue",
|
||||
"exit-fullscreen": "Quitter le plein écran",
|
||||
@@ -206,6 +208,7 @@
|
||||
"hosts-status.unknown.tooltip": "Les métriques de l'hôte sont inaccessibles",
|
||||
"id": "Id",
|
||||
"in-last-three-jobs": "Dans leurs trois derniers jobs",
|
||||
"information": "{n} information | {n} informations",
|
||||
"install-settings": "Paramètres d'installation",
|
||||
"interfaces": "Interface | Interface | Interfaces",
|
||||
"invalid-field": "Champ invalide",
|
||||
@@ -430,8 +433,10 @@
|
||||
"table-actions": "Actions du tableau",
|
||||
"tags": "Tags",
|
||||
"task.estimated-end": "Fin estimée",
|
||||
"task.estimated-end-in": "Fin estimée dans {time}",
|
||||
"task.finished": "terminée",
|
||||
"task.progress": "Progression",
|
||||
"task.started": "Démarré",
|
||||
"task.started": "Démarrée",
|
||||
"tasks": "Tâches",
|
||||
"tasks.n-subtasks": "{n} sous-tâche | {n} sous-tâches",
|
||||
"tasks.no-tasks": "Aucune tâche",
|
||||
@@ -491,6 +496,7 @@
|
||||
"vms-status.suspended": "Suspendues",
|
||||
"vms-status.unknown": "Inconnu",
|
||||
"vms-status.unknown.tooltip": "Dont XO a perdu la connexion avec le pool",
|
||||
"warning": "{n} avertissement | {n} avertissements",
|
||||
"xo-backups": "Sauvegardes XO",
|
||||
"xo-lite-under-construction": "XOLite est en construction",
|
||||
"xo-replications": "Réplications XO",
|
||||
|
||||
20
@xen-orchestra/web-core/lib/types/task.type.ts
Normal file
20
@xen-orchestra/web-core/lib/types/task.type.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export type TaskStatus = 'pending' | 'success' | 'failure' | 'interrupted'
|
||||
|
||||
export type Message = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export type Task = {
|
||||
id: string | number
|
||||
name: string
|
||||
status: TaskStatus
|
||||
tag: string
|
||||
start: number
|
||||
end?: number
|
||||
userId?: string
|
||||
subtasks?: Task[]
|
||||
progress?: number
|
||||
infos?: Message[]
|
||||
errors?: Message[]
|
||||
warnings?: Message[]
|
||||
}
|
||||
8
@xen-orchestra/web/src/types/xo/user.type.ts
Normal file
8
@xen-orchestra/web/src/types/xo/user.type.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { Branded } from '@core/types/utility.type.ts'
|
||||
|
||||
export type XoUser = {
|
||||
id: Branded<'user'>
|
||||
email: string
|
||||
name?: string
|
||||
permission: string
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export const xoApiDefinition = {
|
||||
task: {
|
||||
type: 'collection',
|
||||
path: 'tasks',
|
||||
fields: 'id,start,end,properties,status,progress,tasks',
|
||||
fields: 'id,properties,start,status,updatedAt,end,tasks,infos,errors,warnings',
|
||||
handler: (record: XoTask) => record,
|
||||
},
|
||||
dashboard: {
|
||||
|
||||
Reference in New Issue
Block a user