@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.
- package/lib/assets/css/base.pcss +5 -0
- package/lib/components/copy-all-menu-item/VtsCopyAllMenuItem.vue +48 -0
- package/lib/components/copy-button/VtsCopyButton.vue +2 -2
- package/lib/components/input-wrapper/VtsInputWrapper.vue +2 -2
- package/lib/components/layout/VtsLayoutSidebar.vue +116 -43
- package/lib/components/menu/MenuItem.vue +9 -1
- package/lib/components/menu/MenuList.vue +4 -2
- package/lib/components/menu/VtsActionsMenu.vue +24 -24
- package/lib/components/modal/VtsActionModal.vue +44 -9
- package/lib/components/modal/VtsBlockedModal.vue +1 -1
- package/lib/components/modal/VtsDeleteModal.vue +1 -1
- package/lib/components/overlay/VtsOverlayButton.vue +35 -0
- package/lib/components/overlay/VtsOverlayCancelButton.vue +16 -0
- package/lib/components/overlay/VtsOverlayConfirmButton.vue +25 -0
- package/lib/components/overlay/VtsOverlayList.vue +82 -0
- package/lib/components/progress-bar/VtsProgressBar.vue +5 -3
- package/lib/components/select/VtsSelect.vue +0 -3
- package/lib/components/space-card/VtsSpaceCard.vue +1 -1
- package/lib/components/table/cells/VtsProgressBarCell.vue +1 -1
- package/lib/components/table/cells/VtsUserNameCell.vue +29 -0
- package/lib/components/task/VtsQuickTaskButton.vue +1 -1
- package/lib/components/task/VtsQuickTaskList.vue +1 -1
- package/lib/components/tree/VtsTreeItem.vue +4 -4
- package/lib/components/ui/head-bar/UiHeadBar.vue +1 -1
- package/lib/components/ui/input/UiInput.vue +53 -9
- package/lib/components/ui/query-search-bar/UiQuerySearchBar.vue +1 -15
- package/lib/components/ui/quick-task-item/UiQuickTaskItem.vue +1 -1
- package/lib/components/ui/quick-task-panel/UiQuickTaskPanel.vue +1 -1
- package/lib/components/ui/task-item/UiTaskItem.vue +14 -6
- package/lib/components/ui/task-list/UiTaskList.vue +1 -1
- package/lib/components/ui/text-area/UiTextarea.vue +12 -14
- package/lib/components/ui/tree-item-label/UiTreeItemLabel.vue +34 -14
- package/lib/components/vif-connection-toggle-modal/VtsVifConnectionToggleModal.vue +43 -0
- package/lib/composables/copy.composable.ts +6 -0
- package/lib/composables/table/multi-select.composable.md +33 -0
- package/lib/layouts/CoreLayout.vue +74 -62
- package/lib/locales/cs.json +50 -2
- package/lib/locales/de.json +3 -0
- package/lib/locales/en.json +63 -2
- package/lib/locales/fr.json +63 -2
- package/lib/locales/ko.json +2 -0
- package/lib/locales/nl.json +132 -1
- package/lib/locales/pt.json +1 -1
- package/lib/locales/sk.json +50 -2
- package/lib/packages/form-validation/custom-rules/ip-addresses.rule.ts +15 -0
- package/lib/packages/form-validation/custom-rules/ip.regex.ts +26 -0
- package/lib/packages/form-validation/custom-rules/ipv4-or-cidr.rule.ts +2 -3
- package/lib/packages/form-validation/index.ts +2 -0
- package/lib/packages/hide-permanently/README.md +60 -0
- package/lib/packages/hide-permanently/types.ts +3 -0
- package/lib/packages/hide-permanently/use-hide-permanently.ts +15 -0
- package/lib/packages/overlay/OverlayComponent.vue +18 -0
- package/lib/packages/overlay/README.md +300 -0
- package/lib/packages/overlay/create-event-handler.ts +73 -0
- package/lib/packages/overlay/injection-keys.ts +3 -0
- package/lib/packages/overlay/is-thenable.ts +3 -0
- package/lib/packages/overlay/symbols.ts +5 -0
- package/lib/packages/overlay/types.ts +95 -0
- package/lib/packages/overlay/use-overlay-escape.ts +34 -0
- package/lib/packages/overlay/use-overlay-store.ts +38 -0
- package/lib/packages/overlay/use-overlay-trigger.ts +33 -0
- package/lib/packages/overlay/use-overlay.ts +114 -0
- package/lib/packages/progress/use-progress.ts +12 -2
- package/lib/packages/remote-resource/define-remote-resource.ts +55 -26
- package/lib/packages/remote-resource/sse.store.ts +4 -4
- package/lib/packages/sidebar/index.ts +2 -0
- package/lib/packages/sidebar/sidebar.store.ts +55 -0
- package/lib/packages/sidebar/types.ts +21 -0
- package/lib/packages/sidebar/use-sidebar-resize.ts +47 -0
- package/lib/packages/sidebar/use-sidebar-responsive-expand.ts +19 -0
- package/lib/tables/column-definitions/user-name-column.ts +10 -0
- package/lib/tables/column-sets/server-columns.ts +2 -2
- package/lib/tables/column-sets/user-columns.ts +16 -0
- package/lib/types/object.type.ts +9 -1
- package/lib/types/task.type.ts +5 -12
- package/lib/utils/injection-keys.util.ts +3 -1
- package/lib/utils/ip-address.utils.ts +7 -0
- package/lib/utils/sr.utils.ts +4 -0
- package/package.json +2 -1
- 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(
|
|
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(() =>
|
|
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
|
-
((
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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',
|
|
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',
|
|
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',
|
|
384
|
+
onDataReceived: async receivedData => {
|
|
385
|
+
await onDataReceived(data, receivedData, 'update', sharedContext)
|
|
363
386
|
void flushData()
|
|
364
387
|
},
|
|
365
|
-
onDataRemoved: receivedData => {
|
|
366
|
-
onDataRemoved(data, receivedData,
|
|
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
|
|
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:
|
|
17
|
-
update:
|
|
18
|
-
remove:
|
|
16
|
+
add: EventFn
|
|
17
|
+
update: EventFn
|
|
18
|
+
remove: EventFn
|
|
19
19
|
}
|
|
20
20
|
>
|
|
21
21
|
}
|
|
@@ -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
|
-
|
|
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
|
+
})
|
package/lib/types/object.type.ts
CHANGED
|
@@ -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
|
+
}
|
package/lib/types/task.type.ts
CHANGED
|
@@ -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
|
|
4
|
-
|
|
5
|
-
|
|
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
|
+
}
|
package/lib/utils/sr.utils.ts
CHANGED
|
@@ -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.
|
|
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
|
},
|