@xen-orchestra/web-core 0.34.0 → 0.35.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/components/dropdown/{DropdownTitle.vue → VtsDropdownTitle.vue} +4 -4
- package/lib/components/input-wrapper/VtsInputWrapper.vue +1 -10
- package/lib/components/menu/MenuList.vue +1 -1
- package/lib/components/size-progress-cell/VtsSizeProgressCell.vue +36 -0
- package/lib/components/space-card/VtsSpaceCard.vue +94 -0
- package/lib/components/ui/label/UiLabel.vue +4 -15
- package/lib/components/ui/tag/UiTag.vue +2 -1
- package/lib/components/ui/text-area/UiTextarea.vue +1 -3
- package/lib/i18n.ts +4 -0
- package/lib/locales/cs.json +12 -2
- package/lib/locales/da.json +261 -0
- package/lib/locales/de.json +3 -1
- package/lib/locales/en.json +11 -1
- package/lib/locales/es.json +1 -1
- package/lib/locales/fa.json +1 -1
- package/lib/locales/fr.json +13 -3
- package/lib/locales/it.json +9 -0
- package/lib/locales/nl.json +10 -1
- package/lib/locales/pt_BR.json +75 -14
- package/lib/locales/ru.json +1 -1
- package/lib/locales/sv.json +1 -1
- package/lib/locales/uk.json +2 -2
- package/lib/packages/remote-resource/README.md +32 -0
- package/lib/packages/remote-resource/define-remote-resource.ts +94 -6
- package/lib/packages/remote-resource/sse.store.ts +140 -0
- package/lib/types/utility.type.ts +6 -0
- package/package.json +1 -1
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useSseStore,
|
|
3
|
+
type THandleDelete,
|
|
4
|
+
type THandlePost,
|
|
5
|
+
type THandleWatching,
|
|
6
|
+
} from '@core/packages/remote-resource/sse.store'
|
|
1
7
|
import type { ResourceContext, UseRemoteResource } from '@core/packages/remote-resource/types.ts'
|
|
2
8
|
import type { VoidFunction } from '@core/types/utility.type.ts'
|
|
3
9
|
import { ifElse } from '@core/utils/if-else.utils.ts'
|
|
4
10
|
import { type MaybeRef, noop, useTimeoutPoll } from '@vueuse/core'
|
|
5
|
-
import { merge } from 'lodash-es'
|
|
11
|
+
import { merge, remove } from 'lodash-es'
|
|
6
12
|
import readNDJSONStream from 'ndjson-readablestream'
|
|
7
13
|
import {
|
|
8
14
|
computed,
|
|
@@ -46,6 +52,28 @@ export function defineRemoteResource<TData, TState extends object, TArgs extends
|
|
|
46
52
|
stream?: boolean
|
|
47
53
|
}): UseRemoteResource<TState, TArgs>
|
|
48
54
|
|
|
55
|
+
export function defineRemoteResource<
|
|
56
|
+
TData,
|
|
57
|
+
TState extends object = { data: Ref<TData> },
|
|
58
|
+
TArgs extends any[] = [],
|
|
59
|
+
>(config: {
|
|
60
|
+
url: string | ((...args: TArgs) => string)
|
|
61
|
+
initialData: () => TData
|
|
62
|
+
state?: (data: Ref<NoInfer<TData>>, context: ResourceContext<TArgs>) => TState
|
|
63
|
+
onDataReceived?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
|
|
64
|
+
onDataRemoved?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
|
|
65
|
+
stream?: boolean
|
|
66
|
+
watchCollection: {
|
|
67
|
+
collectionId: string
|
|
68
|
+
resource: string // reactivity only on XAPI XO record for now
|
|
69
|
+
getIdentifier: (obj: unknown) => string
|
|
70
|
+
handleDelete: THandleDelete
|
|
71
|
+
handlePost: THandlePost
|
|
72
|
+
handleWatching: THandleWatching
|
|
73
|
+
predicate?: (receivedData: TData, context: ResourceContext<TArgs> | undefined) => boolean
|
|
74
|
+
}
|
|
75
|
+
}): UseRemoteResource<TState, TArgs>
|
|
76
|
+
|
|
49
77
|
export function defineRemoteResource<
|
|
50
78
|
TData,
|
|
51
79
|
TState extends object = { data: Ref<TData> },
|
|
@@ -55,9 +83,19 @@ export function defineRemoteResource<
|
|
|
55
83
|
initialData?: () => TData
|
|
56
84
|
state?: (data: Ref<TData>, context: ResourceContext<TArgs>) => TState
|
|
57
85
|
onDataReceived?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
|
|
86
|
+
onDataRemoved?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
|
|
58
87
|
cacheExpirationMs?: number | false
|
|
59
88
|
pollingIntervalMs?: number | false
|
|
60
89
|
stream?: boolean
|
|
90
|
+
watchCollection?: {
|
|
91
|
+
collectionId: string
|
|
92
|
+
resource: string // reactivity only on XAPI XO record for now
|
|
93
|
+
getIdentifier: (obj: unknown) => string
|
|
94
|
+
handleDelete: THandleDelete
|
|
95
|
+
handlePost: THandlePost
|
|
96
|
+
handleWatching: THandleWatching
|
|
97
|
+
predicate?: (receivedData: TData, context: ResourceContext<TArgs> | undefined) => boolean
|
|
98
|
+
}
|
|
61
99
|
}) {
|
|
62
100
|
const cache = new Map<
|
|
63
101
|
string,
|
|
@@ -83,22 +121,56 @@ export function defineRemoteResource<
|
|
|
83
121
|
|
|
84
122
|
const pollingInterval = config.pollingIntervalMs ?? DEFAULT_POLLING_INTERVAL_MS
|
|
85
123
|
|
|
124
|
+
const removeData = (data: TData[], dataToRemove: any) => {
|
|
125
|
+
const getIdentifier = config.watchCollection?.getIdentifier ?? JSON.stringify
|
|
126
|
+
|
|
127
|
+
remove(data, item => {
|
|
128
|
+
if (typeof item === 'object') {
|
|
129
|
+
return getIdentifier(item) === getIdentifier(dataToRemove)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return item === dataToRemove
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
86
136
|
const onDataReceived =
|
|
87
137
|
config.onDataReceived ??
|
|
88
|
-
((data: Ref<TData>, receivedData: any) => {
|
|
89
|
-
|
|
138
|
+
((data: Ref<TData>, receivedData: any, args?: ResourceContext<TArgs>) => {
|
|
139
|
+
// allow to ignore some update (like for sub collection. E.g. vms/:id/vdis)
|
|
140
|
+
if (config.watchCollection?.predicate?.(receivedData, args) === false) {
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
if (data.value === undefined || (Array.isArray(data.value) && Array.isArray(receivedData))) {
|
|
90
144
|
data.value = receivedData
|
|
91
145
|
return
|
|
92
146
|
}
|
|
93
147
|
|
|
94
|
-
if (Array.isArray(data.value)
|
|
95
|
-
data.value
|
|
148
|
+
if (Array.isArray(data.value)) {
|
|
149
|
+
removeData(data.value, receivedData)
|
|
150
|
+
data.value.push(receivedData)
|
|
96
151
|
return
|
|
97
152
|
}
|
|
98
153
|
|
|
99
154
|
merge(data.value, receivedData)
|
|
100
155
|
})
|
|
101
156
|
|
|
157
|
+
const onDataRemoved =
|
|
158
|
+
config.onDataRemoved ??
|
|
159
|
+
((data: Ref<TData>, receivedData: any, args?: ResourceContext<TArgs>) => {
|
|
160
|
+
// allow to ignore some update (like for sub collection. E.g. vms/:id/vdis)
|
|
161
|
+
if (!config.watchCollection?.predicate?.(receivedData, args)) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// for now only support `onDataRemoved` when watching XapiXoRecord collection
|
|
166
|
+
if (Array.isArray(data.value) && !Array.isArray(receivedData)) {
|
|
167
|
+
removeData(data.value, receivedData)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.warn('onDataRemoved received but unhandled for:', receivedData)
|
|
172
|
+
})
|
|
173
|
+
|
|
102
174
|
function subscribeToUrl(url: string) {
|
|
103
175
|
const entry = cache.get(url)
|
|
104
176
|
|
|
@@ -184,7 +256,23 @@ export function defineRemoteResource<
|
|
|
184
256
|
let pause: VoidFunction = noop
|
|
185
257
|
let resume: VoidFunction = execute
|
|
186
258
|
|
|
187
|
-
if (
|
|
259
|
+
if (config.watchCollection !== undefined) {
|
|
260
|
+
const { collectionId, resource, handleDelete, handlePost, handleWatching } = config.watchCollection
|
|
261
|
+
const { watch, unwatch } = useSseStore()
|
|
262
|
+
|
|
263
|
+
pause = () => unwatch({ collectionId, resource, handleDelete })
|
|
264
|
+
resume = async function () {
|
|
265
|
+
await execute()
|
|
266
|
+
await watch({
|
|
267
|
+
collectionId,
|
|
268
|
+
handleWatching,
|
|
269
|
+
handlePost,
|
|
270
|
+
resource,
|
|
271
|
+
onDataReceived: receivedData => onDataReceived(data, receivedData, context),
|
|
272
|
+
onDataRemoved: receivedData => onDataRemoved(data, receivedData, context),
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
} else if (pollingInterval !== false) {
|
|
188
276
|
const timeoutPoll = useTimeoutPoll(execute, pollingInterval, {
|
|
189
277
|
immediateCallback: true,
|
|
190
278
|
immediate: false,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref, watch as watchVue } from 'vue'
|
|
3
|
+
|
|
4
|
+
type EventFn = (object: unknown) => void
|
|
5
|
+
export type THandlePost = (sseId: string) => Promise<string>
|
|
6
|
+
export type THandleDelete = (sseId: string, subscriptionId: string) => Promise<void>
|
|
7
|
+
export type THandleWatching = (
|
|
8
|
+
updateSseId: (id: string) => void,
|
|
9
|
+
getConfigByResource: (resource: string) =>
|
|
10
|
+
| {
|
|
11
|
+
subscriptionId: string
|
|
12
|
+
configs: Record<
|
|
13
|
+
string,
|
|
14
|
+
{
|
|
15
|
+
add: (object: unknown) => void
|
|
16
|
+
update: (object: unknown) => void
|
|
17
|
+
remove: (object: unknown) => void
|
|
18
|
+
}
|
|
19
|
+
>
|
|
20
|
+
}
|
|
21
|
+
| undefined
|
|
22
|
+
) => void
|
|
23
|
+
|
|
24
|
+
export const useSseStore = defineStore('sse', () => {
|
|
25
|
+
const sse = ref<{ id?: string; isWatching: boolean }>({ isWatching: false })
|
|
26
|
+
const configsByResource: Map<
|
|
27
|
+
string,
|
|
28
|
+
{
|
|
29
|
+
subscriptionId: string
|
|
30
|
+
configs: Record<
|
|
31
|
+
string,
|
|
32
|
+
{
|
|
33
|
+
add: EventFn
|
|
34
|
+
update: EventFn
|
|
35
|
+
remove: EventFn
|
|
36
|
+
}
|
|
37
|
+
>
|
|
38
|
+
}
|
|
39
|
+
> = new Map()
|
|
40
|
+
|
|
41
|
+
function updateSseId(id: string) {
|
|
42
|
+
sse.value.id = id
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getConfigsByResource(resource: string) {
|
|
46
|
+
return configsByResource.get(resource)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function initializeWatcher(handleWatching: THandleWatching) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
if (sse.value.id !== undefined) {
|
|
52
|
+
return reject(new Error('SSE already initialized'))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
watchVue(
|
|
56
|
+
sse,
|
|
57
|
+
value => {
|
|
58
|
+
if (value.id !== undefined) {
|
|
59
|
+
resolve(undefined)
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{ deep: true }
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if (!sse.value.isWatching) {
|
|
66
|
+
sse.value.isWatching = true
|
|
67
|
+
handleWatching(updateSseId, getConfigsByResource)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function watch({
|
|
73
|
+
collectionId,
|
|
74
|
+
resource,
|
|
75
|
+
onDataReceived,
|
|
76
|
+
onDataRemoved,
|
|
77
|
+
handlePost,
|
|
78
|
+
handleWatching,
|
|
79
|
+
}: {
|
|
80
|
+
collectionId: string
|
|
81
|
+
resource: string
|
|
82
|
+
onDataReceived: EventFn
|
|
83
|
+
onDataRemoved: EventFn
|
|
84
|
+
handlePost: THandlePost
|
|
85
|
+
handleWatching: THandleWatching
|
|
86
|
+
}) {
|
|
87
|
+
if (sse.value.id === undefined) {
|
|
88
|
+
await initializeWatcher(handleWatching)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const id = await handlePost(sse.value.id!)
|
|
92
|
+
|
|
93
|
+
const { subscriptionId, configs } = configsByResource.get(resource) ?? {}
|
|
94
|
+
|
|
95
|
+
if (subscriptionId !== undefined && id !== subscriptionId) {
|
|
96
|
+
throw new Error(`Previous subscription ID: ${subscriptionId} and new one: ${id} are not the same`)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
configsByResource.set(resource, {
|
|
100
|
+
subscriptionId: id,
|
|
101
|
+
configs: {
|
|
102
|
+
...configs,
|
|
103
|
+
[collectionId]: {
|
|
104
|
+
add: onDataReceived,
|
|
105
|
+
update: onDataReceived,
|
|
106
|
+
remove: onDataRemoved,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function unwatch({
|
|
113
|
+
collectionId,
|
|
114
|
+
resource,
|
|
115
|
+
handleDelete,
|
|
116
|
+
}: {
|
|
117
|
+
collectionId: string
|
|
118
|
+
resource: string
|
|
119
|
+
handleDelete: THandleDelete
|
|
120
|
+
}) {
|
|
121
|
+
if (sse.value.id === undefined || !configsByResource.has(resource)) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { configs, subscriptionId } = configsByResource.get(resource)!
|
|
126
|
+
delete configs[collectionId]
|
|
127
|
+
|
|
128
|
+
if (Object.keys(configs).length === 0) {
|
|
129
|
+
await handleDelete(sse.value.id, subscriptionId)
|
|
130
|
+
configsByResource.delete(resource)
|
|
131
|
+
} else {
|
|
132
|
+
configsByResource.set(resource, {
|
|
133
|
+
subscriptionId,
|
|
134
|
+
configs,
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { watch, unwatch }
|
|
140
|
+
})
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { XapiXoRecord, XoAlarm } from '@vates/types/'
|
|
2
|
+
|
|
1
3
|
export type MaybeArray<T> = T | T[]
|
|
2
4
|
|
|
3
5
|
export type VoidFunction = () => void
|
|
@@ -18,3 +20,7 @@ export type KeyOfByValue<T, TValue> =
|
|
|
18
20
|
: never
|
|
19
21
|
|
|
20
22
|
export type ArrayFilterPredicate<T> = (value: T, index: number, array: T[]) => boolean
|
|
23
|
+
|
|
24
|
+
export type GetRecordByType<TType extends XapiXoRecord['type'] | 'alarm'> = TType extends 'alarm'
|
|
25
|
+
? XoAlarm
|
|
26
|
+
: Extract<XapiXoRecord, { type: TType }>
|