@xen-orchestra/web-core 0.56.0 → 0.58.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 (80) hide show
  1. package/lib/assets/css/base.pcss +5 -0
  2. package/lib/components/copy-all-menu-item/VtsCopyAllMenuItem.vue +48 -0
  3. package/lib/components/copy-button/VtsCopyButton.vue +2 -2
  4. package/lib/components/input-wrapper/VtsInputWrapper.vue +2 -2
  5. package/lib/components/layout/VtsLayoutSidebar.vue +116 -43
  6. package/lib/components/menu/MenuItem.vue +9 -1
  7. package/lib/components/menu/MenuList.vue +4 -2
  8. package/lib/components/menu/VtsActionsMenu.vue +24 -24
  9. package/lib/components/modal/VtsActionModal.vue +44 -9
  10. package/lib/components/modal/VtsBlockedModal.vue +1 -1
  11. package/lib/components/modal/VtsDeleteModal.vue +1 -1
  12. package/lib/components/overlay/VtsOverlayButton.vue +35 -0
  13. package/lib/components/overlay/VtsOverlayCancelButton.vue +16 -0
  14. package/lib/components/overlay/VtsOverlayConfirmButton.vue +25 -0
  15. package/lib/components/overlay/VtsOverlayList.vue +82 -0
  16. package/lib/components/progress-bar/VtsProgressBar.vue +5 -3
  17. package/lib/components/select/VtsSelect.vue +0 -3
  18. package/lib/components/space-card/VtsSpaceCard.vue +1 -1
  19. package/lib/components/table/cells/VtsProgressBarCell.vue +1 -1
  20. package/lib/components/table/cells/VtsUserNameCell.vue +29 -0
  21. package/lib/components/task/VtsQuickTaskButton.vue +1 -1
  22. package/lib/components/task/VtsQuickTaskList.vue +1 -1
  23. package/lib/components/tree/VtsTreeItem.vue +4 -4
  24. package/lib/components/ui/head-bar/UiHeadBar.vue +1 -1
  25. package/lib/components/ui/input/UiInput.vue +53 -9
  26. package/lib/components/ui/query-search-bar/UiQuerySearchBar.vue +1 -15
  27. package/lib/components/ui/quick-task-item/UiQuickTaskItem.vue +1 -1
  28. package/lib/components/ui/quick-task-panel/UiQuickTaskPanel.vue +1 -1
  29. package/lib/components/ui/task-item/UiTaskItem.vue +14 -6
  30. package/lib/components/ui/task-list/UiTaskList.vue +1 -1
  31. package/lib/components/ui/text-area/UiTextarea.vue +12 -14
  32. package/lib/components/ui/tree-item-label/UiTreeItemLabel.vue +34 -14
  33. package/lib/components/vif-connection-toggle-modal/VtsVifConnectionToggleModal.vue +43 -0
  34. package/lib/composables/copy.composable.ts +6 -0
  35. package/lib/composables/table/multi-select.composable.md +33 -0
  36. package/lib/layouts/CoreLayout.vue +74 -62
  37. package/lib/locales/cs.json +50 -2
  38. package/lib/locales/de.json +3 -0
  39. package/lib/locales/en.json +63 -2
  40. package/lib/locales/fr.json +63 -2
  41. package/lib/locales/ko.json +2 -0
  42. package/lib/locales/nl.json +132 -1
  43. package/lib/locales/pt.json +1 -1
  44. package/lib/locales/sk.json +50 -2
  45. package/lib/packages/form-validation/custom-rules/ip-addresses.rule.ts +15 -0
  46. package/lib/packages/form-validation/custom-rules/ip.regex.ts +26 -0
  47. package/lib/packages/form-validation/custom-rules/ipv4-or-cidr.rule.ts +2 -3
  48. package/lib/packages/form-validation/index.ts +2 -0
  49. package/lib/packages/hide-permanently/README.md +60 -0
  50. package/lib/packages/hide-permanently/types.ts +3 -0
  51. package/lib/packages/hide-permanently/use-hide-permanently.ts +15 -0
  52. package/lib/packages/overlay/OverlayComponent.vue +18 -0
  53. package/lib/packages/overlay/README.md +300 -0
  54. package/lib/packages/overlay/create-event-handler.ts +73 -0
  55. package/lib/packages/overlay/injection-keys.ts +3 -0
  56. package/lib/packages/overlay/is-thenable.ts +3 -0
  57. package/lib/packages/overlay/symbols.ts +5 -0
  58. package/lib/packages/overlay/types.ts +95 -0
  59. package/lib/packages/overlay/use-overlay-escape.ts +34 -0
  60. package/lib/packages/overlay/use-overlay-store.ts +38 -0
  61. package/lib/packages/overlay/use-overlay-trigger.ts +33 -0
  62. package/lib/packages/overlay/use-overlay.ts +114 -0
  63. package/lib/packages/progress/use-progress.ts +12 -2
  64. package/lib/packages/remote-resource/define-remote-resource.ts +55 -26
  65. package/lib/packages/remote-resource/sse.store.ts +4 -4
  66. package/lib/packages/sidebar/index.ts +2 -0
  67. package/lib/packages/sidebar/sidebar.store.ts +55 -0
  68. package/lib/packages/sidebar/types.ts +21 -0
  69. package/lib/packages/sidebar/use-sidebar-resize.ts +47 -0
  70. package/lib/packages/sidebar/use-sidebar-responsive-expand.ts +19 -0
  71. package/lib/tables/column-definitions/user-name-column.ts +10 -0
  72. package/lib/tables/column-sets/server-columns.ts +2 -2
  73. package/lib/tables/column-sets/user-columns.ts +16 -0
  74. package/lib/types/object.type.ts +9 -1
  75. package/lib/types/task.type.ts +5 -12
  76. package/lib/utils/injection-keys.util.ts +3 -1
  77. package/lib/utils/ip-address.utils.ts +7 -0
  78. package/lib/utils/sr.utils.ts +4 -0
  79. package/package.json +2 -1
  80. package/lib/stores/sidebar.store.ts +0 -79
@@ -0,0 +1,114 @@
1
+ import { createEventHandler } from '@core/packages/overlay/create-event-handler.ts'
2
+ import { OVERLAY_ABORT_EVENT } from '@core/packages/overlay/symbols.ts'
3
+ import type {
4
+ ExtractOverlayResponse,
5
+ ForbidExtraEvents,
6
+ MaybeOptionalArgs,
7
+ MaybeOptionalKey,
8
+ OpenEvents,
9
+ Overlay,
10
+ OverlayEventHandler,
11
+ OverlayEvents,
12
+ Prettify,
13
+ } from '@core/packages/overlay/types.ts'
14
+ import { useOverlayStore } from '@core/packages/overlay/use-overlay-store.ts'
15
+ import { reactiveComputed } from '@vueuse/core'
16
+ import { defineAsyncComponent, markRaw, nextTick, reactive, type AsyncComponentLoader, type Component } from 'vue'
17
+ import type { ComponentProps } from 'vue-component-type-helpers'
18
+
19
+ export function useOverlay<TComponent extends Component, const TEvents extends OverlayEvents<TComponent>>({
20
+ component: componentLoader,
21
+ events,
22
+ }: {
23
+ component: AsyncComponentLoader<TComponent>
24
+ events: TEvents & NoInfer<ForbidExtraEvents<TComponent, TEvents>>
25
+ }) {
26
+ const overlayStore = useOverlayStore()
27
+
28
+ const key = Symbol('new-overlay')
29
+
30
+ const component = defineAsyncComponent(componentLoader)
31
+
32
+ let currentOverlay: Overlay | undefined
33
+
34
+ function abort() {
35
+ currentOverlay?.abort()
36
+ }
37
+
38
+ function open<TOpenEvents extends OpenEvents<TComponent, TEvents>>(
39
+ ...args: MaybeOptionalArgs<
40
+ Prettify<
41
+ { events?: TOpenEvents & NoInfer<ForbidExtraEvents<TComponent, TOpenEvents>> } & MaybeOptionalKey<
42
+ 'props',
43
+ ComponentProps<TComponent>
44
+ >
45
+ >
46
+ >
47
+ ) {
48
+ const { events: openEvents, props } = args[0] ?? {}
49
+
50
+ abort()
51
+
52
+ return new Promise<ExtractOverlayResponse<TComponent, TEvents, TOpenEvents>>(resolve => {
53
+ const definitionEvents = events as Record<string, true | OverlayEventHandler>
54
+ const perOpenEvents = (openEvents ?? {}) as Record<string, OverlayEventHandler>
55
+
56
+ // A new object per `open()` call, so that a pending async handler of a
57
+ // replaced overlay cannot interfere with the one currently displayed
58
+ const overlay = reactive<Overlay>({
59
+ key,
60
+ component: markRaw(component),
61
+ props: {},
62
+ status: 'idle',
63
+ lastTrigger: undefined,
64
+ abort: () => settle({ event: OVERLAY_ABORT_EVENT, payload: undefined }),
65
+ })
66
+
67
+ currentOverlay = overlay
68
+
69
+ async function settle(response: ExtractOverlayResponse<TComponent, TEvents, TOpenEvents>) {
70
+ if (overlay.status === 'settled') {
71
+ return
72
+ }
73
+
74
+ overlay.status = 'settled'
75
+
76
+ // The `leave` transition freezes the DOM in its last painted state,
77
+ // so paint a non-busy frame before removing the overlay
78
+ await nextTick()
79
+
80
+ resolve(response)
81
+
82
+ overlayStore.unregister(overlay)
83
+
84
+ if (currentOverlay === overlay) {
85
+ currentOverlay = undefined
86
+ }
87
+ }
88
+
89
+ const handleEvent = createEventHandler({
90
+ overlay,
91
+ definitionEvents,
92
+ openEvents: perOpenEvents,
93
+ settle: response => settle(response as ExtractOverlayResponse<TComponent, TEvents, TOpenEvents>),
94
+ })
95
+
96
+ // Merge all props + events. If an event is provided in both, the `events`
97
+ // handler runs first, then the `openEvents` one, if needed.
98
+ const eventProps: Record<string, OverlayEventHandler> = {}
99
+
100
+ const eventNames = new Set([...Object.keys(definitionEvents), ...Object.keys(perOpenEvents)])
101
+
102
+ eventNames.forEach(eventName => {
103
+ eventProps[eventName] = (...emitArgs: unknown[]) => handleEvent(eventName, emitArgs)
104
+ })
105
+
106
+ // `reactiveComputed` keeps the merged props live when a reactive `props` object is passed
107
+ overlay.props = reactiveComputed(() => ({ ...(props as Record<string, unknown> | undefined), ...eventProps }))
108
+
109
+ overlayStore.register(overlay)
110
+ })
111
+ }
112
+
113
+ return { open, abort }
114
+ }
@@ -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,
@@ -46,7 +46,7 @@ export function defineRemoteResource<
46
46
  receivedData: any,
47
47
  calledFrom: 'execute' | 'update',
48
48
  context?: ResourceContext<TArgs>
49
- ) => void
49
+ ) => Promise<void>
50
50
  cacheExpirationMs?: number | false
51
51
  pollingIntervalMs?: number | false
52
52
  stream?: boolean
@@ -60,7 +60,7 @@ export function defineRemoteResource<TData, TState extends object, TArgs extends
60
60
  receivedData: any,
61
61
  calledFrom: 'execute' | 'update',
62
62
  context?: ResourceContext<TArgs>
63
- ) => void
63
+ ) => Promise<void>
64
64
  cacheExpirationMs?: number | false
65
65
  pollingIntervalMs?: number | false
66
66
  stream?: boolean
@@ -79,8 +79,8 @@ export function defineRemoteResource<
79
79
  receivedData: any,
80
80
  calledFrom: 'execute' | 'update',
81
81
  context?: ResourceContext<TArgs>
82
- ) => void
83
- onDataRemoved?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
82
+ ) => Promise<void>
83
+ onDataRemoved?: (data: Ref<NoInfer<TData>>, receivedData: any) => Promise<void>
84
84
  stream?: boolean
85
85
  initWatchCollection: () => {
86
86
  collectionId: string
@@ -89,7 +89,7 @@ export function defineRemoteResource<
89
89
  handleDelete: THandleDelete
90
90
  handlePost: THandlePost
91
91
  handleWatching: THandleWatching
92
- predicate?: (receivedData: TData, context: ResourceContext<TArgs> | undefined) => boolean
92
+ predicate?: (receivedData: TData, context: ResourceContext<TArgs> | undefined) => Promise<boolean> | boolean
93
93
  }
94
94
  }): UseRemoteResource<TState, TArgs>
95
95
 
@@ -106,8 +106,8 @@ export function defineRemoteResource<
106
106
  receivedData: any,
107
107
  calledFrom: 'execute' | 'update',
108
108
  context?: ResourceContext<TArgs>
109
- ) => void
110
- onDataRemoved?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
109
+ ) => Promise<void>
110
+ onDataRemoved?: (data: Ref<NoInfer<TData>>, receivedData: any) => Promise<void>
111
111
  cacheExpirationMs?: number | false
112
112
  pollingIntervalMs?: number | false
113
113
  stream?: boolean
@@ -118,15 +118,18 @@ export function defineRemoteResource<
118
118
  handleDelete: THandleDelete
119
119
  handlePost: THandlePost
120
120
  handleWatching: THandleWatching
121
- predicate?: (receivedData: TData, context: ResourceContext<TArgs> | undefined) => boolean
121
+ predicate?: (receivedData: TData, context: ResourceContext<TArgs> | undefined) => Promise<boolean> | boolean
122
122
  }
123
123
  }) {
124
124
  const cache = new Map<
125
125
  string,
126
126
  {
127
127
  count: number
128
+ evictionTimeout?: NodeJS.Timeout // TODO : Fixme , the actual type in browser is number, but NodeJS.Timeout in nodejs.
129
+ // Due to transitive types of vates/types, the type is NodeJS.Timeout.
128
130
  pause: VoidFunction
129
131
  resume: VoidFunction
132
+ isPaused: boolean
130
133
  state: object
131
134
  stateScope: EffectScope
132
135
  isReady: Ref<boolean>
@@ -181,9 +184,14 @@ export function defineRemoteResource<
181
184
 
182
185
  const onDataReceived =
183
186
  config.onDataReceived ??
184
- ((data: Ref<TData>, receivedData: any, calledFrom: 'execute' | 'update', context?: ResourceContext<TArgs>) => {
187
+ (async (
188
+ data: Ref<TData>,
189
+ receivedData: any,
190
+ calledFrom: 'execute' | 'update',
191
+ context?: ResourceContext<TArgs>
192
+ ) => {
185
193
  // allow to ignore some update (like for sub collection. E.g. vms/:id/vdis)
186
- if (watchCollection?.predicate?.(receivedData, context) === false) {
194
+ if ((await watchCollection?.predicate?.(receivedData, context)) === false) {
187
195
  return
188
196
  }
189
197
 
@@ -217,9 +225,9 @@ export function defineRemoteResource<
217
225
 
218
226
  const onDataRemoved =
219
227
  config.onDataRemoved ??
220
- ((data: Ref<TData>, receivedData: any, context?: ResourceContext<TArgs>) => {
228
+ (async (data: Ref<TData>, receivedData: any, context?: ResourceContext<TArgs>) => {
221
229
  // allow to ignore some update (like for sub collection. E.g. vms/:id/vdis)
222
- if (watchCollection?.predicate?.(receivedData, context) === false) {
230
+ if ((await watchCollection?.predicate?.(receivedData, context)) === false) {
223
231
  return
224
232
  }
225
233
 
@@ -246,7 +254,12 @@ export function defineRemoteResource<
246
254
  entry.count += 1
247
255
 
248
256
  if (entry.count === 1) {
249
- entry.resume()
257
+ clearTimeout(entry.evictionTimeout)
258
+ entry.evictionTimeout = undefined
259
+ if (entry.isPaused) {
260
+ entry.resume()
261
+ entry.isPaused = false
262
+ }
250
263
  }
251
264
  }
252
265
 
@@ -263,13 +276,21 @@ export function defineRemoteResource<
263
276
  return
264
277
  }
265
278
 
266
- entry.pause()
279
+ function cleanUp() {
280
+ if (entry === undefined) {
281
+ return
282
+ }
283
+
284
+ entry.pause()
285
+ entry.isPaused = true
286
+ entry.stateScope.stop()
287
+ cache.delete(url)
288
+ }
267
289
 
268
290
  if (cacheExpiration !== false) {
269
- setTimeout(() => {
270
- cache.get(url)?.stateScope.stop()
271
- cache.delete(url)
272
- }, cacheExpiration)
291
+ entry.evictionTimeout = setTimeout(cleanUp, cacheExpiration)
292
+ } else {
293
+ cleanUp()
273
294
  }
274
295
  }
275
296
 
@@ -285,6 +306,8 @@ export function defineRemoteResource<
285
306
  const lastError = ref<Error>()
286
307
 
287
308
  const hasError = computed(() => lastError.value !== undefined)
309
+ const stateScope = effectScope(true)
310
+ const sharedContext = { ...context, scope: stateScope }
288
311
 
289
312
  const data = shallowRef(buildData()) as Ref<TData>
290
313
  // trigger reactivity on data when no more updates since 100ms or after 500ms
@@ -322,7 +345,7 @@ export function defineRemoteResource<
322
345
  }
323
346
 
324
347
  for await (const event of readNDJSONStream(response.body)) {
325
- onDataReceived(data, event, 'execute', context)
348
+ await onDataReceived(data, event, 'execute', sharedContext)
326
349
  void flushData()
327
350
  }
328
351
 
@@ -332,7 +355,7 @@ export function defineRemoteResource<
332
355
  void flushData()
333
356
  }
334
357
  } else {
335
- onDataReceived(data, await response.json(), 'execute', context)
358
+ await onDataReceived(data, await response.json(), 'execute', sharedContext)
336
359
  void flushData()
337
360
  }
338
361
 
@@ -358,12 +381,12 @@ export function defineRemoteResource<
358
381
  handleWatching,
359
382
  handlePost,
360
383
  resource,
361
- onDataReceived: receivedData => {
362
- onDataReceived(data, receivedData, 'update', context)
384
+ onDataReceived: async receivedData => {
385
+ await onDataReceived(data, receivedData, 'update', sharedContext)
363
386
  void flushData()
364
387
  },
365
- onDataRemoved: receivedData => {
366
- onDataRemoved(data, receivedData, context)
388
+ onDataRemoved: async receivedData => {
389
+ await onDataRemoved(data, receivedData, sharedContext)
367
390
  void flushData()
368
391
  },
369
392
  })
@@ -379,13 +402,13 @@ export function defineRemoteResource<
379
402
  resume = timeoutPoll.resume
380
403
  }
381
404
 
382
- const stateScope = effectScope(true)
383
- const state = stateScope.run(() => buildState(data, context))!
405
+ const state = stateScope.run(() => buildState(data, sharedContext))!
384
406
 
385
407
  cache.set(url, {
386
408
  count: 0,
387
409
  pause,
388
410
  resume,
411
+ isPaused: true,
389
412
  state,
390
413
  stateScope,
391
414
  isReady,
@@ -423,6 +446,12 @@ export function defineRemoteResource<
423
446
  throw new Error('No effect scope found. Please provide a scope or use this function within a Vue component.')
424
447
  }
425
448
 
449
+ if (!scope.active) {
450
+ throw new Error(
451
+ 'useRemoteResource() was called with an inactive effect scope. This usually means a stale scope/context, (e.g. captured from an unmounted component) is being reused'
452
+ )
453
+ }
454
+
426
455
  const isEnabled = toRef(optionsOrParentContext?.isEnabled ?? true)
427
456
 
428
457
  return scope.run(() => {
@@ -2,7 +2,7 @@ import { useNow } from '@vueuse/core'
2
2
  import { defineStore } from 'pinia'
3
3
  import { computed, ref, watch as watchVue } from 'vue'
4
4
 
5
- type EventFn = (object: unknown) => void
5
+ type EventFn = (object: unknown) => Promise<void>
6
6
  export type THandlePost = (sseId: string) => Promise<string>
7
7
  export type THandleDelete = (sseId: string, subscriptionId: string) => Promise<void>
8
8
  export type THandleWatching = (
@@ -13,9 +13,9 @@ export type THandleWatching = (
13
13
  configs: Record<
14
14
  string,
15
15
  {
16
- add: (object: unknown) => void
17
- update: (object: unknown) => void
18
- remove: (object: unknown) => void
16
+ add: EventFn
17
+ update: EventFn
18
+ remove: EventFn
19
19
  }
20
20
  >
21
21
  }
@@ -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.58.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
  },