mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
Compare commits
18 Commits
c145a7a579
...
xo6/create
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f43d3a459 | ||
|
|
c4e6166b63 | ||
|
|
c5bf57dc57 | ||
|
|
834e6b9300 | ||
|
|
b6a5e73d4c | ||
|
|
1d3fae75fe | ||
|
|
9acd0fc751 | ||
|
|
e9c0caca23 | ||
|
|
842d0469f7 | ||
|
|
39f307877f | ||
|
|
a7b24c29c4 | ||
|
|
0ad02715dd | ||
|
|
d06e692aeb | ||
|
|
80c66dd60a | ||
|
|
903e94c9b3 | ||
|
|
f9cae857f9 | ||
|
|
50b8b13707 | ||
|
|
eb922e91ec |
@@ -638,6 +638,7 @@ export type XoTask = {
|
||||
name?: string
|
||||
objectId?: string
|
||||
params?: Record<string, unknown>
|
||||
progress?: number
|
||||
type?: string
|
||||
userId?: string
|
||||
[key: string]: unknown | undefined
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Type
|
||||
|
||||
```typescript
|
||||
type Task = {
|
||||
id: string
|
||||
infos?: { data: unknown; message: string }[]
|
||||
name?: string
|
||||
progress?: number
|
||||
end?: number
|
||||
status: 'failure' | 'interrupted' | 'pending' | 'success'
|
||||
tasks?: Task[]
|
||||
warning?: { data: unknown; message: string }[]
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[
|
||||
prop('task').type('object').required().widget().preset(task),
|
||||
prop('expanded').type('boolean').widget(boolean()).preset(false),
|
||||
prop('depth').type('number').required().preset(1),
|
||||
event('select'),
|
||||
]"
|
||||
>
|
||||
<ul>
|
||||
<UiTaskItem v-bind="properties" />
|
||||
</ul>
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { event, prop } from '@/libs/story/story-param.ts'
|
||||
import { boolean } from '@/libs/story/story-widget.ts'
|
||||
import UiTaskItem, { type Task } from '@core/components/ui/task-item/UiTaskItem.vue'
|
||||
|
||||
const task: Task = {
|
||||
id: '1',
|
||||
name: 'VM backup',
|
||||
status: 'pending',
|
||||
progress: 25,
|
||||
tasks: [
|
||||
{
|
||||
id: '1-1',
|
||||
name: 'Snapshot',
|
||||
end: Date.now() - 1000 * 60 * 3,
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
},
|
||||
],
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,14 @@
|
||||
# Type
|
||||
|
||||
```typescript
|
||||
type Task = {
|
||||
id: string
|
||||
infos?: { data: unknown; message: string }[]
|
||||
name?: string
|
||||
progress?: number
|
||||
end?: number
|
||||
status: 'failure' | 'interrupted' | 'pending' | 'success'
|
||||
tasks?: Task[]
|
||||
warning?: { data: unknown; message: string }[]
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<ComponentStory
|
||||
v-slot="{ properties }"
|
||||
:params="[prop('tasks').type('Task[]').required().preset(tasks), event('select')]"
|
||||
>
|
||||
<UiTaskList v-bind="properties" />
|
||||
</ComponentStory>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ComponentStory from '@/components/component-story/ComponentStory.vue'
|
||||
import { event, prop } from '@/libs/story/story-param.ts'
|
||||
import type { Task } from '@core/components/ui/task-item/UiTaskItem.vue'
|
||||
import UiTaskList from '@core/components/ui/task-list/UiTaskList.vue'
|
||||
|
||||
const tasks: Task[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'VM backup',
|
||||
status: 'pending',
|
||||
progress: 25,
|
||||
tasks: [
|
||||
{
|
||||
id: '1-1',
|
||||
name: 'Snapshot',
|
||||
status: 'success',
|
||||
progress: 100,
|
||||
},
|
||||
{
|
||||
id: '1-2',
|
||||
name: 'Export',
|
||||
status: 'pending',
|
||||
progress: 10,
|
||||
tasks: [
|
||||
{
|
||||
id: '1-2-1',
|
||||
name: 'Sub task',
|
||||
status: 'pending',
|
||||
progress: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'VM Migration',
|
||||
status: 'failure',
|
||||
progress: 100,
|
||||
warning: [
|
||||
{
|
||||
data: { file: 'server-1.log' },
|
||||
message: 'warning',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Storage Migration',
|
||||
status: 'interrupted',
|
||||
progress: 100,
|
||||
infos: [
|
||||
{ data: { rows: 1200 }, message: 'info' },
|
||||
{ data: null, message: 'info' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Final task',
|
||||
end: Date.now() - 1000 * 60 * 15,
|
||||
status: 'success',
|
||||
progress: 75,
|
||||
warning: [
|
||||
{
|
||||
data: { file: 'server-1.log' },
|
||||
message: 'warning',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
</script>
|
||||
2
@xen-orchestra/lite/typed-router.d.ts
vendored
2
@xen-orchestra/lite/typed-router.d.ts
vendored
@@ -117,6 +117,8 @@ declare module 'vue-router/auto-routes' {
|
||||
'/story/web-core/ui/table-pagination/ui-table-pagination': RouteRecordInfo<'/story/web-core/ui/table-pagination/ui-table-pagination', '/story/web-core/ui/table-pagination/ui-table-pagination', Record<never, never>, Record<never, never>>,
|
||||
'/story/web-core/ui/tag/ui-tag': RouteRecordInfo<'/story/web-core/ui/tag/ui-tag', '/story/web-core/ui/tag/ui-tag', Record<never, never>, Record<never, never>>,
|
||||
'/story/web-core/ui/tag/ui-tags-list': RouteRecordInfo<'/story/web-core/ui/tag/ui-tags-list', '/story/web-core/ui/tag/ui-tags-list', Record<never, never>, Record<never, never>>,
|
||||
'/story/web-core/ui/task-item/ui-task-item': RouteRecordInfo<'/story/web-core/ui/task-item/ui-task-item', '/story/web-core/ui/task-item/ui-task-item', Record<never, never>, Record<never, never>>,
|
||||
'/story/web-core/ui/task-list/ui-task-list': RouteRecordInfo<'/story/web-core/ui/task-list/ui-task-list', '/story/web-core/ui/task-list/ui-task-list', Record<never, never>, Record<never, never>>,
|
||||
'/story/web-core/ui/title/ui-title': RouteRecordInfo<'/story/web-core/ui/title/ui-title', '/story/web-core/ui/title/ui-title', Record<never, never>, Record<never, never>>,
|
||||
'/story/web-core/ui/toaster/ui-toaster': RouteRecordInfo<'/story/web-core/ui/toaster/ui-toaster', '/story/web-core/ui/toaster/ui-toaster', Record<never, never>, Record<never, never>>,
|
||||
'/story/web-core/ui/toggle/ui-toggle': RouteRecordInfo<'/story/web-core/ui/toggle/ui-toggle', '/story/web-core/ui/toggle/ui-toggle', Record<never, never>, Record<never, never>>,
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
<script lang="ts" setup>
|
||||
import VtsBackdrop from '@core/components/backdrop/VtsBackdrop.vue'
|
||||
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
|
||||
import type { Task } from '@core/components/ui/quick-task-item/UiQuickTaskItem.vue'
|
||||
import UiQuickTaskPanel from '@core/components/ui/quick-task-panel/UiQuickTaskPanel.vue'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive'
|
||||
import type { Task } from '@core/types/task.type.ts'
|
||||
import { unrefElement, watchArray, whenever } from '@vueuse/core'
|
||||
import placementJs from 'placement.js'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiLoader from '@core/components/ui/loader/UiLoader.vue'
|
||||
import UiQuickTaskItem, { type Task } from '@core/components/ui/quick-task-item/UiQuickTaskItem.vue'
|
||||
import UiQuickTaskItem from '@core/components/ui/quick-task-item/UiQuickTaskItem.vue'
|
||||
import type { Task } from '@core/types/task.type.ts'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ import VtsDivider from '@core/components/divider/VtsDivider.vue'
|
||||
import TabItem from '@core/components/tab/TabItem.vue'
|
||||
import TabList from '@core/components/tab/TabList.vue'
|
||||
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
|
||||
import type { TaskStatus } from '@core/components/ui/quick-task-item/UiQuickTaskItem.vue'
|
||||
import { useTabList } from '@core/composables/tab-list.composable'
|
||||
import type { TaskStatus } from '@core/types/task.type.ts'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export type TaskTab = TaskStatus | 'all'
|
||||
|
||||
@@ -34,22 +34,11 @@ import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
||||
import VtsQuickTaskList from '@core/components/task/VtsQuickTaskList.vue'
|
||||
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
|
||||
import UiTag from '@core/components/ui/tag/UiTag.vue'
|
||||
import type { Task } from '@core/types/task.type.ts'
|
||||
import { useToggle } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export type TaskStatus = 'pending' | 'success' | 'failure'
|
||||
|
||||
export type Task = {
|
||||
id: string | number
|
||||
name: string
|
||||
status: TaskStatus
|
||||
tag?: string
|
||||
start?: number
|
||||
end?: number
|
||||
subtasks?: Task[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
task: Task
|
||||
}>()
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<script lang="ts" setup>
|
||||
import VtsQuickTaskList from '@core/components/task/VtsQuickTaskList.vue'
|
||||
import VtsQuickTaskTabBar, { type TaskTab } from '@core/components/task/VtsQuickTaskTabBar.vue'
|
||||
import type { Task } from '@core/components/ui/quick-task-item/UiQuickTaskItem.vue'
|
||||
import { useUiStore } from '@core/stores/ui.store'
|
||||
import type { Task } from '@core/types/task.type.ts'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
<!-- v10 -->
|
||||
<template>
|
||||
<li class="ui-task-item" :data-depth="depth">
|
||||
<div class="container">
|
||||
<div class="tree-section">
|
||||
<div class="tree-lines">
|
||||
<div v-for="index in depth - 1" :key="index" class="tree-line">
|
||||
<div class="tree-line-vertical" />
|
||||
</div>
|
||||
</div>
|
||||
<UiButtonIcon
|
||||
v-if="hasSubTasks"
|
||||
v-tooltip="expanded ? t('core.close') : t('core.open')"
|
||||
accent="brand"
|
||||
:icon="expanded ? 'fa:angle-down' : 'fa:angle-right'"
|
||||
size="small"
|
||||
:target-scale="{ x: 1.5, y: 2 }"
|
||||
@click="emit('expand')"
|
||||
/>
|
||||
<div v-else class="h-space" />
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div v-if="task.name" class="content-left">
|
||||
<UiLink disabled :to="`#task-${task.id}`" size="medium">
|
||||
{{ task.name }}
|
||||
</UiLink>
|
||||
<div v-if="shouldShowInfos || hasSubTasks" class="infos">
|
||||
<UiCounter v-if="hasSubTasks" :value="subTasksCount" accent="brand" variant="secondary" size="small" />
|
||||
<UiInfo v-if="hasInfos" accent="info" />
|
||||
<UiInfo v-if="hasWarnings" accent="warning" />
|
||||
<UiInfo v-if="isError" accent="danger" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-right typo-body-regular-small">
|
||||
<span v-if="task.end">
|
||||
{{ `${t('task.ended')} ${end}` }}
|
||||
</span>
|
||||
<div class="progress">
|
||||
<UiCircleProgressBar v-if="task.progress" :accent="progressAccent" size="small" :value="task.progress" />
|
||||
</div>
|
||||
<div class="actions">
|
||||
<UiButtonIcon icon="fa:eye" size="medium" accent="brand" @click="emit('select')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="hasSubTasks && expanded">
|
||||
<UiTaskList :tasks="subTasks" :depth />
|
||||
</template>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
|
||||
import UiInfo from '@core/components/ui/info/UiInfo.vue'
|
||||
import UiLink from '@core/components/ui/link/UiLink.vue'
|
||||
import UiTaskList from '@core/components/ui/task-list/UiTaskList.vue'
|
||||
import { useTimeAgo } from '@core/composables/locale-time-ago.composable.ts'
|
||||
import { vTooltip } from '@core/directives/tooltip.directive'
|
||||
import { logicOr } from '@vueuse/math'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import UiButtonIcon from '../button-icon/UiButtonIcon.vue'
|
||||
import UiCircleProgressBar from '../circle-progress-bar/UiCircleProgressBar.vue'
|
||||
|
||||
export type Task = {
|
||||
id: string
|
||||
infos?: { data: unknown; message: string }[]
|
||||
name?: string
|
||||
progress?: number
|
||||
end?: number
|
||||
status: 'failure' | 'interrupted' | 'pending' | 'success'
|
||||
tasks?: Task[]
|
||||
warning?: { data: unknown; message: string }[]
|
||||
}
|
||||
|
||||
const { task } = defineProps<{
|
||||
task: Task
|
||||
depth: number
|
||||
expanded?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
expand: []
|
||||
select: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const subTasks = computed(() => task.tasks ?? [])
|
||||
|
||||
const subTasksCount = computed(() => subTasks.value.length)
|
||||
|
||||
const hasSubTasks = computed(() => subTasksCount.value > 0)
|
||||
|
||||
const isError = computed(() => task.status === 'failure' || task.status === 'interrupted')
|
||||
|
||||
const end = useTimeAgo(() => task.end ?? 0)
|
||||
|
||||
const hasWarnings = computed(() => task.warning && task.warning.length > 0)
|
||||
|
||||
const hasInfos = computed(() => task.infos && task.infos.length > 0)
|
||||
|
||||
const shouldShowInfos = logicOr(isError, hasWarnings, hasInfos)
|
||||
|
||||
const progressAccent = computed(() => (isError.value ? 'danger' : 'info'))
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.ui-task-item {
|
||||
&[data-depth='1']:last-child {
|
||||
border-bottom: 0.1rem solid var(--color-neutral-border);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
min-height: 4.8rem;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
clip-path: inset(0 0 0 calc(4rem * v-bind(depth - 1)));
|
||||
height: 0.1rem;
|
||||
background: var(--color-neutral-border);
|
||||
}
|
||||
|
||||
.tree-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 1.6rem;
|
||||
gap: 0.4rem;
|
||||
|
||||
.tree-lines {
|
||||
display: flex;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.tree-line {
|
||||
flex: 0 0 2.8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.tree-line-vertical {
|
||||
width: 0.1rem;
|
||||
background: var(--color-brand-txt-base);
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.h-space {
|
||||
width: 2.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: 1.6rem;
|
||||
padding: 0.4rem 1.6rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content-left {
|
||||
display: flex;
|
||||
gap: 1.6rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
color: var(--color-neutral-txt-secondary);
|
||||
word-break: break-word;
|
||||
|
||||
.infos {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.content-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
color: var(--color-neutral-txt-secondary);
|
||||
flex-shrink: 0;
|
||||
|
||||
.progress {
|
||||
display: flex;
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
.main-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.8rem;
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
.content-right {
|
||||
gap: 0.8rem;
|
||||
|
||||
.actions {
|
||||
gap: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<ul class="ui-task-list">
|
||||
<UiTaskItem
|
||||
v-for="task of tasksItems"
|
||||
:key="task.id"
|
||||
:task="task.source"
|
||||
:expanded="task.flags.expanded"
|
||||
:depth="depth + 1"
|
||||
@select="emit('select', task.id)"
|
||||
@expand="task.toggleFlag('expanded')"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import UiTaskItem from '@core/components/ui/task-item/UiTaskItem.vue'
|
||||
import { useCollection } from '@core/packages/collection'
|
||||
import type { Task } from '@core/types/task.type.ts'
|
||||
|
||||
const { tasks, depth = 0 } = defineProps<{
|
||||
tasks: Task[]
|
||||
depth?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [id: string]
|
||||
}>()
|
||||
|
||||
const { items: tasksItems } = useCollection(() => tasks, {
|
||||
flags: ['expanded'],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.ui-task-list {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -7,12 +7,7 @@
|
||||
v-bind="attrs"
|
||||
>
|
||||
<template v-if="depth > 1">
|
||||
<VtsTreeLine
|
||||
v-for="i in depth - 1"
|
||||
:key="i"
|
||||
:half-height="(!hasToggle && i === depth - 1) || !isExpanded"
|
||||
:right="i === depth - 1"
|
||||
/>
|
||||
<VtsTreeLine v-for="i in depth - 1" :key="i" />
|
||||
</template>
|
||||
<UiButtonIcon
|
||||
v-if="hasToggle"
|
||||
@@ -116,7 +111,7 @@ const depth = inject(IK_TREE_LIST_DEPTH, 0)
|
||||
|
||||
.h-line {
|
||||
width: 2rem;
|
||||
border-bottom: 0.1rem solid var(--color-brand-txt-base);
|
||||
//border-bottom: 0.1rem solid var(--color-brand-txt-base);
|
||||
margin-left: -0.4rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"boot-firmware-uefi": "The boot firmware is UEFI",
|
||||
"boot-vm": "Boot VM",
|
||||
"build-number": "Build number",
|
||||
"by": "By",
|
||||
"bytes.gi": "GiB",
|
||||
"bytes.ki": "KiB",
|
||||
"bytes.mi": "MiB",
|
||||
@@ -465,6 +466,7 @@
|
||||
"no-selected-vm-can-be-migrated": "No selected VM can be migrated",
|
||||
"no-server-detected": "No server detected",
|
||||
"no-storage-repositories-detected": "No storage repositories detected.",
|
||||
"no-tasks-detected": "No tasks detected",
|
||||
"no-tasks": "No tasks",
|
||||
"no-vdis-attached": "No VDIs attached",
|
||||
"no-vdis-detected": "No VDIs detected",
|
||||
@@ -662,6 +664,7 @@
|
||||
"table-actions": "Table actions",
|
||||
"tags": "Tags",
|
||||
"task": "Task",
|
||||
"task.ended": "Ended",
|
||||
"task.estimated-end": "Estimated end",
|
||||
"task.progress": "Progress",
|
||||
"task.started": "Started",
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"boot-firmware-uefi": "Le boot firmware est UEFI",
|
||||
"boot-vm": "Démarrer la VM",
|
||||
"build-number": "Numéro de build",
|
||||
"by": "Par",
|
||||
"bytes.gi": "Gio",
|
||||
"bytes.ki": "Kio",
|
||||
"bytes.mi": "Mio",
|
||||
@@ -466,6 +467,7 @@
|
||||
"no-server-detected": "Aucun serveur détecté",
|
||||
"no-storage-repositories-detected": "Aucun dépôt de stockage détecté.",
|
||||
"no-tasks": "Aucune tâche",
|
||||
"no-tasks-detected": "Aucune tâche détectée",
|
||||
"no-vdis-attached": "Aucun VDI attaché",
|
||||
"no-vdis-detected": "Aucun VDI détecté",
|
||||
"no-vif-detected": "Aucun VIF détecté",
|
||||
@@ -662,9 +664,10 @@
|
||||
"table-actions": "Actions du tableau",
|
||||
"tags": "Tags",
|
||||
"task": "Tâche",
|
||||
"task.ended": "Terminée",
|
||||
"task.estimated-end": "Fin estimé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",
|
||||
|
||||
15
@xen-orchestra/web-core/lib/types/task.type.ts
Normal file
15
@xen-orchestra/web-core/lib/types/task.type.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type TaskStatus = 'failure' | 'interrupted' | 'pending' | 'success'
|
||||
|
||||
export type Task = {
|
||||
id: string
|
||||
infos?: { data: unknown; message: string }[]
|
||||
name?: string
|
||||
progress?: number
|
||||
tag?: string
|
||||
userName?: string
|
||||
start?: number
|
||||
end?: number
|
||||
status: TaskStatus
|
||||
subtasks?: Task[]
|
||||
warning?: { data: unknown; message: string }[]
|
||||
}
|
||||
@@ -18,7 +18,11 @@
|
||||
{{ t('backups') }}
|
||||
</TabItem>
|
||||
</RouterLink>
|
||||
<TabItem disabled>{{ t('tasks') }}</TabItem>
|
||||
<RouterLink v-slot="{ isExactActive, href }" to="/tasks" custom>
|
||||
<TabItem :active="isExactActive" :href tag="a">
|
||||
{{ t('tasks') }}
|
||||
</TabItem>
|
||||
</RouterLink>
|
||||
<RouterLink v-slot="{ isExactActive, href }" to="/pools" custom>
|
||||
<TabItem :active="isExactActive" :href tag="a">{{ t('pools') }}</TabItem>
|
||||
</RouterLink>
|
||||
|
||||
78
@xen-orchestra/web/src/components/site/tasks/TasksList.vue
Normal file
78
@xen-orchestra/web/src/components/site/tasks/TasksList.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div class="tasks-list">
|
||||
<div class="container">
|
||||
<div class="actions">
|
||||
<UiQuerySearchBar @search="value => (searchQuery = value)" />
|
||||
<UiTopBottomTable :selected-items="0" :total-items="0">
|
||||
<UiTablePagination v-bind="paginationBindings" />
|
||||
</UiTopBottomTable>
|
||||
</div>
|
||||
<VtsStateHero v-if="hasError" format="table" type="error" size="small" no-background>
|
||||
{{ t('error-no-data') }}
|
||||
</VtsStateHero>
|
||||
<VtsStateHero v-else-if="tasks.length === 0" format="table" type="no-data" size="small">
|
||||
{{ t('no-tasks-detected') }}
|
||||
</VtsStateHero>
|
||||
<UiTaskList v-else :tasks="tasksRecords" />
|
||||
<VtsStateHero v-if="searchQuery && filteredTasks.length === 0" format="table" type="no-result" size="small">
|
||||
{{ t('no-result') }}
|
||||
</VtsStateHero>
|
||||
<UiTopBottomTable :selected-items="0" :total-items="0">
|
||||
<UiTablePagination v-bind="paginationBindings" />
|
||||
</UiTopBottomTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Task } from '@core/types/task.type.ts'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import UiQuerySearchBar from '@core/components/ui/query-search-bar/UiQuerySearchBar.vue'
|
||||
import UiTablePagination from '@core/components/ui/table-pagination/UiTablePagination.vue'
|
||||
import UiTaskList from '@core/components/ui/task-list/UiTaskList.vue'
|
||||
import UiTopBottomTable from '@core/components/ui/top-bottom-table/UiTopBottomTable.vue'
|
||||
import { usePagination } from '@core/composables/pagination.composable'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { tasks } = defineProps<{
|
||||
tasks: Task[]
|
||||
hasError: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// const selectedTaskId = useRouteQuery('id')
|
||||
|
||||
const searchQuery = ref('')
|
||||
|
||||
const filteredTasks = computed(() => {
|
||||
const searchTerm = searchQuery.value.trim().toLocaleLowerCase()
|
||||
|
||||
if (!searchTerm) {
|
||||
return tasks
|
||||
}
|
||||
|
||||
return tasks.filter(task => Object.values(task).some(value => String(value).toLocaleLowerCase().includes(searchTerm)))
|
||||
})
|
||||
|
||||
const { pageRecords: tasksRecords, paginationBindings } = usePagination('tasks', filteredTasks)
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.tasks-list,
|
||||
.actions,
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tasks-list {
|
||||
gap: 2.4rem;
|
||||
|
||||
.container,
|
||||
.actions {
|
||||
gap: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
55
@xen-orchestra/web/src/pages/(site)/tasks.vue
Normal file
55
@xen-orchestra/web/src/pages/(site)/tasks.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="tasks" :class="{ mobile: uiStore.isMobile }">
|
||||
<UiCard class="container">
|
||||
<VtsStateHero v-if="areTasksFetching" busy format="card" size="medium" />
|
||||
<TasksList v-else :tasks="convertedTasks" :has-error="hasTaskFetchError" />
|
||||
</UiCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TasksList from '@/components/site/tasks/TasksList.vue'
|
||||
import { useXoTaskCollection } from '@/remote-resources/use-xo-task-collection.ts'
|
||||
import { useXoUserCollection } from '@/remote-resources/use-xo-user-collections.ts'
|
||||
import { convertTaskToCore } from '@/utils/convert-task-to-core.util.ts'
|
||||
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
|
||||
import UiCard from '@core/components/ui/card/UiCard.vue'
|
||||
import { useUiStore } from '@core/stores/ui.store'
|
||||
import type { XoUser } from '@vates/types'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const uiStore = useUiStore()
|
||||
|
||||
const { tasks, hasTaskFetchError, areTasksFetching } = useXoTaskCollection()
|
||||
const { getUserById } = useXoUserCollection()
|
||||
|
||||
const convertedTasks = computed(() =>
|
||||
tasks.value.map(task => {
|
||||
const userId = task.properties.userId
|
||||
|
||||
if (!userId) {
|
||||
return convertTaskToCore(task)
|
||||
}
|
||||
|
||||
const user = getUserById(userId as XoUser['id'])
|
||||
|
||||
// TODO , just put username when it is available in endpoint
|
||||
return convertTaskToCore(task, user?.name ? user?.name : user?.email)
|
||||
})
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
.tasks {
|
||||
&:not(.mobile) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.container {
|
||||
height: fit-content;
|
||||
gap: 4rem;
|
||||
margin: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
|
||||
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
|
||||
import type { XoUser } from '@vates/types'
|
||||
|
||||
export const useXoUserCollection = defineRemoteResource({
|
||||
url: `/rest/v0/users?fields=id,name,permissions,id,email,groups`,
|
||||
initialData: () => [] as XoUser[],
|
||||
state: (users, context) => {
|
||||
return useXoCollectionState(users, {
|
||||
context,
|
||||
baseName: 'user',
|
||||
})
|
||||
},
|
||||
})
|
||||
0
@xen-orchestra/web/src/types/xo/task.type.ts
Normal file
0
@xen-orchestra/web/src/types/xo/task.type.ts
Normal file
@@ -1,12 +1,14 @@
|
||||
import type { Task as CoreTask } from '@core/components/ui/quick-task-item/UiQuickTaskItem.vue'
|
||||
import type { Task } from '@core/types/task.type.ts'
|
||||
import type { XoTask } from '@vates/types'
|
||||
|
||||
export const convertTaskToCore = (task: XoTask): CoreTask => ({
|
||||
export const convertTaskToCore = (task: XoTask, userName?: string): Task => ({
|
||||
id: task.id,
|
||||
status: task.status === 'interrupted' ? 'failure' : task.status,
|
||||
name: task.properties.name ?? '',
|
||||
tag: task.properties.type,
|
||||
progress: typeof task.properties.progress === 'number' ? task.properties.progress : 0,
|
||||
userName,
|
||||
start: task.start,
|
||||
end: task.end,
|
||||
subtasks: task.tasks !== undefined ? task.tasks.map(subtask => convertTaskToCore(subtask)) : undefined,
|
||||
subtasks: task.tasks?.map(subtask => convertTaskToCore(subtask, userName)),
|
||||
})
|
||||
|
||||
1
@xen-orchestra/web/typed-router.d.ts
vendored
1
@xen-orchestra/web/typed-router.d.ts
vendored
@@ -23,6 +23,7 @@ declare module 'vue-router/auto-routes' {
|
||||
'/(site)/dashboard': RouteRecordInfo<'/(site)/dashboard', '/dashboard', Record<never, never>, Record<never, never>>,
|
||||
'/(site)/hosts': RouteRecordInfo<'/(site)/hosts', '/hosts', Record<never, never>, Record<never, never>>,
|
||||
'/(site)/pools': RouteRecordInfo<'/(site)/pools', '/pools', Record<never, never>, Record<never, never>>,
|
||||
'/(site)/tasks': RouteRecordInfo<'/(site)/tasks', '/tasks', Record<never, never>, Record<never, never>>,
|
||||
'/(site)/vms': RouteRecordInfo<'/(site)/vms', '/vms', Record<never, never>, Record<never, never>>,
|
||||
'/[...path]': RouteRecordInfo<'/[...path]', '/:path(.*)', { path: ParamValue<true> }, { path: ParamValue<false> }>,
|
||||
'/backup/[id]': RouteRecordInfo<'/backup/[id]', '/backup/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
|
||||
|
||||
Reference in New Issue
Block a user