@xen-orchestra/web-core 0.56.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/lib/assets/css/base.pcss +5 -0
  2. package/lib/components/input-wrapper/VtsInputWrapper.vue +2 -2
  3. package/lib/components/layout/VtsLayoutSidebar.vue +116 -43
  4. package/lib/components/menu/MenuItem.vue +9 -1
  5. package/lib/components/menu/MenuList.vue +4 -2
  6. package/lib/components/menu/VtsActionsMenu.vue +24 -24
  7. package/lib/components/modal/VtsActionModal.vue +44 -9
  8. package/lib/components/modal/VtsBlockedModal.vue +1 -1
  9. package/lib/components/modal/VtsDeleteModal.vue +1 -1
  10. package/lib/components/overlay/VtsOverlayButton.vue +35 -0
  11. package/lib/components/overlay/VtsOverlayCancelButton.vue +16 -0
  12. package/lib/components/overlay/VtsOverlayConfirmButton.vue +25 -0
  13. package/lib/components/overlay/VtsOverlayList.vue +82 -0
  14. package/lib/components/progress-bar/VtsProgressBar.vue +5 -3
  15. package/lib/components/select/VtsSelect.vue +0 -3
  16. package/lib/components/space-card/VtsSpaceCard.vue +1 -1
  17. package/lib/components/table/cells/VtsProgressBarCell.vue +1 -1
  18. package/lib/components/table/cells/VtsUserNameCell.vue +29 -0
  19. package/lib/components/task/VtsQuickTaskButton.vue +1 -1
  20. package/lib/components/task/VtsQuickTaskList.vue +1 -1
  21. package/lib/components/tree/VtsTreeItem.vue +4 -4
  22. package/lib/components/ui/head-bar/UiHeadBar.vue +1 -1
  23. package/lib/components/ui/input/UiInput.vue +53 -9
  24. package/lib/components/ui/query-search-bar/UiQuerySearchBar.vue +1 -15
  25. package/lib/components/ui/quick-task-item/UiQuickTaskItem.vue +1 -1
  26. package/lib/components/ui/quick-task-panel/UiQuickTaskPanel.vue +1 -1
  27. package/lib/components/ui/task-item/UiTaskItem.vue +14 -6
  28. package/lib/components/ui/task-list/UiTaskList.vue +1 -1
  29. package/lib/components/ui/text-area/UiTextarea.vue +12 -14
  30. package/lib/components/vif-connection-toggle-modal/VtsVifConnectionToggleModal.vue +43 -0
  31. package/lib/composables/table/multi-select.composable.md +33 -0
  32. package/lib/layouts/CoreLayout.vue +74 -62
  33. package/lib/locales/en.json +63 -2
  34. package/lib/locales/fr.json +63 -2
  35. package/lib/packages/form-validation/custom-rules/ip-addresses.rule.ts +15 -0
  36. package/lib/packages/form-validation/custom-rules/ip.regex.ts +26 -0
  37. package/lib/packages/form-validation/custom-rules/ipv4-or-cidr.rule.ts +2 -3
  38. package/lib/packages/form-validation/index.ts +2 -0
  39. package/lib/packages/hide-permanently/README.md +60 -0
  40. package/lib/packages/hide-permanently/types.ts +3 -0
  41. package/lib/packages/hide-permanently/use-hide-permanently.ts +15 -0
  42. package/lib/packages/overlay/OverlayComponent.vue +18 -0
  43. package/lib/packages/overlay/README.md +285 -0
  44. package/lib/packages/overlay/create-event-handler.ts +80 -0
  45. package/lib/packages/overlay/injection-keys.ts +3 -0
  46. package/lib/packages/overlay/is-thenable.ts +3 -0
  47. package/lib/packages/overlay/symbols.ts +5 -0
  48. package/lib/packages/overlay/types.ts +94 -0
  49. package/lib/packages/overlay/use-overlay-escape.ts +34 -0
  50. package/lib/packages/overlay/use-overlay-store.ts +33 -0
  51. package/lib/packages/overlay/use-overlay-trigger.ts +33 -0
  52. package/lib/packages/overlay/use-overlay.ts +99 -0
  53. package/lib/packages/progress/use-progress.ts +12 -2
  54. package/lib/packages/sidebar/index.ts +2 -0
  55. package/lib/packages/sidebar/sidebar.store.ts +55 -0
  56. package/lib/packages/sidebar/types.ts +21 -0
  57. package/lib/packages/sidebar/use-sidebar-resize.ts +47 -0
  58. package/lib/packages/sidebar/use-sidebar-responsive-expand.ts +19 -0
  59. package/lib/tables/column-definitions/user-name-column.ts +10 -0
  60. package/lib/tables/column-sets/server-columns.ts +2 -2
  61. package/lib/tables/column-sets/user-columns.ts +16 -0
  62. package/lib/types/object.type.ts +9 -1
  63. package/lib/types/task.type.ts +5 -12
  64. package/lib/utils/injection-keys.util.ts +3 -1
  65. package/lib/utils/ip-address.utils.ts +7 -0
  66. package/lib/utils/sr.utils.ts +4 -0
  67. package/package.json +2 -1
  68. package/lib/stores/sidebar.store.ts +0 -79
@@ -2,16 +2,26 @@ import { toComputed } from '@core/utils/to-computed.util.ts'
2
2
  import type { Progress } from './types.ts'
3
3
  import { computed, type MaybeRefOrGetter } from 'vue'
4
4
 
5
- export function useProgress(rawCurrent: MaybeRefOrGetter<number>, rawTotal: MaybeRefOrGetter<number>): Progress {
5
+ export function useProgress(
6
+ rawCurrent: MaybeRefOrGetter<number>,
7
+ rawTotal: MaybeRefOrGetter<number>,
8
+ rawNoRuler: MaybeRefOrGetter<boolean> = false
9
+ ): Progress {
6
10
  const current = toComputed(rawCurrent)
7
11
 
8
12
  const total = toComputed(rawTotal)
9
13
 
14
+ const noRuler = toComputed(rawNoRuler)
15
+
10
16
  const percentage = computed(() => (total.value === 0 ? 0 : (current.value / total.value) * 100))
11
17
 
12
18
  const percentageCap = computed(() => Math.max(100, Math.ceil(percentage.value / 100) * 100))
13
19
 
14
- const fillWidth = computed(() => `${(percentage.value / percentageCap.value) * 100}%`)
20
+ const fillWidth = computed(() => {
21
+ const max = noRuler.value ? 100 : percentageCap.value
22
+
23
+ return `${(Math.min(percentage.value, max) / max) * 100}%`
24
+ })
15
25
 
16
26
  return {
17
27
  current,
@@ -0,0 +1,2 @@
1
+ export { useLeftSidebarStore, useRightSidebarStore, useSidebar } from '@core/packages/sidebar/sidebar.store.ts'
2
+ export { SIDES, SIDEBAR_DEFAULTS, type SidebarSide, type SidebarState } from '@core/packages/sidebar/types.ts'
@@ -0,0 +1,55 @@
1
+ import { SIDEBAR_DEFAULTS, type SidebarSide } from '@core/packages/sidebar/types.ts'
2
+ import { useSidebarResize } from '@core/packages/sidebar/use-sidebar-resize.ts'
3
+ import { useSidebarResponsiveExpand } from '@core/packages/sidebar/use-sidebar-responsive-expand.ts'
4
+ import { useUiStore } from '@core/stores/ui.store.ts'
5
+ import { useLocalStorage, useToggle } from '@vueuse/core'
6
+ import { defineStore } from 'pinia'
7
+ import { computed } from 'vue'
8
+
9
+ function createSidebar(side: SidebarSide) {
10
+ const uiStore = useUiStore()
11
+ const prefix = `sidebar.${side}`
12
+ const defaults = SIDEBAR_DEFAULTS[side]
13
+
14
+ const isExpanded = useLocalStorage(`${prefix}.expanded`, defaults.isExpanded)
15
+ const isLocked = useLocalStorage(`${prefix}.locked`, defaults.isLocked)
16
+ const width = useLocalStorage(`${prefix}.width`, defaults.width)
17
+
18
+ const toggleExpand = useToggle(isExpanded)
19
+ const toggleLock = useToggle(isLocked)
20
+ const { startResize, isResizing } = useSidebarResize(side, width)
21
+
22
+ useSidebarResponsiveExpand(isExpanded)
23
+
24
+ const cssWidth = computed(() => (uiStore.isSmall ? '100%' : `${width.value}px`))
25
+
26
+ const cssLockedOffset = computed(() => (isExpanded.value ? '0' : `-${cssWidth.value}`))
27
+
28
+ const cssUnlockedOffset = computed(() => {
29
+ if (isExpanded.value) {
30
+ return '0'
31
+ }
32
+
33
+ return side === 'left' ? `-${cssWidth.value}` : cssWidth.value
34
+ })
35
+
36
+ return {
37
+ isExpanded,
38
+ isLocked,
39
+ width,
40
+ toggleExpand,
41
+ toggleLock,
42
+ startResize,
43
+ isResizing,
44
+ cssWidth,
45
+ cssLockedOffset,
46
+ cssUnlockedOffset,
47
+ }
48
+ }
49
+
50
+ export const useLeftSidebarStore = defineStore('sidebar-left', () => createSidebar('left'))
51
+ export const useRightSidebarStore = defineStore('sidebar-right', () => createSidebar('right'))
52
+
53
+ export function useSidebar(side: SidebarSide = 'left') {
54
+ return side === 'left' ? useLeftSidebarStore() : useRightSidebarStore()
55
+ }
@@ -0,0 +1,21 @@
1
+ export const SIDES = ['left', 'right'] as const
2
+
3
+ export type SidebarSide = (typeof SIDES)[number]
4
+
5
+ export type SidebarState = {
6
+ isExpanded: boolean
7
+ isLocked: boolean
8
+ width: number
9
+ toggleExpand: (value?: boolean) => boolean
10
+ toggleLock: (value?: boolean) => boolean
11
+ startResize: (event: MouseEvent) => void
12
+ isResizing: boolean
13
+ cssWidth: string
14
+ cssLockedOffset: string | number
15
+ cssUnlockedOffset: string | number
16
+ }
17
+
18
+ export const SIDEBAR_DEFAULTS = {
19
+ left: { isExpanded: true, isLocked: true, width: 350 },
20
+ right: { isExpanded: false, isLocked: false, width: 350 },
21
+ } satisfies Record<SidebarSide, Pick<SidebarState, 'isExpanded' | 'isLocked' | 'width'>>
@@ -0,0 +1,47 @@
1
+ import type { SidebarSide } from '@core/packages/sidebar/types.ts'
2
+ import { ifElse } from '@core/utils/if-else.utils.ts'
3
+ import { useRafFn, useStyleTag } from '@vueuse/core'
4
+ import { ref, type Ref } from 'vue'
5
+
6
+ export function useSidebarResize(side: SidebarSide, width: Ref<number>) {
7
+ const isResizing = ref(false)
8
+
9
+ let initialX: number
10
+ let initialWidth: number
11
+ let clientX: number
12
+
13
+ function startResize(event: MouseEvent) {
14
+ clientX = event.clientX
15
+ initialX = clientX
16
+ initialWidth = width.value
17
+ isResizing.value = true
18
+ document.addEventListener('mousemove', handleMouseMove)
19
+ document.addEventListener('mouseup', handleMouseUp)
20
+ }
21
+
22
+ function handleMouseMove(event: MouseEvent) {
23
+ clientX = event.clientX
24
+ }
25
+
26
+ function handleMouseUp() {
27
+ isResizing.value = false
28
+ document.removeEventListener('mousemove', handleMouseMove)
29
+ document.removeEventListener('mouseup', handleMouseUp)
30
+ }
31
+
32
+ const { load, unload } = useStyleTag('body { cursor: col-resize !important; }', { immediate: false })
33
+
34
+ const resizeDelta = () => (side === 'left' ? initialWidth + clientX - initialX : initialWidth + initialX - clientX)
35
+
36
+ const { pause, resume } = useRafFn(
37
+ () => (width.value = Math.min(500, window.innerWidth * 0.3, Math.max(200, resizeDelta()))),
38
+ { immediate: false }
39
+ )
40
+
41
+ ifElse(isResizing, [load, resume], [pause, unload])
42
+
43
+ return {
44
+ startResize,
45
+ isResizing,
46
+ }
47
+ }
@@ -0,0 +1,19 @@
1
+ import { useUiStore } from '@core/stores/ui.store.ts'
2
+ import { watch, type Ref } from 'vue'
3
+
4
+ export function useSidebarResponsiveExpand(isExpanded: Ref<boolean>) {
5
+ const uiStore = useUiStore()
6
+
7
+ let desktopExpanded = isExpanded.value
8
+
9
+ watch(
10
+ () => uiStore.isSmall,
11
+ (isSmall, wasSmall) => {
12
+ if (isSmall && !wasSmall) {
13
+ desktopExpanded = isExpanded.value
14
+ } else if (!isSmall && wasSmall) {
15
+ isExpanded.value = desktopExpanded
16
+ }
17
+ }
18
+ )
19
+ }
@@ -0,0 +1,10 @@
1
+ import VtsUserNameCell from '@core/components/table/cells/VtsUserNameCell.vue'
2
+ import { defineColumn } from '@core/packages/table/define-column.ts'
3
+ import { renderHeadCell } from '@core/tables/helpers/render-head-cell.ts'
4
+ import type { HeaderConfig } from '@core/tables/types.ts'
5
+ import { h } from 'vue'
6
+
7
+ export const useUserNameColumn = defineColumn((config?: HeaderConfig) => ({
8
+ renderHead: () => renderHeadCell(config?.headerLabel),
9
+ renderBody: (content: string, props?: { href?: string }) => h(VtsUserNameCell, { content, href: props?.href }),
10
+ }))
@@ -1,7 +1,7 @@
1
1
  import { defineColumns } from '@core/packages/table/define-columns.ts'
2
+ import { useActionColumn } from '@core/tables/column-definitions/action-column.ts'
2
3
  import { useAddressColumn } from '@core/tables/column-definitions/address-column.ts'
3
4
  import { useLinkColumn } from '@core/tables/column-definitions/link-column'
4
- import { useSelectItemColumn } from '@core/tables/column-definitions/select-item-column'
5
5
  import { useStatusColumn } from '@core/tables/column-definitions/status-column.ts'
6
6
  import { useI18n } from 'vue-i18n'
7
7
 
@@ -13,6 +13,6 @@ export const useServerColumns = defineColumns(() => {
13
13
  hostIp: useAddressColumn({ headerLabel: () => t('ip-address') }),
14
14
  status: useStatusColumn({ headerLabel: () => t('status') }),
15
15
  primaryHost: useLinkColumn({ headerLabel: () => t('master') }),
16
- selectItem: useSelectItemColumn(),
16
+ actions: useActionColumn({}),
17
17
  }
18
18
  })
@@ -0,0 +1,16 @@
1
+ import { defineColumns } from '@core/packages/table/define-columns.ts'
2
+ import { useSelectItemColumn } from '@core/tables/column-definitions/select-item-column.ts'
3
+ import { useTruncatedTextColumn } from '@core/tables/column-definitions/truncated-text-column.ts'
4
+ import { useUserNameColumn } from '@core/tables/column-definitions/user-name-column.ts'
5
+ import { useI18n } from 'vue-i18n'
6
+
7
+ export const useUserColumns = defineColumns(() => {
8
+ const { t } = useI18n()
9
+
10
+ return {
11
+ username: useUserNameColumn({ headerLabel: () => t('username') }),
12
+ email: useTruncatedTextColumn({ headerLabel: () => t('email') }),
13
+ providers: useTruncatedTextColumn({ headerLabel: () => t('providers') }),
14
+ selectItem: useSelectItemColumn(),
15
+ }
16
+ })
@@ -1,5 +1,8 @@
1
- export type ObjectType = 'vm'
1
+ export type ObjectType = 'vm' | 'host'
2
+
2
3
  export type VmActions = 'reboot' | 'shutdown' | 'force-reboot' | 'force-shutdown'
4
+ export type HostActions = 'enable' | 'disable'
5
+
3
6
  export type VmBlockedOperations =
4
7
  | 'clean_shutdown'
5
8
  | 'hard_shutdown'
@@ -8,3 +11,8 @@ export type VmBlockedOperations =
8
11
  | 'hard_reboot'
9
12
  | 'suspend'
10
13
  | 'destroy'
14
+
15
+ export type ActionsByObject = {
16
+ vm: VmActions
17
+ host: HostActions
18
+ }
@@ -1,15 +1,8 @@
1
+ import type { RouteLocationRaw } from 'vue-router'
2
+
1
3
  export type TaskStatus = 'failure' | 'interrupted' | 'pending' | 'success'
2
4
 
3
- export type Task = {
4
- id: string
5
- infos?: { data: unknown; message: string }[]
6
- name?: string
7
- progress?: number
8
- tag?: string
9
- userName?: string
10
- start?: number
11
- end?: number
12
- status: TaskStatus
13
- subtasks?: Task[]
14
- warnings?: { data: unknown; message: string }[]
5
+ export type TaskObjectSegment = {
6
+ text: string
7
+ to?: RouteLocationRaw
15
8
  }
@@ -1,6 +1,6 @@
1
1
  import type { InputWrapperController } from '@core/components/input-wrapper/VtsInputWrapper.vue'
2
2
  import type { ModalAccent } from '@core/components/ui/modal/UiModal.vue'
3
- import type { ValueFormatter } from '@core/types/chart'
3
+ import type { ValueFormatter } from '@core/types/chart.ts'
4
4
  import type { ComputedRef, InjectionKey, Ref } from 'vue'
5
5
 
6
6
  export const IK_CHART_VALUE_FORMATTER = Symbol('IK_CHART_VALUE_FORMATTER') as InjectionKey<ComputedRef<ValueFormatter>>
@@ -24,3 +24,5 @@ export const IK_DISABLED = Symbol('IK_DISABLED') as InjectionKey<ComputedRef<boo
24
24
  export const IK_INPUT_WRAPPER_CONTROLLER = Symbol('IK_INPUT_WRAPPER_CONTROLLER') as InjectionKey<InputWrapperController>
25
25
 
26
26
  export const IK_MODAL_ACCENT = Symbol('IK_MODAL_ACCENT') as InjectionKey<ComputedRef<ModalAccent>>
27
+
28
+ export const IK_OVERLAY_ACCENT = Symbol('IK_OVERLAY_ACCENT') as InjectionKey<ComputedRef<ModalAccent>>
@@ -16,3 +16,10 @@ export const getUniqueIpAddressesForDevice = (
16
16
 
17
17
  return [...new Set(getIpAddressesByDevice(addresses)[device])]
18
18
  }
19
+
20
+ export function parseIpList(value: string, separator = ';'): string[] {
21
+ return value
22
+ .split(separator)
23
+ .map(ip => ip.trim())
24
+ .filter(ip => ip !== '')
25
+ }
@@ -32,3 +32,7 @@ export function getSrModalInfoVariant(scope: SrScope, accessMode: SrAccessMode):
32
32
 
33
33
  return 'pool-shared'
34
34
  }
35
+
36
+ export function shouldShowTargetCount(scope: SrScope, targetCount: number) {
37
+ return targetCount > (scope.type === SR_SCOPE_TYPE.POOL ? 0 : 1)
38
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xen-orchestra/web-core",
3
3
  "type": "module",
4
- "version": "0.56.0",
4
+ "version": "0.57.0",
5
5
  "private": false,
6
6
  "exports": {
7
7
  "./*": {
@@ -34,6 +34,7 @@
34
34
  "ndjson-readablestream": "^1.3.0",
35
35
  "placement.js": "^1.0.0-beta.5",
36
36
  "simple-icons": "^14.14.0",
37
+ "vue-component-type-helpers": "~3.3.7",
37
38
  "vue-echarts": "^6.6.8",
38
39
  "vue-virtual-scroller": "^2.0.0-beta.8"
39
40
  },
@@ -1,79 +0,0 @@
1
- import { useUiStore } from '@core/stores/ui.store'
2
- import { ifElse } from '@core/utils/if-else.utils'
3
- import { useLocalStorage, useRafFn, useStyleTag, useToggle } from '@vueuse/core'
4
- import { defineStore } from 'pinia'
5
- import { computed, ref, watch } from 'vue'
6
-
7
- export const useSidebarStore = defineStore('layout', () => {
8
- const uiStore = useUiStore()
9
- const isExpanded = useLocalStorage('sidebar.expanded', true)
10
- const isLocked = useLocalStorage('sidebar.locked', true)
11
- const width = useLocalStorage('sidebar.width', 350)
12
- const toggleExpand = useToggle(isExpanded)
13
- const toggleLock = useToggle(isLocked)
14
- const isResizing = ref(false)
15
- let desktopState = false
16
-
17
- let initialX: number
18
- let initialWidth: number
19
- let clientX: number
20
-
21
- function startResize(event: MouseEvent) {
22
- clientX = event.clientX
23
- initialX = clientX
24
- initialWidth = width.value
25
- isResizing.value = true
26
- document.addEventListener('mousemove', handleMouseMove)
27
- document.addEventListener('mouseup', handleMouseUp)
28
- }
29
-
30
- function handleMouseMove(event: MouseEvent) {
31
- clientX = event.clientX
32
- }
33
-
34
- function handleMouseUp() {
35
- isResizing.value = false
36
- document.removeEventListener('mousemove', handleMouseMove)
37
- document.removeEventListener('mouseup', handleMouseUp)
38
- }
39
-
40
- const cssWidth = computed(() => (uiStore.isSmall ? '100%' : `${width.value}px`))
41
-
42
- const cssOffset = computed(() => (isExpanded.value ? 0 : `-${cssWidth.value}`))
43
-
44
- const { load, unload } = useStyleTag('body { cursor: col-resize !important; }', { immediate: false })
45
-
46
- const { pause, resume } = useRafFn(
47
- () => (width.value = Math.min(500, Math.max(200, initialWidth + clientX - initialX))),
48
- { immediate: false }
49
- )
50
-
51
- ifElse(isResizing, [load, resume], [pause, unload])
52
-
53
- watch(
54
- () => ({
55
- isSmall: uiStore.isSmall,
56
- isLarge: uiStore.isLarge,
57
- }),
58
- ({ isSmall, isLarge }) => {
59
- if (isSmall) {
60
- desktopState = isExpanded.value
61
- } else {
62
- isExpanded.value = isLarge ? true : desktopState
63
- }
64
- },
65
- { immediate: true }
66
- )
67
-
68
- return {
69
- isExpanded,
70
- isLocked,
71
- width,
72
- toggleExpand,
73
- toggleLock,
74
- startResize,
75
- isResizing,
76
- cssWidth,
77
- cssOffset,
78
- }
79
- })