@xen-orchestra/web-core 0.26.1 → 0.28.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/card/VtsCardRowKeyValue.vue +1 -0
- package/lib/components/cell-object/VtsCellObject.vue +1 -2
- package/lib/components/console/VtsActionsConsole.vue +3 -9
- package/lib/components/copy-button/VtsCopyButton.vue +1 -3
- package/lib/components/dropdown/DropdownTitle.vue +3 -3
- package/lib/components/icon/VtsIcon.vue +32 -76
- package/lib/components/input-wrapper/VtsInputWrapper.vue +2 -2
- package/lib/components/layout/VtsLayoutSidebar.vue +1 -2
- package/lib/components/menu/MenuItem.vue +4 -5
- package/lib/components/menu/MenuTrigger.vue +3 -3
- package/lib/components/object-icon/VtsObjectIcon.vue +2 -2
- package/lib/components/resources/VtsResource.vue +3 -3
- package/lib/components/select/VtsSelect.vue +4 -5
- package/lib/components/table/ColumnTitle.vue +11 -12
- package/lib/components/task/VtsQuickTaskButton.vue +1 -2
- package/lib/components/tree/VtsTreeLoadingItem.vue +3 -3
- package/lib/components/ui/alarm-item/UiAlarmItem.vue +1 -2
- package/lib/components/ui/alert/UiAlert.vue +14 -13
- package/lib/components/ui/button/UiButton.vue +4 -5
- package/lib/components/ui/button-icon/UiButtonIcon.vue +3 -3
- package/lib/components/ui/checkbox/UiCheckbox.vue +2 -9
- package/lib/components/ui/chip/ChipIcon.vue +3 -3
- package/lib/components/ui/chip/ChipRemoveIcon.vue +1 -2
- package/lib/components/ui/chip/UiChip.vue +2 -2
- package/lib/components/ui/circle-progress-bar/UiCircleProgressBar.vue +7 -4
- package/lib/components/ui/collapsible-list/UiCollapsibleList.vue +73 -0
- package/lib/components/ui/donut-chart/UiDonutChart.vue +30 -17
- package/lib/components/ui/dropdown/UiDropdown.vue +5 -7
- package/lib/components/ui/dropdown-button/UiDropdownButton.vue +4 -5
- package/lib/components/ui/head-bar/UiHeadBar.vue +6 -11
- package/lib/components/ui/info/UiInfo.vue +14 -20
- package/lib/components/ui/input/UiInput.vue +6 -7
- package/lib/components/ui/label/UiLabel.vue +3 -3
- package/lib/components/ui/legend/UiLegend.vue +3 -3
- package/lib/components/ui/legend-title/UiLegendTitle.vue +7 -3
- package/lib/components/ui/link/UiLink.vue +4 -5
- package/lib/components/ui/{quoteCode/UiQuoteCode.vue → log-entry-viewer/UiLogEntryViewer.vue} +58 -21
- package/lib/components/ui/object-link/UiObjectLink.vue +4 -4
- package/lib/components/ui/query-search-bar/UiQuerySearchBar.vue +3 -4
- package/lib/components/ui/quick-task-item/UiQuickTaskItem.vue +3 -4
- package/lib/components/ui/radio-button/UiRadioButton.vue +1 -2
- package/lib/components/ui/table-pagination/PaginationButton.vue +2 -25
- package/lib/components/ui/table-pagination/UiTablePagination.vue +56 -95
- package/lib/components/ui/tag/UiTag.vue +3 -3
- package/lib/components/ui/text-area/UiTextarea.vue +2 -2
- package/lib/components/ui/toaster/UiToaster.vue +15 -13
- package/lib/components/ui/tree-item-label/UiTreeItemLabel.vue +4 -5
- 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/composables/pagination.composable.ts +5 -0
- package/lib/i18n.ts +4 -0
- package/lib/icons/fa-icons.ts +48 -2
- package/lib/icons/index.ts +4 -0
- package/lib/icons/legacy-icons.ts +10 -0
- package/lib/icons/object-icons.ts +1 -1
- package/lib/layouts/CoreLayout.vue +1 -2
- package/lib/locales/cs.json +29 -10
- package/lib/locales/de.json +70 -7
- package/lib/locales/en.json +25 -0
- package/lib/locales/es.json +3 -3
- package/lib/locales/fr.json +25 -0
- package/lib/locales/it.json +17 -5
- package/lib/locales/nl.json +28 -10
- package/lib/locales/pt_BR.json +67 -0
- package/lib/locales/sv.json +17 -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/icon/DisplayIcon.vue +1 -0
- package/lib/packages/progress/README.md +62 -0
- package/lib/packages/progress/types.ts +19 -0
- package/lib/packages/progress/use-progress-group.ts +68 -0
- package/lib/packages/progress/use-progress.ts +22 -0
- 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/lib/packages/threshold/README.md +30 -0
- package/lib/packages/threshold/type.ts +3 -0
- package/lib/packages/threshold/use-threshold.ts +19 -0
- package/lib/types/object-icon.type.ts +0 -40
- package/package.json +2 -2
- package/lib/components/icon/NewVtsIcon.vue +0 -49
- package/lib/components/ui/complex-icon/UiComplexIcon.vue +0 -45
- package/lib/components/ui/object-icon/UiObjectIcon.vue +0 -251
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# `defineRemoteResource` documentation
|
|
2
|
+
|
|
3
|
+
This utility will create a function that can be used to fetch data from a remote resource (i.e., an API endpoint)
|
|
4
|
+
|
|
5
|
+
When called, a subscription to the resource is registered.
|
|
6
|
+
|
|
7
|
+
If multiple Components call that same function, a subscription will be registered for each of them.
|
|
8
|
+
|
|
9
|
+
They will share the same data and state, which means that the data will be fetched only once and shared across all Components.
|
|
10
|
+
|
|
11
|
+
The fetch will start automatically when at least one Component is using the resource, then, when the last Component releases the resource, the polling will stop.
|
|
12
|
+
|
|
13
|
+
The data is cached for a certain duration, so if a Component remounts in the frame time, the data will be retrieved from the cache to be displayed immediately, while the data is being fetched again in the background.
|
|
14
|
+
|
|
15
|
+
## Basic usage
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
const useMyResource = defineRemoteResource({
|
|
19
|
+
url: '/api/path/to/resource',
|
|
20
|
+
})
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
By default, the initial data (`TData`) will be `undefined`, the returned state will be `{ data: Ref<TData> }` and when new data is received, it will just replace the previous one.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
const { data } = useMyResource()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Initial data, state and received data handling
|
|
30
|
+
|
|
31
|
+
The initial data, the state and what happen when data is retrieved can be customized by passing the `initialData`, `state` and `onDataReceived` properties:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const useMyResource = defineRemoteResource({
|
|
35
|
+
url: '/api/path/to/resource',
|
|
36
|
+
initialData: () => {} as Partial<MyResource>,
|
|
37
|
+
state: (data) => {
|
|
38
|
+
return {
|
|
39
|
+
myResource: data,
|
|
40
|
+
customProp: computed(() => data.foobar)
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
onDataReceived: (currentData, receivedData) => {
|
|
44
|
+
deepMerge(currentData.value, receivedData)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { myResource, customProp } = useMyResource()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Context
|
|
52
|
+
|
|
53
|
+
The context is available in the `$context` property of the returned state.
|
|
54
|
+
|
|
55
|
+
It contains the following properties:
|
|
56
|
+
|
|
57
|
+
| Property | Type | Description |
|
|
58
|
+
| ------------- | --------------------------------- | ---------------------------------------------------------------------------------- |
|
|
59
|
+
| `scope` | `EffectScope` | The Vue effect scope used to be shared across the resource and its dependencies |
|
|
60
|
+
| `args` | `any[]` | The arguments passed to the `useMyResource` function |
|
|
61
|
+
| `isReady` | `ComputedRef<boolean>` | Indicates if the resource is ready (i.e., the data has been fetched at least once) |
|
|
62
|
+
| `hasError` | `ComputedRef<boolean>` | Indicates if an error occurred during the last fetch |
|
|
63
|
+
| `lastError` | `ComputedRef<Error \| undefined>` | The last error that occurred during the fetch, if any |
|
|
64
|
+
| `isEnabled` | `Ref<boolean>` | Whether the resource is enabled (execute the request) or not |
|
|
65
|
+
| `enable` | `() => void` | Function to manually enable the resource |
|
|
66
|
+
| `disable` | `() => void` | Function to manually disable the resource |
|
|
67
|
+
| `forceReload` | `() => void` | Function to manually reload the resource |
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const { myResource, $context } = useMyResource()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
It's also passed as the second argument of the `state` builder and can be used to extend the resource state.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const useMyResource = defineRemoteResource({
|
|
77
|
+
url: '/api/path/to/resource',
|
|
78
|
+
state: (data, context) => ({
|
|
79
|
+
myResource: data,
|
|
80
|
+
isMyResourceReady: context.isReady,
|
|
81
|
+
}),
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const { myResource, isMyResourceReady } = useMyResource()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Dynamic URLs
|
|
88
|
+
|
|
89
|
+
You can define a dynamic URL by using a function that returns the URL string.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const useMyResource = defineRemoteResource({
|
|
93
|
+
url: (id: string) => `/api/path/to/resource/${id}`,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const { id } = defineProps<{
|
|
97
|
+
id: string
|
|
98
|
+
}>()
|
|
99
|
+
|
|
100
|
+
const { data } = useMyResource(() => id)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Polling and caching
|
|
104
|
+
|
|
105
|
+
By default, the resource will poll for updates every 30 seconds and cache the result for 10 seconds.
|
|
106
|
+
|
|
107
|
+
This can be customized by passing the `pollingIntervalMs` and `cacheDurationMs` properties:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const useMyResource = defineRemoteResource({
|
|
111
|
+
url: '/api/path/to/resource',
|
|
112
|
+
pollingIntervalMs: 60_000, // Poll every 60 seconds
|
|
113
|
+
cacheDurationMs: 5 * 60_000, // Cache for 5 minutes
|
|
114
|
+
})
|
|
115
|
+
```
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# `useThreshold`
|
|
2
|
+
|
|
3
|
+
`useThreshold` composable allows defining payloads for different thresholds.
|
|
4
|
+
|
|
5
|
+
It returns the payload of the highest matching threshold for that value.
|
|
6
|
+
|
|
7
|
+
## Arguments
|
|
8
|
+
|
|
9
|
+
| Argument | Type | Required | Description |
|
|
10
|
+
| -------------- | -------------------------------------------------------------------- | :------: | ------------------------------------- |
|
|
11
|
+
| `currentValue` | `MaybeRefOrGetter<number>` | ✓ | The value to check against thresholds |
|
|
12
|
+
| `config` | `MaybeRefOrGetter<Record<number, TPayload> & { default: TPayload }>` | ✓ | The thresholds configuration |
|
|
13
|
+
|
|
14
|
+
## Returns
|
|
15
|
+
|
|
16
|
+
`ComputedRef<{ value: number | undefined; payload: TPayload }>`
|
|
17
|
+
|
|
18
|
+
## Example
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
const threshold = useThreshold(value, {
|
|
22
|
+
50: 'orange',
|
|
23
|
+
100: 'red',
|
|
24
|
+
default: 'green',
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// value = 20 → threshold = { value: undefined, payload: 'green' }
|
|
28
|
+
// value = 70 → threshold = { value: 50, payload: 'orange' }
|
|
29
|
+
// value = 130 → threshold = { value: 100, payload: 'red' }
|
|
30
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ThresholdConfig, ThresholdResult } from '@core/packages/threshold/type.ts'
|
|
2
|
+
import { computed, type ComputedRef, type MaybeRefOrGetter, toValue } from 'vue'
|
|
3
|
+
|
|
4
|
+
export function useThreshold<TPayload>(
|
|
5
|
+
rawCurrentValue: MaybeRefOrGetter<number>,
|
|
6
|
+
rawConfig: MaybeRefOrGetter<ThresholdConfig<TPayload>>
|
|
7
|
+
): ComputedRef<ThresholdResult<TPayload>> {
|
|
8
|
+
const currentValue = computed(() => toValue(rawCurrentValue))
|
|
9
|
+
|
|
10
|
+
const config = computed(() => toValue(rawConfig))
|
|
11
|
+
|
|
12
|
+
return computed(
|
|
13
|
+
() =>
|
|
14
|
+
Object.entries(config.value)
|
|
15
|
+
.map(([value, payload]) => ({ value: value === 'default' ? -Infinity : Number(value), payload }))
|
|
16
|
+
.sort((a, b) => b.value - a.value)
|
|
17
|
+
.find(threshold => currentValue.value >= threshold.value)!
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -1,43 +1,3 @@
|
|
|
1
|
-
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
|
|
2
|
-
|
|
3
|
-
export type ObjectIconSize = 'extra-small' | 'small' | 'medium'
|
|
4
|
-
|
|
5
1
|
export type VmState = 'running' | 'halted' | 'paused' | 'suspended'
|
|
6
2
|
|
|
7
3
|
export type HostState = 'running' | 'halted' | 'maintenance'
|
|
8
|
-
|
|
9
|
-
export type SrState = 'connected' | 'partially-connected' | 'disconnected'
|
|
10
|
-
|
|
11
|
-
export type BackupRepositoryState = 'connected' | 'disconnected'
|
|
12
|
-
|
|
13
|
-
export type NetworkState = 'connected' | 'disconnected'
|
|
14
|
-
|
|
15
|
-
export type SupportedStateByType = {
|
|
16
|
-
host: HostState
|
|
17
|
-
vm: VmState
|
|
18
|
-
sr: SrState
|
|
19
|
-
'backup-repository': BackupRepositoryState
|
|
20
|
-
network: NetworkState
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type SupportedType = keyof SupportedStateByType
|
|
24
|
-
|
|
25
|
-
export type SupportedState<TType extends SupportedType> = SupportedStateByType[TType]
|
|
26
|
-
|
|
27
|
-
export type StatusConfig = {
|
|
28
|
-
icon: IconDefinition
|
|
29
|
-
color: `--${string}`
|
|
30
|
-
translate: {
|
|
31
|
-
x: [number, number, number]
|
|
32
|
-
y: [number, number, number]
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type TypeConfig<TType extends SupportedType> = {
|
|
37
|
-
mainIcon: IconDefinition
|
|
38
|
-
states: Record<SupportedState<TType>, StatusConfig>
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export type ObjectIconConfig = {
|
|
42
|
-
[K in SupportedType]: TypeConfig<K>
|
|
43
|
-
}
|
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.28.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"
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<DisplayIcon v-if="icon" class="vts-icon" :class="className" :icon />
|
|
3
|
-
</template>
|
|
4
|
-
|
|
5
|
-
<script lang="ts" setup>
|
|
6
|
-
import { type IconName, icons } from '@core/icons'
|
|
7
|
-
import { DisplayIcon } from '@core/packages/icon'
|
|
8
|
-
import { toVariants } from '@core/utils/to-variants.util.ts'
|
|
9
|
-
import { computed } from 'vue'
|
|
10
|
-
|
|
11
|
-
export type IconSize = 'small' | 'medium' | 'large' | 'current'
|
|
12
|
-
|
|
13
|
-
const { size, name } = defineProps<{
|
|
14
|
-
size: IconSize
|
|
15
|
-
name: IconName
|
|
16
|
-
}>()
|
|
17
|
-
|
|
18
|
-
const className = computed(() =>
|
|
19
|
-
toVariants({
|
|
20
|
-
size: size === 'current' ? undefined : size,
|
|
21
|
-
})
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
const icon = computed(() => {
|
|
25
|
-
const icon = icons[name]
|
|
26
|
-
|
|
27
|
-
if (icon === undefined) {
|
|
28
|
-
console.warn(`Icon "${name}" not found.`)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return icon
|
|
32
|
-
})
|
|
33
|
-
</script>
|
|
34
|
-
|
|
35
|
-
<style lang="postcss" scoped>
|
|
36
|
-
.vts-icon {
|
|
37
|
-
&.size--small {
|
|
38
|
-
font-size: 1.2rem;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
&.size--medium {
|
|
42
|
-
font-size: 1.6rem;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
&.size--large {
|
|
46
|
-
font-size: 2rem;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
</style>
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
<!-- v2 -->
|
|
2
|
-
<template>
|
|
3
|
-
<FontAwesomeLayers :class="toVariants({ size })" class="ui-complex-icon">
|
|
4
|
-
<slot />
|
|
5
|
-
</FontAwesomeLayers>
|
|
6
|
-
</template>
|
|
7
|
-
|
|
8
|
-
<script lang="ts" setup>
|
|
9
|
-
import { toVariants } from '@core/utils/to-variants.util'
|
|
10
|
-
import { FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
|
|
11
|
-
|
|
12
|
-
defineProps<{
|
|
13
|
-
size: 'small' | 'medium' | 'large'
|
|
14
|
-
}>()
|
|
15
|
-
</script>
|
|
16
|
-
|
|
17
|
-
<style lang="postcss" scoped>
|
|
18
|
-
.ui-complex-icon {
|
|
19
|
-
:nth-child(2) {
|
|
20
|
-
font-size: 0.5em;
|
|
21
|
-
transform: translate(100%, 80%);
|
|
22
|
-
|
|
23
|
-
:deep(path) {
|
|
24
|
-
stroke: var(--color-neutral-background-primary);
|
|
25
|
-
stroke-width: 100px;
|
|
26
|
-
stroke-linejoin: round;
|
|
27
|
-
paint-order: stroke;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/*SIZE VARIANTS*/
|
|
32
|
-
|
|
33
|
-
&.size--small {
|
|
34
|
-
font-size: 1.2rem;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
&.size--medium {
|
|
38
|
-
font-size: 1.6rem;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
&.size--large {
|
|
42
|
-
font-size: 2rem;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
</style>
|