@xen-orchestra/web-core 0.26.1 → 0.27.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/ui/collapsible-list/UiCollapsibleList.vue +73 -0
- package/lib/components/ui/{quoteCode/UiQuoteCode.vue → log-entry-viewer/UiLogEntryViewer.vue} +54 -21
- package/lib/components/ui/table-pagination/UiTablePagination.vue +19 -79
- package/lib/composables/default-tab.composable.md +42 -0
- package/lib/composables/default-tab.composable.ts +26 -0
- package/lib/composables/link-component.composable.ts +3 -2
- package/lib/locales/en.json +3 -0
- package/lib/locales/fr.json +3 -0
- package/lib/packages/form-select/types.ts +3 -0
- package/lib/packages/form-select/use-form-option-controller.ts +5 -6
- package/lib/packages/form-select/use-form-select-controller.ts +1 -0
- package/lib/packages/form-select/use-form-select.ts +153 -109
- package/lib/packages/remote-resource/README.md +115 -0
- package/lib/packages/remote-resource/define-remote-resource.ts +294 -0
- package/lib/packages/remote-resource/types.ts +28 -0
- package/package.json +2 -2
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import type { ResourceContext, UseRemoteResource } from '@core/packages/remote-resource/types.ts'
|
|
2
|
+
import type { VoidFunction } from '@core/types/utility.type.ts'
|
|
3
|
+
import { ifElse } from '@core/utils/if-else.utils.ts'
|
|
4
|
+
import { type MaybeRef, noop, useTimeoutPoll } from '@vueuse/core'
|
|
5
|
+
import { merge } from 'lodash-es'
|
|
6
|
+
import readNDJSONStream from 'ndjson-readablestream'
|
|
7
|
+
import {
|
|
8
|
+
computed,
|
|
9
|
+
type ComputedRef,
|
|
10
|
+
type EffectScope,
|
|
11
|
+
getCurrentScope,
|
|
12
|
+
type MaybeRefOrGetter,
|
|
13
|
+
onScopeDispose,
|
|
14
|
+
reactive,
|
|
15
|
+
type Ref,
|
|
16
|
+
ref,
|
|
17
|
+
toRef,
|
|
18
|
+
toValue,
|
|
19
|
+
watch,
|
|
20
|
+
} from 'vue'
|
|
21
|
+
|
|
22
|
+
const DEFAULT_CACHE_DURATION_MS = 10_000
|
|
23
|
+
|
|
24
|
+
const DEFAULT_POLLING_INTERVAL_MS = 30_000
|
|
25
|
+
|
|
26
|
+
export function defineRemoteResource<
|
|
27
|
+
TData,
|
|
28
|
+
TState extends object = { data: Ref<TData> },
|
|
29
|
+
TArgs extends any[] = [],
|
|
30
|
+
>(config: {
|
|
31
|
+
url: string | ((...args: TArgs) => string)
|
|
32
|
+
initialData: () => TData
|
|
33
|
+
state?: (data: Ref<NoInfer<TData>>, context: ResourceContext<TArgs>) => TState
|
|
34
|
+
onDataReceived?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
|
|
35
|
+
cacheDurationMs?: number
|
|
36
|
+
pollingIntervalMs?: number
|
|
37
|
+
stream?: boolean
|
|
38
|
+
}): UseRemoteResource<TState, TArgs>
|
|
39
|
+
|
|
40
|
+
export function defineRemoteResource<TData, TState extends object, TArgs extends any[] = []>(config: {
|
|
41
|
+
url: string | ((...args: TArgs) => string)
|
|
42
|
+
state?: (data: Ref<TData | undefined>, context: ResourceContext<TArgs>) => TState
|
|
43
|
+
onDataReceived?: (data: Ref<TData | undefined>, receivedData: any) => void
|
|
44
|
+
cacheDurationMs?: number
|
|
45
|
+
pollingIntervalMs?: number
|
|
46
|
+
stream?: boolean
|
|
47
|
+
}): UseRemoteResource<TState, TArgs>
|
|
48
|
+
|
|
49
|
+
export function defineRemoteResource<
|
|
50
|
+
TData,
|
|
51
|
+
TState extends object = { data: Ref<TData> },
|
|
52
|
+
TArgs extends any[] = [],
|
|
53
|
+
>(config: {
|
|
54
|
+
url: string | ((...args: TArgs) => string)
|
|
55
|
+
initialData?: () => TData
|
|
56
|
+
state?: (data: Ref<TData>, context: ResourceContext<TArgs>) => TState
|
|
57
|
+
onDataReceived?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
|
|
58
|
+
cacheDurationMs?: number
|
|
59
|
+
pollingIntervalMs?: number
|
|
60
|
+
stream?: boolean
|
|
61
|
+
}) {
|
|
62
|
+
const cache = new Map<
|
|
63
|
+
string,
|
|
64
|
+
{
|
|
65
|
+
count: number
|
|
66
|
+
pause: VoidFunction
|
|
67
|
+
resume: VoidFunction
|
|
68
|
+
state: object
|
|
69
|
+
isReady: Ref<boolean>
|
|
70
|
+
isFetching: Ref<boolean>
|
|
71
|
+
lastError: Ref<Error | undefined>
|
|
72
|
+
hasError: ComputedRef<boolean>
|
|
73
|
+
}
|
|
74
|
+
>()
|
|
75
|
+
|
|
76
|
+
const buildUrl = typeof config.url === 'string' ? () => config.url as string : config.url
|
|
77
|
+
|
|
78
|
+
const buildData = config.initialData ?? (() => undefined as TData | undefined)
|
|
79
|
+
|
|
80
|
+
const buildState = config.state ?? ((data: Ref<TData>) => ({ data }))
|
|
81
|
+
|
|
82
|
+
const cacheDuration = config.cacheDurationMs ?? DEFAULT_CACHE_DURATION_MS
|
|
83
|
+
|
|
84
|
+
const pollingInterval = config.pollingIntervalMs ?? DEFAULT_POLLING_INTERVAL_MS
|
|
85
|
+
|
|
86
|
+
const onDataReceived =
|
|
87
|
+
config.onDataReceived ??
|
|
88
|
+
((data: Ref<TData>, receivedData: any) => {
|
|
89
|
+
if (!config.stream || data.value === undefined) {
|
|
90
|
+
data.value = receivedData
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (Array.isArray(data.value) && Array.isArray(receivedData)) {
|
|
95
|
+
data.value.push(...receivedData)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
merge(data.value, receivedData)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
function subscribeToUrl(url: string) {
|
|
103
|
+
const entry = cache.get(url)
|
|
104
|
+
|
|
105
|
+
if (!entry) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
entry.count += 1
|
|
110
|
+
|
|
111
|
+
if (entry.count === 1) {
|
|
112
|
+
entry.resume()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function unsubscribeFromUrl(url: string) {
|
|
117
|
+
const entry = cache.get(url)
|
|
118
|
+
|
|
119
|
+
if (!entry) {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
entry.count -= 1
|
|
124
|
+
|
|
125
|
+
if (entry.count > 0) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
entry.pause()
|
|
130
|
+
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
cache.delete(url)
|
|
133
|
+
}, cacheDuration)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function registerUrl(url: string, context: ResourceContext<TArgs>) {
|
|
137
|
+
if (cache.has(url)) {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const isReady = ref(false)
|
|
142
|
+
|
|
143
|
+
const isFetching = ref(false)
|
|
144
|
+
|
|
145
|
+
const lastError = ref<Error>()
|
|
146
|
+
|
|
147
|
+
const hasError = computed(() => lastError.value !== undefined)
|
|
148
|
+
|
|
149
|
+
const data = ref(buildData()) as Ref<TData>
|
|
150
|
+
|
|
151
|
+
async function execute() {
|
|
152
|
+
try {
|
|
153
|
+
isFetching.value = true
|
|
154
|
+
|
|
155
|
+
const response = await fetch(url)
|
|
156
|
+
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
lastError.value = Error(`Failed to fetch: ${response.statusText}`)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!response.body) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (config.stream) {
|
|
167
|
+
for await (const event of readNDJSONStream(response.body)) {
|
|
168
|
+
onDataReceived(data, event)
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
onDataReceived(data, await response.json())
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
isReady.value = true
|
|
175
|
+
} catch (error) {
|
|
176
|
+
lastError.value = error instanceof Error ? error : new Error(String(error))
|
|
177
|
+
} finally {
|
|
178
|
+
isFetching.value = false
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let pause: VoidFunction = noop
|
|
183
|
+
let resume: VoidFunction = execute
|
|
184
|
+
|
|
185
|
+
if (pollingInterval > 0) {
|
|
186
|
+
const timeoutPoll = useTimeoutPoll(execute, pollingInterval, {
|
|
187
|
+
immediateCallback: true,
|
|
188
|
+
immediate: false,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
pause = timeoutPoll.pause
|
|
192
|
+
resume = timeoutPoll.resume
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const state = buildState(data, context)
|
|
196
|
+
|
|
197
|
+
cache.set(url, {
|
|
198
|
+
count: 0,
|
|
199
|
+
pause,
|
|
200
|
+
resume,
|
|
201
|
+
state,
|
|
202
|
+
isReady,
|
|
203
|
+
isFetching,
|
|
204
|
+
lastError,
|
|
205
|
+
hasError,
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function initializeUrl(url: ComputedRef<string>, context: ResourceContext<TArgs>) {
|
|
210
|
+
watch(
|
|
211
|
+
url,
|
|
212
|
+
(toUrl, fromUrl) => {
|
|
213
|
+
registerUrl(toUrl, context)
|
|
214
|
+
|
|
215
|
+
if (context.isEnabled.value) {
|
|
216
|
+
subscribeToUrl(toUrl)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (fromUrl) {
|
|
220
|
+
unsubscribeFromUrl(fromUrl)
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
{ immediate: true }
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return function useRemoteResource(
|
|
228
|
+
optionsOrParentContext?: { isEnabled?: MaybeRef<boolean>; scope?: EffectScope },
|
|
229
|
+
...args: { [K in keyof TArgs]: MaybeRefOrGetter<TArgs[K]> }
|
|
230
|
+
) {
|
|
231
|
+
const scope = optionsOrParentContext?.scope ?? getCurrentScope()
|
|
232
|
+
|
|
233
|
+
if (!scope) {
|
|
234
|
+
throw new Error('No effect scope found. Please provide a scope or use this function within a Vue component.')
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const isEnabled = toRef(optionsOrParentContext?.isEnabled ?? true)
|
|
238
|
+
|
|
239
|
+
return scope.run(() => {
|
|
240
|
+
const url = computed(() => buildUrl(...(args.map(arg => toValue(arg)) as TArgs)))
|
|
241
|
+
|
|
242
|
+
onScopeDispose(() => {
|
|
243
|
+
unsubscribeFromUrl(url.value)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
const context: ResourceContext<TArgs> = {
|
|
247
|
+
scope,
|
|
248
|
+
args,
|
|
249
|
+
isReady: computed(() => cache.get(url.value)?.isReady.value ?? false),
|
|
250
|
+
isFetching: computed(() => cache.get(url.value)?.isFetching?.value ?? false),
|
|
251
|
+
lastError: computed(() => cache.get(url.value)?.lastError.value),
|
|
252
|
+
hasError: computed(() => cache.get(url.value)?.hasError.value ?? false),
|
|
253
|
+
isEnabled,
|
|
254
|
+
enable: () => {
|
|
255
|
+
isEnabled.value = true
|
|
256
|
+
},
|
|
257
|
+
disable: () => {
|
|
258
|
+
isEnabled.value = false
|
|
259
|
+
},
|
|
260
|
+
forceReload: () => {
|
|
261
|
+
cache.get(url.value)?.pause()
|
|
262
|
+
cache.get(url.value)?.resume()
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
initializeUrl(url, context)
|
|
267
|
+
|
|
268
|
+
const state = reactive({} as TState)
|
|
269
|
+
|
|
270
|
+
ifElse(
|
|
271
|
+
isEnabled,
|
|
272
|
+
() => subscribeToUrl(url.value),
|
|
273
|
+
() => unsubscribeFromUrl(url.value)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
watch(
|
|
277
|
+
url,
|
|
278
|
+
() => {
|
|
279
|
+
Object.assign(state, cache.get(url.value)!.state)
|
|
280
|
+
},
|
|
281
|
+
{ immediate: true }
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
...Object.fromEntries(
|
|
286
|
+
Object.entries(state).map(([key, value]) =>
|
|
287
|
+
typeof value === 'function' ? [key, value] : [key, toRef(state, key as any)]
|
|
288
|
+
)
|
|
289
|
+
),
|
|
290
|
+
$context: context,
|
|
291
|
+
}
|
|
292
|
+
})!
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { MaybeRef } from '@vueuse/core'
|
|
2
|
+
import type { MaybeRefOrGetter } from '@vueuse/shared'
|
|
3
|
+
import type { ComputedRef, EffectScope, Ref, ToRef } from 'vue'
|
|
4
|
+
|
|
5
|
+
export type ResourceContext<TArgs extends any[]> = {
|
|
6
|
+
scope: EffectScope
|
|
7
|
+
args: { [K in keyof TArgs]: MaybeRefOrGetter<TArgs[K]> }
|
|
8
|
+
isReady: ComputedRef<boolean>
|
|
9
|
+
isFetching: ComputedRef<boolean>
|
|
10
|
+
hasError: ComputedRef<boolean>
|
|
11
|
+
lastError: ComputedRef<Error | undefined>
|
|
12
|
+
isEnabled: Ref<boolean>
|
|
13
|
+
enable: () => void
|
|
14
|
+
disable: () => void
|
|
15
|
+
forceReload: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type UseRemoteResource<TState, TArgs extends any[]> = (
|
|
19
|
+
optionsOrParentContext?: {
|
|
20
|
+
isEnabled?: MaybeRef<boolean>
|
|
21
|
+
scope?: EffectScope
|
|
22
|
+
},
|
|
23
|
+
...args: { [K in keyof TArgs]: MaybeRefOrGetter<TArgs[K]> }
|
|
24
|
+
) => {
|
|
25
|
+
[K in keyof TState]: TState[K] extends (...args: any[]) => any ? TState[K] : ToRef<TState[K]>
|
|
26
|
+
} & {
|
|
27
|
+
$context: ResourceContext<TArgs>
|
|
28
|
+
}
|
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.27.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"exports": {
|
|
7
7
|
"./*": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"human-format": "^1.2.1",
|
|
27
27
|
"iterable-backoff": "^0.1.0",
|
|
28
28
|
"lodash-es": "^4.17.21",
|
|
29
|
-
"ndjson-readablestream": "^1.
|
|
29
|
+
"ndjson-readablestream": "^1.3.0",
|
|
30
30
|
"placement.js": "^1.0.0-beta.5",
|
|
31
31
|
"simple-icons": "^14.14.0",
|
|
32
32
|
"vue-echarts": "^6.6.8"
|