@xen-orchestra/web-core 0.55.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 (111) hide show
  1. package/lib/assets/css/base.pcss +5 -0
  2. package/lib/components/card-object-title/VtsCardObjectTitle.vue +36 -0
  3. package/lib/components/code-snippet/VtsCodeSnippet.vue +11 -5
  4. package/lib/components/drawer/VtsDrawer.vue +5 -1
  5. package/lib/components/drawer/VtsDrawerButton.vue +4 -3
  6. package/lib/components/drawer/VtsDrawerConfirmButton.vue +4 -2
  7. package/lib/components/icon/VtsIcon.vue +3 -1
  8. package/lib/components/input-wrapper/VtsInputWrapper.vue +2 -2
  9. package/lib/components/layout/VtsContentSidePanel.vue +41 -0
  10. package/lib/components/layout/VtsLayoutSidebar.vue +116 -43
  11. package/lib/components/menu/MenuItem.vue +9 -1
  12. package/lib/components/menu/MenuList.vue +4 -2
  13. package/lib/components/menu/VtsActionsMenu.vue +48 -14
  14. package/lib/components/modal/VtsActionModal.vue +44 -9
  15. package/lib/components/modal/VtsBlockedModal.vue +1 -1
  16. package/lib/components/modal/VtsDeleteModal.vue +1 -1
  17. package/lib/components/operation-card/VtsOperationCard.vue +2 -1
  18. package/lib/components/overlay/VtsOverlayButton.vue +35 -0
  19. package/lib/components/overlay/VtsOverlayCancelButton.vue +16 -0
  20. package/lib/components/overlay/VtsOverlayConfirmButton.vue +25 -0
  21. package/lib/components/overlay/VtsOverlayList.vue +82 -0
  22. package/lib/components/panel/VtsPanel.vue +80 -0
  23. package/lib/components/panel/VtsSidePanel.vue +140 -0
  24. package/lib/components/progress-bar/VtsProgressBar.vue +5 -3
  25. package/lib/components/select/VtsSelect.vue +1 -3
  26. package/lib/components/space-card/VtsSpaceCard.vue +1 -1
  27. package/lib/components/state-hero/VtsStateHero.vue +9 -17
  28. package/lib/components/table/VtsTable.vue +2 -1
  29. package/lib/components/table/cells/VtsActionCell.vue +7 -0
  30. package/lib/components/table/cells/VtsLinkCell.vue +3 -1
  31. package/lib/components/table/cells/VtsProgressBarCell.vue +1 -1
  32. package/lib/components/table/cells/VtsUserNameCell.vue +29 -0
  33. package/lib/components/task/VtsQuickTaskButton.vue +1 -1
  34. package/lib/components/task/VtsQuickTaskList.vue +1 -1
  35. package/lib/components/tree/VtsTreeItem.vue +13 -7
  36. package/lib/components/tree/VtsTreeList.vue +6 -3
  37. package/lib/components/tree/VtsTreeLoadingItem.vue +3 -3
  38. package/lib/components/ui/head-bar/UiHeadBar.vue +2 -2
  39. package/lib/components/ui/input/UiInput.vue +56 -10
  40. package/lib/components/ui/link/UiLink.vue +3 -1
  41. package/lib/components/ui/panel/UiPanel.vue +7 -24
  42. package/lib/components/ui/query-search-bar/UiQuerySearchBar.vue +1 -15
  43. package/lib/components/ui/quick-task-item/UiQuickTaskItem.vue +1 -1
  44. package/lib/components/ui/quick-task-panel/UiQuickTaskPanel.vue +1 -1
  45. package/lib/components/ui/task-item/UiTaskItem.vue +14 -6
  46. package/lib/components/ui/task-list/UiTaskList.vue +1 -1
  47. package/lib/components/ui/text-area/UiTextarea.vue +12 -14
  48. package/lib/components/ui/tree-item-label/UiTreeItemLabel.vue +1 -1
  49. package/lib/components/vif-connection-toggle-modal/VtsVifConnectionToggleModal.vue +43 -0
  50. package/lib/composables/table/multi-select.composable.md +33 -0
  51. package/lib/composables/table-state.composable.ts +1 -1
  52. package/lib/icons/action-icons.ts +14 -0
  53. package/lib/layouts/CoreLayout.vue +74 -62
  54. package/lib/locales/cs.json +56 -1
  55. package/lib/locales/de.json +3 -2
  56. package/lib/locales/en.json +85 -4
  57. package/lib/locales/es.json +11 -1
  58. package/lib/locales/fa.json +0 -1
  59. package/lib/locales/fr.json +85 -4
  60. package/lib/locales/it.json +0 -1
  61. package/lib/locales/ko.json +2 -0
  62. package/lib/locales/nl.json +2 -1
  63. package/lib/locales/pt-BR.json +0 -1
  64. package/lib/locales/pt.json +0 -1
  65. package/lib/locales/ru.json +0 -1
  66. package/lib/locales/sk.json +56 -1
  67. package/lib/locales/sv.json +3 -2
  68. package/lib/locales/uk.json +0 -1
  69. package/lib/locales/zh-Hans.json +79 -4
  70. package/lib/packages/form-validation/custom-rules/ip-addresses.rule.ts +15 -0
  71. package/lib/packages/form-validation/custom-rules/ip.regex.ts +26 -0
  72. package/lib/packages/form-validation/custom-rules/ipv4-or-cidr.rule.ts +2 -3
  73. package/lib/packages/form-validation/index.ts +2 -0
  74. package/lib/packages/hide-permanently/README.md +60 -0
  75. package/lib/packages/hide-permanently/types.ts +3 -0
  76. package/lib/packages/hide-permanently/use-hide-permanently.ts +15 -0
  77. package/lib/packages/overlay/OverlayComponent.vue +18 -0
  78. package/lib/packages/overlay/README.md +285 -0
  79. package/lib/packages/overlay/create-event-handler.ts +80 -0
  80. package/lib/packages/overlay/injection-keys.ts +3 -0
  81. package/lib/packages/overlay/is-thenable.ts +3 -0
  82. package/lib/packages/overlay/symbols.ts +5 -0
  83. package/lib/packages/overlay/types.ts +94 -0
  84. package/lib/packages/overlay/use-overlay-escape.ts +34 -0
  85. package/lib/packages/overlay/use-overlay-store.ts +33 -0
  86. package/lib/packages/overlay/use-overlay-trigger.ts +33 -0
  87. package/lib/packages/overlay/use-overlay.ts +99 -0
  88. package/lib/packages/progress/use-progress.ts +12 -2
  89. package/lib/packages/remote-resource/define-remote-resource.ts +64 -29
  90. package/lib/packages/sidebar/index.ts +2 -0
  91. package/lib/packages/sidebar/sidebar.store.ts +55 -0
  92. package/lib/packages/sidebar/types.ts +21 -0
  93. package/lib/packages/sidebar/use-sidebar-resize.ts +47 -0
  94. package/lib/packages/sidebar/use-sidebar-responsive-expand.ts +19 -0
  95. package/lib/packages/tree/types.ts +6 -0
  96. package/lib/packages/tree/use-tree.ts +38 -37
  97. package/lib/stores/panel.store.ts +56 -5
  98. package/lib/tables/column-definitions/action-column.ts +21 -9
  99. package/lib/tables/column-definitions/user-name-column.ts +10 -0
  100. package/lib/tables/column-sets/server-columns.ts +2 -2
  101. package/lib/tables/column-sets/user-columns.ts +16 -0
  102. package/lib/tables/column-sets/vm-columns.ts +2 -3
  103. package/lib/types/object.type.ts +9 -1
  104. package/lib/types/state-hero.type.ts +19 -0
  105. package/lib/types/task.type.ts +5 -12
  106. package/lib/types/vue-virtual-scroller.d.ts +8 -2
  107. package/lib/utils/injection-keys.util.ts +4 -2
  108. package/lib/utils/ip-address.utils.ts +7 -0
  109. package/lib/utils/sr.utils.ts +4 -0
  110. package/package.json +2 -1
  111. package/lib/stores/sidebar.store.ts +0 -79
@@ -90,3 +90,8 @@ pre {
90
90
  --scrollbar-border: 0.2rem;
91
91
  }
92
92
  }
93
+
94
+ .em-dash-prefix::before {
95
+ content: '\2014'; /* em dash — */
96
+ margin-inline-end: 0.8rem;
97
+ }
@@ -0,0 +1,36 @@
1
+ <template>
2
+ <div class="vts-card-object-title">
3
+ <UiLink v-if="label" size="medium" :to :href :icon>
4
+ {{ label }}
5
+ </UiLink>
6
+ <VtsCodeSnippet class="id" :content="id" copy />
7
+ </div>
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ import VtsCodeSnippet from '@core/components/code-snippet/VtsCodeSnippet.vue'
12
+ import UiLink from '@core/components/ui/link/UiLink.vue'
13
+ import type { IconName } from '@core/icons/index.ts'
14
+ import type { RouteLocationRaw } from 'vue-router'
15
+
16
+ defineProps<{
17
+ id: string
18
+ label?: string
19
+ icon?: IconName
20
+ to?: RouteLocationRaw
21
+ href?: string
22
+ }>()
23
+ </script>
24
+
25
+ <style scoped lang="postcss">
26
+ .vts-card-object-title {
27
+ display: flex;
28
+ flex-direction: column;
29
+ align-items: start;
30
+ gap: 0.8rem;
31
+
32
+ .id {
33
+ width: 100%;
34
+ }
35
+ }
36
+ </style>
@@ -1,8 +1,10 @@
1
1
  <template>
2
2
  <div class="vts-code-snippet">
3
- <slot>
4
- {{ content }}
5
- </slot>
3
+ <div v-tooltip="{ placement: 'bottom' }" class="content text-ellipsis">
4
+ <slot>
5
+ {{ content }}
6
+ </slot>
7
+ </div>
6
8
  <div v-if="copy || slots.addons" class="addons">
7
9
  <slot name="addons">
8
10
  <VtsCopyButton v-if="copy && content" :value="String(content)" />
@@ -13,6 +15,7 @@
13
15
 
14
16
  <script lang="ts" setup>
15
17
  import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
18
+ import { vTooltip } from '@core/directives/tooltip.directive.ts'
16
19
 
17
20
  defineProps<{
18
21
  content?: string | number
@@ -27,13 +30,16 @@ const slots = defineSlots<{
27
30
 
28
31
  <style lang="postcss" scoped>
29
32
  .vts-code-snippet {
30
- font-family: 'Courier New', Courier, monospace;
31
33
  font-size: 1.4rem;
32
- color: var(--color-neutral-txt-primary);
33
34
  display: flex;
34
35
  align-items: center;
35
36
  gap: 0.8rem;
36
37
 
38
+ .content {
39
+ color: var(--color-neutral-txt-primary);
40
+ font-family: 'Courier New', Courier, monospace;
41
+ }
42
+
37
43
  .addons {
38
44
  margin-inline-start: auto;
39
45
  display: flex;
@@ -24,7 +24,11 @@ import { IK_DRAWER } from '@core/packages/drawer/types.ts'
24
24
  import { useMagicKeys, whenever } from '@vueuse/core'
25
25
  import { computed, inject, onMounted, ref } from 'vue'
26
26
 
27
- const { dismissible, isOpen, current } = defineProps<{
27
+ const {
28
+ dismissible,
29
+ isOpen = undefined,
30
+ current,
31
+ } = defineProps<{
28
32
  dismissible?: boolean
29
33
  isOpen?: boolean
30
34
  current?: boolean
@@ -1,16 +1,17 @@
1
1
  <template>
2
- <UiButton accent="brand" :busy="drawer?.isBusy.value" :variant size="medium">
2
+ <UiButton :accent :busy="drawer?.isBusy.value" :variant size="medium">
3
3
  <slot />
4
4
  </UiButton>
5
5
  </template>
6
6
 
7
7
  <script lang="ts" setup>
8
- import UiButton, { type ButtonVariant } from '@core/components/ui/button/UiButton.vue'
8
+ import UiButton, { type ButtonAccent, type ButtonVariant } from '@core/components/ui/button/UiButton.vue'
9
9
  import { IK_DRAWER } from '@core/packages/drawer/types.ts'
10
10
  import { inject } from 'vue'
11
11
 
12
- defineProps<{
12
+ const { accent = 'brand' } = defineProps<{
13
13
  variant: ButtonVariant
14
+ accent?: ButtonAccent
14
15
  }>()
15
16
 
16
17
  defineSlots<{
@@ -1,16 +1,18 @@
1
1
  <template>
2
- <VtsDrawerButton variant="primary" @click="handleClick()">
2
+ <VtsDrawerButton :accent variant="primary" @click="handleClick()">
3
3
  <slot />
4
4
  </VtsDrawerButton>
5
5
  </template>
6
6
 
7
7
  <script lang="ts" setup>
8
8
  import VtsDrawerButton from '@core/components/drawer/VtsDrawerButton.vue'
9
+ import type { ButtonAccent } from '@core/components/ui/button/UiButton.vue'
9
10
  import { IK_DRAWER } from '@core/packages/drawer/types.ts'
10
11
  import { inject } from 'vue'
11
12
 
12
- const { onClick } = defineProps<{
13
+ const { onClick, accent = 'brand' } = defineProps<{
13
14
  onClick?: () => void
15
+ accent?: ButtonAccent
14
16
  }>()
15
17
 
16
18
  defineSlots<{
@@ -1,10 +1,11 @@
1
1
  <template>
2
- <UiLoader v-if="busy" />
2
+ <UiLoader v-if="busy" v-tooltip="busyTooltip ?? false" />
3
3
  <DisplayIcon v-else-if="icon" class="vts-icon" :class="className" :icon />
4
4
  </template>
5
5
 
6
6
  <script lang="ts" setup>
7
7
  import UiLoader from '@core/components/ui/loader/UiLoader.vue'
8
+ import { vTooltip } from '@core/directives/tooltip.directive'
8
9
  import { type IconName, icons } from '@core/icons'
9
10
  import { DisplayIcon } from '@core/packages/icon'
10
11
  import { toVariants } from '@core/utils/to-variants.util.ts'
@@ -16,6 +17,7 @@ const { size, name } = defineProps<{
16
17
  size: IconSize
17
18
  name: IconName | undefined
18
19
  busy?: boolean
20
+ busyTooltip?: string
19
21
  }>()
20
22
 
21
23
  const className = computed(() =>
@@ -51,10 +51,10 @@ const unsortedMessages = useArrayMap(
51
51
  })
52
52
  )
53
53
 
54
- const messages = useRanked(unsortedMessages, ({ accent }) => accent, ['danger', 'warning', 'success', 'info'])
54
+ const messages = useRanked(unsortedMessages, ({ accent }) => accent, ['muted', 'info', 'success', 'warning', 'danger'])
55
55
 
56
56
  const labelAccent = useMapper<InfoAccent, LabelAccent>(
57
- () => messages.value[0]?.accent,
57
+ () => messages.value.at(-1)?.accent,
58
58
  {
59
59
  info: 'neutral',
60
60
  success: 'neutral',
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <div
3
+ class="vts-content-side-panel"
4
+ :class="{
5
+ mobile: uiStore.isSmall,
6
+ 'has-panel-column': hasPanelColumn,
7
+ }"
8
+ >
9
+ <slot />
10
+ </div>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { usePanelStore } from '@core/stores/panel.store'
15
+ import { useUiStore } from '@core/stores/ui.store'
16
+ import { computed } from 'vue'
17
+
18
+ const panelStore = usePanelStore()
19
+ const uiStore = useUiStore()
20
+
21
+ const hasPanelColumn = computed(() => (panelStore.isLocked || panelStore.isExpanded) && !uiStore.isSmall)
22
+ </script>
23
+
24
+ <style scoped lang="postcss">
25
+ .vts-content-side-panel {
26
+ --side-panel-width: 40rem;
27
+
28
+ &:not(.mobile) {
29
+ display: grid;
30
+ grid-template-columns: minmax(0, 1fr) 0;
31
+ align-items: start;
32
+ overflow-x: clip;
33
+ min-height: min-content;
34
+ transition: grid-template-columns 0.25s;
35
+
36
+ &.has-panel-column {
37
+ grid-template-columns: minmax(0, 1fr) var(--side-panel-width);
38
+ }
39
+ }
40
+ }
41
+ </style>
@@ -1,27 +1,30 @@
1
1
  <template>
2
- <div
3
- :class="{
4
- locked: sidebar.isLocked && !ui.isSmall,
5
- expanded: sidebar.isExpanded,
6
- mobile: ui.isSmall,
7
- 'border-right': !ui.isSmall,
8
- }"
9
- class="vts-layout-sidebar"
10
- >
11
- <div v-if="!ui.isSmall" class="lock">
12
- <UiButtonIcon
13
- v-tooltip="{
14
- content: sidebar.isLocked ? t('action:sidebar-unlock') : t('action:sidebar-lock'),
15
- placement: 'right',
16
- }"
17
- accent="brand"
18
- size="small"
19
- :icon="sidebar.isLocked ? 'fa:thumb-tack-slash' : 'fa:thumb-tack'"
20
- @click="sidebar.toggleLock()"
21
- />
2
+ <div :class="className" class="vts-layout-sidebar">
3
+ <div
4
+ v-if="!ui.isSmall"
5
+ :class="{ active: sidebar.isResizing }"
6
+ class="resize-handle"
7
+ @mousedown="sidebar.startResize"
8
+ />
9
+ <div v-if="slots.header || (showLock && !ui.isSmall)" class="header">
10
+ <div v-if="slots.header" class="start">
11
+ <slot name="header" />
12
+ </div>
13
+ <div v-if="showLock && !ui.isSmall" class="lock">
14
+ <UiButtonIcon
15
+ v-tooltip="{
16
+ content: sidebar.isLocked ? t('action:sidebar-unlock') : t('action:sidebar-lock'),
17
+ placement: lockTooltipPosition,
18
+ }"
19
+ accent="brand"
20
+ size="small"
21
+ :icon="sidebar.isLocked ? 'fa:thumb-tack-slash' : 'fa:thumb-tack'"
22
+ @click="sidebar.toggleLock()"
23
+ />
24
+ </div>
22
25
  </div>
23
- <div v-if="slots.header">
24
- <slot name="header" />
26
+ <div v-if="slots.subheader" class="subheader">
27
+ <slot name="subheader" />
25
28
  </div>
26
29
  <div class="content">
27
30
  <slot />
@@ -29,32 +32,47 @@
29
32
  <div v-if="slots.footer">
30
33
  <slot name="footer" />
31
34
  </div>
32
- <div
33
- v-if="!ui.isSmall"
34
- :class="{ active: sidebar.isResizing }"
35
- class="resize-handle"
36
- @mousedown="sidebar.startResize"
37
- />
38
35
  </div>
39
36
  </template>
40
37
 
41
38
  <script lang="ts" setup>
42
39
  import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
43
- import { vTooltip } from '@core/directives/tooltip.directive'
44
- import { useSidebarStore } from '@core/stores/sidebar.store'
45
- import { useUiStore } from '@core/stores/ui.store'
40
+ import { vTooltip } from '@core/directives/tooltip.directive.ts'
41
+ import { type SidebarSide, useLeftSidebarStore, useRightSidebarStore } from '@core/packages/sidebar'
42
+ import { useUiStore } from '@core/stores/ui.store.ts'
43
+ import { toVariants } from '@core/utils/to-variants.util.ts'
44
+ import { computed } from 'vue'
46
45
  import { useI18n } from 'vue-i18n'
47
46
 
47
+ const { side = 'left', showLock = true } = defineProps<{
48
+ side?: SidebarSide
49
+ showLock?: boolean
50
+ }>()
51
+
48
52
  const slots = defineSlots<{
49
53
  default(): any
50
54
  header?(): any
55
+ subheader?(): any
51
56
  footer?(): any
52
57
  }>()
53
58
 
54
59
  const { t } = useI18n()
55
60
 
56
- const sidebar = useSidebarStore()
57
61
  const ui = useUiStore()
62
+
63
+ const leftSidebar = useLeftSidebarStore()
64
+ const rightSidebar = useRightSidebarStore()
65
+ const sidebar = computed(() => (side === 'right' ? rightSidebar : leftSidebar))
66
+
67
+ const lockTooltipPosition = computed(() => (side === 'left' ? 'right' : 'left'))
68
+
69
+ const className = computed(() => [
70
+ {
71
+ locked: sidebar.value.isLocked && !ui.isSmall,
72
+ bordered: !ui.isSmall,
73
+ },
74
+ ...toVariants({ side }),
75
+ ])
58
76
  </script>
59
77
 
60
78
  <style lang="postcss" scoped>
@@ -62,32 +80,74 @@ const ui = useUiStore()
62
80
  position: relative;
63
81
  display: flex;
64
82
  flex-direction: column;
83
+ flex-shrink: 0;
65
84
  height: 100%;
66
85
  background-color: var(--color-neutral-background-secondary);
67
86
  width: v-bind('sidebar.cssWidth');
68
87
  z-index: 1010;
69
88
  transition:
70
- margin-left 0.25s,
89
+ margin-inline 0.25s,
71
90
  transform 0.25s;
72
91
 
73
- &.border-right {
74
- border-right: 0.1rem solid var(--color-neutral-border);
92
+ &.bordered {
93
+ &.side--left {
94
+ border-inline-end: 0.1rem solid var(--color-neutral-border);
95
+ }
96
+
97
+ &.side--right {
98
+ border-inline-start: 0.1rem solid var(--color-neutral-border);
99
+ }
75
100
  }
76
101
 
77
102
  &.locked {
78
- margin-left: v-bind('sidebar.cssOffset');
103
+ &.side--left {
104
+ margin-inline-start: v-bind('sidebar.cssLockedOffset');
105
+ }
106
+
107
+ &.side--right {
108
+ margin-inline-end: v-bind('sidebar.cssLockedOffset');
109
+ }
79
110
  }
80
111
 
81
112
  &:not(.locked) {
82
113
  position: absolute;
83
- transform: translateX(v-bind('sidebar.cssOffset'));
114
+
115
+ &.side--left {
116
+ inset-inline-start: 0;
117
+ }
118
+
119
+ &.side--right {
120
+ inset-inline-end: 0;
121
+ }
122
+
123
+ transform: translateX(v-bind('sidebar.cssUnlockedOffset'));
84
124
  }
85
125
 
86
- .lock {
87
- position: absolute;
88
- inset-block-start: 0.8rem;
89
- inset-inline-end: 1.2rem;
90
- z-index: 1;
126
+ .header,
127
+ .subheader,
128
+ .content {
129
+ &:not(:last-child) {
130
+ border-block-end: 0.1rem solid var(--color-neutral-border);
131
+ }
132
+ }
133
+
134
+ .header {
135
+ display: flex;
136
+ align-items: center;
137
+ background-color: var(--color-neutral-background-primary);
138
+
139
+ .start {
140
+ flex: 1;
141
+ overflow-x: auto;
142
+ }
143
+
144
+ .lock {
145
+ flex-shrink: 0;
146
+ margin-inline-start: auto;
147
+ margin-inline-end: 0.8rem;
148
+ margin-block: 0.4rem;
149
+ z-index: 2;
150
+ }
91
151
  }
92
152
 
93
153
  .content {
@@ -97,12 +157,13 @@ const ui = useUiStore()
97
157
 
98
158
  .resize-handle {
99
159
  position: absolute;
100
- inset: 0 0 0 auto;
160
+ inset-block: 0;
101
161
  width: 0.8rem;
102
162
  background-color: transparent;
103
163
  cursor: col-resize;
104
164
  transition: background-color 0.4s;
105
165
  user-select: none;
166
+ z-index: 1;
106
167
 
107
168
  &:hover,
108
169
  &.active {
@@ -110,5 +171,17 @@ const ui = useUiStore()
110
171
  transition: background-color 0.05s;
111
172
  }
112
173
  }
174
+
175
+ &.side--left {
176
+ .resize-handle {
177
+ inset-inline-end: 0;
178
+ }
179
+ }
180
+
181
+ &.side--right {
182
+ .resize-handle {
183
+ inset-inline-start: 0;
184
+ }
185
+ }
113
186
  }
114
187
  </style>
@@ -7,13 +7,21 @@
7
7
  :busy="isBusy"
8
8
  :disabled="isDisabled"
9
9
  :icon
10
+ class="typo-body-bold-small"
10
11
  @click="handleClick"
11
12
  >
12
13
  <slot />
13
14
  </MenuTrigger>
14
15
  <MenuList v-else :disabled="isDisabled">
15
16
  <template #trigger="{ open, isOpen }">
16
- <MenuTrigger :active="isOpen" :busy="isBusy" :disabled="isDisabled" :icon @click="open">
17
+ <MenuTrigger
18
+ :active="isOpen"
19
+ :busy="isBusy"
20
+ :disabled="isDisabled"
21
+ class="typo-body-bold-small"
22
+ :icon
23
+ @click="open"
24
+ >
17
25
  <slot />
18
26
  <VtsIcon :name="submenuIcon" size="medium" class="submenu-icon" />
19
27
  </MenuTrigger>
@@ -69,13 +69,15 @@ const open = (event: MouseEvent) => {
69
69
 
70
70
  isOpen.value = true
71
71
 
72
+ const trigger = event.currentTarget as HTMLElement
73
+
72
74
  nextTick(() => {
73
75
  clearClickOutsideEvent = onClickOutside(menu, () => (isOpen.value = false), {
74
- ignore: [event.currentTarget as HTMLElement],
76
+ ignore: () => [trigger, '.menu-list'],
75
77
  controls: false,
76
78
  })
77
79
 
78
- placementJs(event.currentTarget as HTMLElement, unrefElement(menu), {
80
+ placementJs(trigger, unrefElement(menu), {
79
81
  placement: props.placement ?? (isParentHorizontal.value ? 'bottom-start' : 'right-start'),
80
82
  })
81
83
  })
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <MenuList placement="bottom-end">
2
+ <MenuList placement="bottom-start">
3
3
  <template #trigger="{ open }">
4
4
  <UiButtonIcon
5
5
  v-tooltip="{
@@ -12,17 +12,32 @@
12
12
  @click="open($event)"
13
13
  />
14
14
  </template>
15
- <MenuItem
16
- v-for="(action, index) of actions"
17
- :key="index"
18
- :icon="action.icon"
19
- :disabled="action.disabled"
20
- :busy="action.busy"
21
- :on-click="action.onClick"
22
- >
23
- {{ action.label }}
24
- <i v-if="action.hint">{{ action.hint }}</i>
25
- </MenuItem>
15
+ <slot>
16
+ <MenuItem
17
+ v-for="(action, index) of actions"
18
+ :key="index"
19
+ :icon="action.icon"
20
+ :disabled="action.disabled"
21
+ :busy="action.busy"
22
+ :on-click="action.onClick"
23
+ >
24
+ {{ action.label }}
25
+ <i v-if="action.hint" class="em-dash-prefix">{{ action.hint }}</i>
26
+ <template v-if="isGroupAction(action)" #submenu>
27
+ <MenuItem
28
+ v-for="(child, childIndex) of action.children"
29
+ :key="childIndex"
30
+ :icon="child.icon"
31
+ :disabled="child.disabled"
32
+ :busy="child.busy"
33
+ :on-click="child.onClick"
34
+ >
35
+ {{ child.label }}
36
+ <i v-if="child.hint" class="em-dash-prefix">{{ child.hint }}</i>
37
+ </MenuItem>
38
+ </template>
39
+ </MenuItem>
40
+ </slot>
26
41
  </MenuList>
27
42
  </template>
28
43
 
@@ -40,14 +55,33 @@ const { size = 'small', actions = [] } = defineProps<{
40
55
  actions?: ActionItem[]
41
56
  }>()
42
57
 
58
+ defineSlots<{
59
+ default(): any
60
+ }>()
61
+
43
62
  const { t } = useI18n()
44
63
 
45
- export type ActionItem = {
64
+ type BaseActionItem = {
46
65
  label: string
47
66
  hint?: string
48
67
  icon?: IconName
49
- onClick: () => any
50
68
  disabled?: boolean
51
69
  busy?: boolean
52
70
  }
71
+
72
+ export type LeafActionItem = BaseActionItem & {
73
+ onClick: () => unknown
74
+ children?: never
75
+ }
76
+
77
+ export type GroupActionItem = BaseActionItem & {
78
+ onClick?: never
79
+ children: LeafActionItem[]
80
+ }
81
+
82
+ export type ActionItem = LeafActionItem | GroupActionItem
83
+
84
+ function isGroupAction(action: ActionItem): action is GroupActionItem {
85
+ return 'children' in action
86
+ }
53
87
  </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <VtsModal :accent icon="status:info-picto">
2
+ <VtsModal :accent :icon dismissible>
3
3
  <template #title>
4
4
  <span>{{ modalTexts.title }}</span>
5
5
  </template>
@@ -20,8 +20,9 @@ import VtsModal from '@core/components/modal/VtsModal.vue'
20
20
  import VtsModalCancelButton from '@core/components/modal/VtsModalCancelButton.vue'
21
21
  import VtsModalConfirmButton from '@core/components/modal/VtsModalConfirmButton.vue'
22
22
  import type { ModalAccent } from '@core/components/ui/modal/UiModal.vue'
23
+ import type { IconName } from '@core/icons'
23
24
  import { useMapper } from '@core/packages/mapper/use-mapper.ts'
24
- import type { ObjectType, VmActions } from '@core/types/object.type.ts'
25
+ import type { ActionsByObject, HostActions, ObjectType, VmActions } from '@core/types/object.type.ts'
25
26
  import { useI18n } from 'vue-i18n'
26
27
 
27
28
  type ActionTexts = {
@@ -30,15 +31,37 @@ type ActionTexts = {
30
31
  action: string
31
32
  }
32
33
 
33
- const { action, object } = defineProps<{
34
+ type TextMappingByObject = {
35
+ [O in ObjectType]: Record<ActionsByObject[O], ActionTexts>
36
+ }
37
+
38
+ type VtsActionModalVmProps = {
39
+ object: 'vm'
34
40
  action: VmActions
35
- accent: ModalAccent
36
- object: ObjectType
37
- }>()
41
+ hostName?: never
42
+ }
43
+
44
+ type VtsActionModalHostProps = {
45
+ object: 'host'
46
+ action: HostActions
47
+ hostName: string
48
+ }
49
+
50
+ const { action, object, hostName } = defineProps<
51
+ (VtsActionModalVmProps | VtsActionModalHostProps) & {
52
+ accent: ModalAccent
53
+ icon: IconName
54
+ }
55
+ >()
56
+
57
+ const defaultActionByObject: { [O in ObjectType]: ActionsByObject[O] } = {
58
+ vm: 'shutdown',
59
+ host: 'disable',
60
+ }
38
61
 
39
62
  const { t } = useI18n()
40
63
 
41
- const textMappingsByObject: Record<ObjectType, Record<VmActions, ActionTexts>> = {
64
+ const textMappingsByObject: TextMappingByObject = {
42
65
  vm: {
43
66
  'force-reboot': {
44
67
  title: t('modal:confirm-vm-force-reboot'),
@@ -61,11 +84,23 @@ const textMappingsByObject: Record<ObjectType, Record<VmActions, ActionTexts>> =
61
84
  action: t('modal:action:vm-shutdown'),
62
85
  },
63
86
  },
87
+ host: {
88
+ enable: {
89
+ title: t('modal:confirm-host-enable', { host: hostName }),
90
+ message: t('modal:host-enable-message'),
91
+ action: t('action:enable-host'),
92
+ },
93
+ disable: {
94
+ title: t('modal:confirm-host-disable', { host: hostName }),
95
+ message: t('modal:host-disable-message'),
96
+ action: t('action:disable-host'),
97
+ },
98
+ },
64
99
  }
65
100
 
66
101
  const modalTexts = useMapper(
67
102
  () => action,
68
- () => textMappingsByObject[object],
69
- () => 'shutdown'
103
+ () => textMappingsByObject[object] as Record<ActionsByObject[ObjectType], ActionTexts>,
104
+ () => defaultActionByObject[object]
70
105
  )
71
106
  </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <VtsModal accent="danger" icon="status:danger-picto">
2
+ <VtsModal accent="danger" icon="status:danger-picto" dismissible>
3
3
  <template #title>
4
4
  <span>{{ modalTexts.title }}</span>
5
5
  </template>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <VtsModal accent="warning" icon="status:warning-picto">
2
+ <VtsModal accent="warning" icon="status:warning-picto" dismissible>
3
3
  <template #title>
4
4
  <I18nT keypath="confirm-delete" scope="global" tag="div">
5
5
  <span class="n-delete">