@xen-orchestra/web-core 0.42.0 → 0.43.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/banner/VtsBanner.vue +50 -0
- package/lib/components/input-wrapper/VtsInputWrapper.vue +1 -0
- package/lib/components/ui/checkbox/UiCheckbox.vue +1 -1
- package/lib/components/ui/head-bar/UiHeadBar.vue +1 -0
- package/lib/components/ui/radio-button/UiRadioButton.vue +1 -1
- package/lib/components/ui/radio-button-group/UiRadioButtonGroup.vue +16 -3
- package/lib/components/ui/rich-radio-button/UiRichRadioButton.vue +247 -0
- package/lib/layouts/CoreLayout.vue +26 -0
- package/lib/locales/en.json +2 -0
- package/lib/locales/fr.json +2 -0
- package/lib/packages/remote-resource/define-remote-resource.ts +11 -9
- package/lib/packages/remote-resource/sse.store.ts +31 -3
- package/package.json +1 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="className" class="vts-banner">
|
|
3
|
+
<slot />
|
|
4
|
+
<div v-if="slots.addons">
|
|
5
|
+
<slot name="addons" />
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
import { toVariants } from '@core/utils/to-variants.util.ts'
|
|
12
|
+
import { computed } from 'vue'
|
|
13
|
+
|
|
14
|
+
export type BannerAccent = 'brand' | 'warning' | 'danger'
|
|
15
|
+
|
|
16
|
+
const { accent } = defineProps<{
|
|
17
|
+
accent: BannerAccent
|
|
18
|
+
}>()
|
|
19
|
+
|
|
20
|
+
const slots = defineSlots<{
|
|
21
|
+
default(): any
|
|
22
|
+
addons?(): any
|
|
23
|
+
}>()
|
|
24
|
+
|
|
25
|
+
const className = computed(() => toVariants({ accent }))
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<style scoped lang="postcss">
|
|
29
|
+
.vts-banner {
|
|
30
|
+
display: flex;
|
|
31
|
+
justify-content: center;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: 1.6rem;
|
|
34
|
+
padding: 0.8rem 1.6rem;
|
|
35
|
+
border-block-end: 0.1rem solid var(--color-neutral-border);
|
|
36
|
+
|
|
37
|
+
/* ACCENT */
|
|
38
|
+
&.accent--brand {
|
|
39
|
+
background-color: var(--color-brand-background-selected);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&.accent--warning {
|
|
43
|
+
background-color: var(--color-warning-background-selected);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&.accent--danger {
|
|
47
|
+
background-color: var(--color-danger-background-selected);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</style>
|
|
@@ -78,10 +78,10 @@ const attrs = useAttrs()
|
|
|
78
78
|
border-color 0.125s ease-in-out;
|
|
79
79
|
border: 0.2rem solid transparent;
|
|
80
80
|
border-radius: 0.2rem;
|
|
81
|
+
flex-shrink: 0;
|
|
81
82
|
|
|
82
83
|
.icon {
|
|
83
84
|
font-size: 0.75rem;
|
|
84
|
-
position: absolute;
|
|
85
85
|
color: var(--color-info-txt-item);
|
|
86
86
|
transition: opacity 0.125s ease-in-out;
|
|
87
87
|
}
|
|
@@ -41,6 +41,7 @@ const isDisabled = useDisabled(() => disabled)
|
|
|
41
41
|
.radio-container {
|
|
42
42
|
display: inline-flex;
|
|
43
43
|
align-items: center;
|
|
44
|
+
flex-shrink: 0;
|
|
44
45
|
justify-content: center;
|
|
45
46
|
border: 0.2rem solid var(--accent-color);
|
|
46
47
|
background-color: transparent;
|
|
@@ -80,7 +81,6 @@ const isDisabled = useDisabled(() => disabled)
|
|
|
80
81
|
|
|
81
82
|
.radio-icon {
|
|
82
83
|
font-size: 0.8rem;
|
|
83
|
-
position: absolute;
|
|
84
84
|
transition: font-size 0.125s ease-in-out;
|
|
85
85
|
color: var(--radio-icon-color);
|
|
86
86
|
--radio-icon-color: var(--color-neutral-background-primary);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
{{ label }}
|
|
7
7
|
</UiLabel>
|
|
8
8
|
</slot>
|
|
9
|
-
<div class="group" :class="{ vertical }">
|
|
9
|
+
<div class="group" :class="[className, { vertical }]">
|
|
10
10
|
<slot />
|
|
11
11
|
</div>
|
|
12
12
|
<slot v-if="slots.info || info !== undefined" name="info">
|
|
@@ -20,10 +20,12 @@
|
|
|
20
20
|
<script setup lang="ts">
|
|
21
21
|
import UiInfo from '@core/components/ui/info/UiInfo.vue'
|
|
22
22
|
import UiLabel from '@core/components/ui/label/UiLabel.vue'
|
|
23
|
+
import { toVariants } from '@core/utils/to-variants.util.ts'
|
|
23
24
|
import { computed } from 'vue'
|
|
24
25
|
|
|
25
|
-
const { accent } = defineProps<{
|
|
26
|
+
const { accent, gap } = defineProps<{
|
|
26
27
|
accent: 'brand' | 'warning' | 'danger'
|
|
28
|
+
gap: 'narrow' | 'wide'
|
|
27
29
|
label?: string
|
|
28
30
|
info?: string
|
|
29
31
|
vertical?: boolean
|
|
@@ -35,6 +37,7 @@ const slots = defineSlots<{
|
|
|
35
37
|
info?(): any
|
|
36
38
|
}>()
|
|
37
39
|
const labelAccent = computed(() => (accent === 'brand' ? 'neutral' : accent))
|
|
40
|
+
const className = computed(() => toVariants({ gap }))
|
|
38
41
|
</script>
|
|
39
42
|
|
|
40
43
|
<style scoped lang="postcss">
|
|
@@ -45,12 +48,22 @@ const labelAccent = computed(() => (accent === 'brand' ? 'neutral' : accent))
|
|
|
45
48
|
|
|
46
49
|
.group {
|
|
47
50
|
display: flex;
|
|
48
|
-
gap: 6.4rem;
|
|
49
51
|
|
|
50
52
|
&.vertical {
|
|
51
53
|
flex-direction: column;
|
|
52
54
|
gap: 0.8rem;
|
|
53
55
|
}
|
|
56
|
+
|
|
57
|
+
/* GAP */
|
|
58
|
+
|
|
59
|
+
&.gap--narrow {
|
|
60
|
+
flex-wrap: wrap;
|
|
61
|
+
gap: 0.4rem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&.gap--wide {
|
|
65
|
+
gap: 6.4rem;
|
|
66
|
+
}
|
|
54
67
|
}
|
|
55
68
|
}
|
|
56
69
|
</style>
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
<!-- v1 -->
|
|
2
|
+
<template>
|
|
3
|
+
<label :class="variants" class="ui-rich-radio-button typo-body-regular">
|
|
4
|
+
<span class="rich-image">
|
|
5
|
+
<img :src :alt />
|
|
6
|
+
</span>
|
|
7
|
+
<span class="radio-wrapper typo-body-regular">
|
|
8
|
+
<span class="radio-container">
|
|
9
|
+
<input v-model="model" :disabled="isDisabled" :value class="input" type="radio" />
|
|
10
|
+
<VtsIcon name="fa:circle" size="small" class="radio-icon" />
|
|
11
|
+
</span>
|
|
12
|
+
<slot />
|
|
13
|
+
</span>
|
|
14
|
+
</label>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script lang="ts" setup>
|
|
18
|
+
import VtsIcon from '@core/components/icon/VtsIcon.vue'
|
|
19
|
+
import { useDisabled } from '@core/composables/disabled.composable.ts'
|
|
20
|
+
import { toVariants } from '@core/utils/to-variants.util.ts'
|
|
21
|
+
import { computed } from 'vue'
|
|
22
|
+
|
|
23
|
+
const { accent, value, disabled, size } = defineProps<{
|
|
24
|
+
accent: 'brand' | 'warning' | 'danger'
|
|
25
|
+
size: 'small' | 'medium'
|
|
26
|
+
value: unknown
|
|
27
|
+
src: string
|
|
28
|
+
alt: string
|
|
29
|
+
disabled?: boolean
|
|
30
|
+
}>()
|
|
31
|
+
|
|
32
|
+
const model = defineModel<string>()
|
|
33
|
+
|
|
34
|
+
defineSlots<{
|
|
35
|
+
default(): any
|
|
36
|
+
}>()
|
|
37
|
+
|
|
38
|
+
const variants = computed(() => toVariants({ accent, size }))
|
|
39
|
+
const isDisabled = useDisabled(() => disabled)
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<style lang="postcss" scoped>
|
|
43
|
+
.ui-rich-radio-button {
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
border: 0.1rem solid var(--color-neutral-border);
|
|
46
|
+
border-radius: 0.4rem;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
|
|
49
|
+
.rich-image {
|
|
50
|
+
border-bottom: 0.1rem solid;
|
|
51
|
+
border-color: inherit;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
width: 100%;
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
|
|
58
|
+
img {
|
|
59
|
+
width: 100%;
|
|
60
|
+
height: 100%;
|
|
61
|
+
object-fit: cover;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.radio-container {
|
|
66
|
+
display: inline-flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
justify-content: center;
|
|
69
|
+
border: 0.2rem solid var(--accent-color);
|
|
70
|
+
background-color: transparent;
|
|
71
|
+
border-radius: 0.8rem;
|
|
72
|
+
width: 1.6rem;
|
|
73
|
+
height: 1.6rem;
|
|
74
|
+
position: relative;
|
|
75
|
+
transition:
|
|
76
|
+
border-color 0.125s ease-in-out,
|
|
77
|
+
background-color 0.125s ease-in-out;
|
|
78
|
+
|
|
79
|
+
&:has(input:focus-visible) {
|
|
80
|
+
outline: none;
|
|
81
|
+
|
|
82
|
+
&::after {
|
|
83
|
+
position: absolute;
|
|
84
|
+
content: '';
|
|
85
|
+
inset: -0.5rem;
|
|
86
|
+
border: 0.2rem solid var(--color-brand-txt-base);
|
|
87
|
+
border-radius: 0.4rem;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
&:has(.input:checked) {
|
|
92
|
+
border-color: var(--accent-checked-color);
|
|
93
|
+
background-color: var(--accent-checked-color);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
&:has(.input:checked:disabled) {
|
|
97
|
+
border-color: var(--color-neutral-txt-secondary);
|
|
98
|
+
background-color: var(--color-neutral-txt-secondary);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.input {
|
|
102
|
+
opacity: 0;
|
|
103
|
+
position: absolute;
|
|
104
|
+
pointer-events: none;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.radio-icon {
|
|
108
|
+
font-size: 0.8rem;
|
|
109
|
+
position: absolute;
|
|
110
|
+
transition: font-size 0.125s ease-in-out;
|
|
111
|
+
color: var(--radio-icon-color);
|
|
112
|
+
--radio-icon-color: var(--color-neutral-background-primary);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.input:not(:checked) + .radio-icon {
|
|
116
|
+
font-size: 1.2rem;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.input:disabled + .radio-icon {
|
|
120
|
+
--radio-icon-color: var(--color-neutral-background-disabled);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ACCENT */
|
|
125
|
+
|
|
126
|
+
&.accent--brand {
|
|
127
|
+
--accent-color: var(--color-brand-txt-base);
|
|
128
|
+
--accent-hover-color: var(--color-brand-txt-hover);
|
|
129
|
+
--accent-checked-color: var(--color-brand-item-base);
|
|
130
|
+
--accent-active-color: var(--color-brand-txt-active);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
&.accent--warning {
|
|
134
|
+
--accent-color: var(--color-warning-txt-base);
|
|
135
|
+
--accent-hover-color: var(--color-warning-txt-hover);
|
|
136
|
+
--accent-checked-color: var(--color-warning-item-base);
|
|
137
|
+
--accent-active-color: var(--color-warning-txt-active);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
&.accent--danger {
|
|
141
|
+
--accent-color: var(--color-danger-txt-base);
|
|
142
|
+
--accent-hover-color: var(--color-danger-txt-hover);
|
|
143
|
+
--accent-checked-color: var(--color-danger-item-base);
|
|
144
|
+
--accent-active-color: var(--color-danger-txt-active);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* DISABLED */
|
|
148
|
+
|
|
149
|
+
&:has(.input:disabled) {
|
|
150
|
+
cursor: not-allowed;
|
|
151
|
+
--accent-color: var(--color-neutral-txt-secondary);
|
|
152
|
+
}
|
|
153
|
+
.radio-wrapper {
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
gap: 0.8rem;
|
|
157
|
+
padding: 1.6rem;
|
|
158
|
+
background-color: var(--color-neutral-background-primary);
|
|
159
|
+
cursor: pointer;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* INITIAL BORDER EXCEPT BRAND */
|
|
163
|
+
|
|
164
|
+
&.accent--warning,
|
|
165
|
+
&.accent--danger {
|
|
166
|
+
border-color: var(--accent-color);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* CHECKED STATE */
|
|
170
|
+
|
|
171
|
+
&:has(input:checked:not(:disabled)) {
|
|
172
|
+
border-color: var(--accent-checked-color);
|
|
173
|
+
|
|
174
|
+
.radio-container {
|
|
175
|
+
border-color: var(--accent-checked-color);
|
|
176
|
+
background-color: var(--accent-checked-color);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* HOVER STATE */
|
|
181
|
+
|
|
182
|
+
&:hover:not(:has(input:disabled)) {
|
|
183
|
+
border-color: var(--accent-hover-color);
|
|
184
|
+
|
|
185
|
+
.radio-container {
|
|
186
|
+
border-color: var(--accent-hover-color);
|
|
187
|
+
}
|
|
188
|
+
.radio-container:has(.input:checked) {
|
|
189
|
+
background-color: var(--accent-hover-color);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* ACTIVE STATE */
|
|
194
|
+
|
|
195
|
+
&:active:not(:has(input:disabled)) {
|
|
196
|
+
.radio-container {
|
|
197
|
+
border-color: var(--accent-active-color);
|
|
198
|
+
}
|
|
199
|
+
.radio-container:has(.input:checked) {
|
|
200
|
+
background-color: var(--accent-active-color);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* DISABLED STATE */
|
|
205
|
+
|
|
206
|
+
&:has(input:disabled) {
|
|
207
|
+
border-color: var(--color-neutral-border);
|
|
208
|
+
|
|
209
|
+
.rich-image {
|
|
210
|
+
opacity: 0.5;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.radio-wrapper {
|
|
214
|
+
background-color: var(--color-neutral-background-disabled);
|
|
215
|
+
cursor: not-allowed;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* SIZES */
|
|
220
|
+
|
|
221
|
+
&.size--small {
|
|
222
|
+
height: 16.4rem;
|
|
223
|
+
width: 12.8rem;
|
|
224
|
+
|
|
225
|
+
.rich-image {
|
|
226
|
+
height: 10.8rem;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.radio-wrapper {
|
|
230
|
+
height: 5.6rem;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
&.size--medium {
|
|
235
|
+
height: 25.6rem;
|
|
236
|
+
width: 20rem;
|
|
237
|
+
|
|
238
|
+
.rich-image {
|
|
239
|
+
height: 20rem;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.radio-wrapper {
|
|
243
|
+
height: 5.6rem;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
</style>
|
|
@@ -16,6 +16,16 @@
|
|
|
16
16
|
/>
|
|
17
17
|
<slot name="app-header" />
|
|
18
18
|
</header>
|
|
19
|
+
<VtsBanner v-if="showBanner" accent="danger">
|
|
20
|
+
<UiInfo accent="danger">
|
|
21
|
+
{{ t('unable-to-connect-to-xo-server') }}
|
|
22
|
+
</UiInfo>
|
|
23
|
+
<template #addons>
|
|
24
|
+
<UiButton variant="primary" accent="brand" size="small" @click="handleRetry()">
|
|
25
|
+
{{ t('retry') }}
|
|
26
|
+
</UiButton>
|
|
27
|
+
</template>
|
|
28
|
+
</VtsBanner>
|
|
19
29
|
<div class="container">
|
|
20
30
|
<template v-if="uiStore.hasUi">
|
|
21
31
|
<VtsBackdrop
|
|
@@ -43,17 +53,33 @@
|
|
|
43
53
|
|
|
44
54
|
<script lang="ts" setup>
|
|
45
55
|
import VtsBackdrop from '@core/components/backdrop/VtsBackdrop.vue'
|
|
56
|
+
import VtsBanner from '@core/components/banner/VtsBanner.vue'
|
|
46
57
|
import VtsLayoutSidebar from '@core/components/layout/VtsLayoutSidebar.vue'
|
|
58
|
+
import UiButton from '@core/components/ui/button/UiButton.vue'
|
|
47
59
|
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
|
|
60
|
+
import UiInfo from '@core/components/ui/info/UiInfo.vue'
|
|
48
61
|
import { vTooltip } from '@core/directives/tooltip.directive'
|
|
62
|
+
import { useSseStore } from '@core/packages/remote-resource/sse.store.ts'
|
|
49
63
|
import { useSidebarStore } from '@core/stores/sidebar.store'
|
|
50
64
|
import { useUiStore } from '@core/stores/ui.store'
|
|
65
|
+
import { storeToRefs } from 'pinia'
|
|
66
|
+
import { computed } from 'vue'
|
|
51
67
|
import { useI18n } from 'vue-i18n'
|
|
52
68
|
|
|
53
69
|
const { t } = useI18n()
|
|
54
70
|
|
|
55
71
|
const uiStore = useUiStore()
|
|
56
72
|
const sidebarStore = useSidebarStore()
|
|
73
|
+
|
|
74
|
+
const sseStore = useSseStore()
|
|
75
|
+
|
|
76
|
+
const { hasErrorSse } = storeToRefs(sseStore)
|
|
77
|
+
|
|
78
|
+
const showBanner = computed(() => hasErrorSse.value)
|
|
79
|
+
|
|
80
|
+
function handleRetry() {
|
|
81
|
+
sseStore.retry()
|
|
82
|
+
}
|
|
57
83
|
</script>
|
|
58
84
|
|
|
59
85
|
<style lang="postcss" scoped>
|
package/lib/locales/en.json
CHANGED
|
@@ -661,6 +661,7 @@
|
|
|
661
661
|
"resources": "Resources",
|
|
662
662
|
"resources-overview": "Resources overview",
|
|
663
663
|
"rest-api": "REST API",
|
|
664
|
+
"retry": "Retry",
|
|
664
665
|
"root-by-default": "\"root\" by default.",
|
|
665
666
|
"run": "Run",
|
|
666
667
|
"running-vm": "Running VM | Running VMs",
|
|
@@ -791,6 +792,7 @@
|
|
|
791
792
|
"translation-tool": "Translation tool",
|
|
792
793
|
"unable-to-connect-to": "Unable to connect to {ip}",
|
|
793
794
|
"unable-to-connect-to-the-pool": "Unable to connect to the pool",
|
|
795
|
+
"unable-to-connect-to-xo-server": "Unable to connect to XO server.",
|
|
794
796
|
"unknown": "Unknown",
|
|
795
797
|
"unlocked": "Unlocked",
|
|
796
798
|
"unreachable-hosts": "Unreachable hosts",
|
package/lib/locales/fr.json
CHANGED
|
@@ -661,6 +661,7 @@
|
|
|
661
661
|
"resources": "Ressources",
|
|
662
662
|
"resources-overview": "Vue d'ensemble des ressources",
|
|
663
663
|
"rest-api": "API REST",
|
|
664
|
+
"retry": "Réessayer",
|
|
664
665
|
"root-by-default": "\"root\" par défaut.",
|
|
665
666
|
"run": "Run",
|
|
666
667
|
"running-vm": "VM en cours d'exécution | VMs en cours d'exécution",
|
|
@@ -791,6 +792,7 @@
|
|
|
791
792
|
"translation-tool": "Outil de traduction",
|
|
792
793
|
"unable-to-connect-to": "Impossible de se connecter à {ip}",
|
|
793
794
|
"unable-to-connect-to-the-pool": "Impossible de se connecter au Pool",
|
|
795
|
+
"unable-to-connect-to-xo-server": "Impossible de se connecter au serveur XO.",
|
|
794
796
|
"unknown": "Inconnu",
|
|
795
797
|
"unlocked": "Débloqué",
|
|
796
798
|
"unreachable-hosts": "Hôtes inaccessibles",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
useSseStore,
|
|
3
2
|
type THandleDelete,
|
|
4
3
|
type THandlePost,
|
|
5
4
|
type THandleWatching,
|
|
5
|
+
useSseStore,
|
|
6
6
|
} from '@core/packages/remote-resource/sse.store'
|
|
7
7
|
import type { ResourceContext, UseRemoteResource } from '@core/packages/remote-resource/types.ts'
|
|
8
8
|
import type { VoidFunction } from '@core/types/utility.type.ts'
|
|
@@ -63,7 +63,7 @@ export function defineRemoteResource<
|
|
|
63
63
|
onDataReceived?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
|
|
64
64
|
onDataRemoved?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
|
|
65
65
|
stream?: boolean
|
|
66
|
-
|
|
66
|
+
initWatchCollection: () => {
|
|
67
67
|
collectionId: string
|
|
68
68
|
resource: string // reactivity only on XAPI XO record for now
|
|
69
69
|
getIdentifier: (obj: unknown) => string
|
|
@@ -87,7 +87,7 @@ export function defineRemoteResource<
|
|
|
87
87
|
cacheExpirationMs?: number | false
|
|
88
88
|
pollingIntervalMs?: number | false
|
|
89
89
|
stream?: boolean
|
|
90
|
-
|
|
90
|
+
initWatchCollection?: () => {
|
|
91
91
|
collectionId: string
|
|
92
92
|
resource: string // reactivity only on XAPI XO record for now
|
|
93
93
|
getIdentifier: (obj: unknown) => string
|
|
@@ -121,8 +121,10 @@ export function defineRemoteResource<
|
|
|
121
121
|
|
|
122
122
|
const pollingInterval = config.pollingIntervalMs ?? DEFAULT_POLLING_INTERVAL_MS
|
|
123
123
|
|
|
124
|
+
const watchCollection = config.initWatchCollection?.()
|
|
125
|
+
|
|
124
126
|
const removeData = (data: TData[], dataToRemove: any) => {
|
|
125
|
-
const getIdentifier =
|
|
127
|
+
const getIdentifier = watchCollection?.getIdentifier ?? JSON.stringify
|
|
126
128
|
|
|
127
129
|
remove(data, item => {
|
|
128
130
|
if (typeof item === 'object') {
|
|
@@ -156,14 +158,14 @@ export function defineRemoteResource<
|
|
|
156
158
|
config.onDataReceived ??
|
|
157
159
|
((data: Ref<TData>, receivedData: any, context?: ResourceContext<TArgs>) => {
|
|
158
160
|
// allow to ignore some update (like for sub collection. E.g. vms/:id/vdis)
|
|
159
|
-
if (
|
|
161
|
+
if (watchCollection?.predicate?.(receivedData, context) === false) {
|
|
160
162
|
return
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
if (data.value === undefined || (Array.isArray(data.value) && Array.isArray(receivedData))) {
|
|
164
166
|
data.value = receivedData
|
|
165
167
|
|
|
166
|
-
if (
|
|
168
|
+
if (watchCollection !== undefined && Array.isArray(data.value)) {
|
|
167
169
|
handleBuffer(data as Ref<TData[]>)
|
|
168
170
|
isBufferEventsProcessed = true
|
|
169
171
|
}
|
|
@@ -187,7 +189,7 @@ export function defineRemoteResource<
|
|
|
187
189
|
config.onDataRemoved ??
|
|
188
190
|
((data: Ref<TData>, receivedData: any, context?: ResourceContext<TArgs>) => {
|
|
189
191
|
// allow to ignore some update (like for sub collection. E.g. vms/:id/vdis)
|
|
190
|
-
if (
|
|
192
|
+
if (watchCollection?.predicate?.(receivedData, context) === false) {
|
|
191
193
|
return
|
|
192
194
|
}
|
|
193
195
|
|
|
@@ -289,8 +291,8 @@ export function defineRemoteResource<
|
|
|
289
291
|
let pause: VoidFunction = noop
|
|
290
292
|
let resume: VoidFunction = execute
|
|
291
293
|
|
|
292
|
-
if (
|
|
293
|
-
const { collectionId, resource, handleDelete, handlePost, handleWatching } =
|
|
294
|
+
if (watchCollection !== undefined) {
|
|
295
|
+
const { collectionId, resource, handleDelete, handlePost, handleWatching } = watchCollection
|
|
294
296
|
const { watch, unwatch } = useSseStore()
|
|
295
297
|
|
|
296
298
|
pause = () => unwatch({ collectionId, resource, handleDelete })
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { useNow } from '@vueuse/core'
|
|
1
2
|
import { defineStore } from 'pinia'
|
|
2
|
-
import { ref, watch as watchVue } from 'vue'
|
|
3
|
+
import { computed, ref, watch as watchVue } from 'vue'
|
|
3
4
|
|
|
4
5
|
type EventFn = (object: unknown) => void
|
|
5
6
|
export type THandlePost = (sseId: string) => Promise<string>
|
|
@@ -22,7 +23,10 @@ export type THandleWatching = (
|
|
|
22
23
|
) => void
|
|
23
24
|
|
|
24
25
|
export const useSseStore = defineStore('sse', () => {
|
|
25
|
-
const sse = ref<{ id?: string; isWatching: boolean }>({
|
|
26
|
+
const sse = ref<{ id?: string; isWatching: boolean; lastPing?: number; errorSse?: unknown | null }>({
|
|
27
|
+
isWatching: false,
|
|
28
|
+
errorSse: null,
|
|
29
|
+
})
|
|
26
30
|
const configsByResource: Map<
|
|
27
31
|
string,
|
|
28
32
|
{
|
|
@@ -38,10 +42,30 @@ export const useSseStore = defineStore('sse', () => {
|
|
|
38
42
|
}
|
|
39
43
|
> = new Map()
|
|
40
44
|
|
|
45
|
+
const now = useNow({ interval: 1000 })
|
|
46
|
+
|
|
47
|
+
const isError = computed(() => {
|
|
48
|
+
if (!sse.value.lastPing) {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return now.value.getTime() - sse.value.lastPing > 32_000
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const hasErrorSse = computed(() => isError.value || sse.value.errorSse !== null)
|
|
56
|
+
|
|
57
|
+
function setErrorSse(error: unknown | null) {
|
|
58
|
+
sse.value.errorSse = error
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
function updateSseId(id: string) {
|
|
42
62
|
sse.value.id = id
|
|
43
63
|
}
|
|
44
64
|
|
|
65
|
+
function setPing(timestamp: number) {
|
|
66
|
+
sse.value.lastPing = timestamp
|
|
67
|
+
}
|
|
68
|
+
|
|
45
69
|
function getConfigsByResource(resource: string) {
|
|
46
70
|
return configsByResource.get(resource)
|
|
47
71
|
}
|
|
@@ -135,6 +159,10 @@ export const useSseStore = defineStore('sse', () => {
|
|
|
135
159
|
})
|
|
136
160
|
}
|
|
137
161
|
}
|
|
162
|
+
// TODO need to be improve
|
|
163
|
+
function retry() {
|
|
164
|
+
window.location.reload()
|
|
165
|
+
}
|
|
138
166
|
|
|
139
|
-
return { watch, unwatch }
|
|
167
|
+
return { watch, unwatch, retry, hasErrorSse, setErrorSse, setPing }
|
|
140
168
|
})
|