@xen-orchestra/web-core 0.16.0 → 0.17.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.
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :class="uiStore.isMobile ? 'mobile' : undefined" class="vts-remote-console">
3
- <VtsLoadingHero :disabled="isReady" type="panel" />
3
+ <VtsLoadingHero v-if="!isReady" type="panel" />
4
4
  <div ref="console-container" class="console" />
5
5
  </div>
6
6
  </template>
@@ -1,24 +1,23 @@
1
1
  <template>
2
2
  <div class="table-container">
3
- <VtsLoadingHero :disabled="isReady" type="table">
4
- <VtsTable vertical-border>
5
- <thead>
6
- <slot name="thead" />
7
- </thead>
8
- <tbody>
9
- <slot name="tbody" />
10
- </tbody>
11
- </VtsTable>
12
- </VtsLoadingHero>
13
- <VtsErrorNoDataHero v-if="isReady && hasError" type="table" />
14
- <VtsStateHero v-if="isReady && noDataMessage" type="table" image="no-data" />
3
+ <VtsLoadingHero v-if="!isReady" type="table" />
4
+ <VtsErrorNoDataHero v-else-if="hasError" type="table" />
5
+ <VtsNoDataHero v-else-if="noDataMessage" type="table" />
6
+ <VtsTable v-else vertical-border>
7
+ <thead>
8
+ <slot name="thead" />
9
+ </thead>
10
+ <tbody>
11
+ <slot name="tbody" />
12
+ </tbody>
13
+ </VtsTable>
15
14
  </div>
16
15
  </template>
17
16
 
18
- <script setup lang="ts">
17
+ <script lang="ts" setup>
19
18
  import VtsErrorNoDataHero from '@core/components/state-hero/VtsErrorNoDataHero.vue'
20
19
  import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
21
- import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
20
+ import VtsNoDataHero from '@core/components/state-hero/VtsNoDataHero.vue'
22
21
  import VtsTable from '@core/components/table/VtsTable.vue'
23
22
 
24
23
  defineProps<{
@@ -0,0 +1,94 @@
1
+ <template>
2
+ <div class="vts-input-wrapper">
3
+ <UiLabel
4
+ :accent="labelAccent"
5
+ :for="id"
6
+ :href="learnMoreUrl"
7
+ :icon
8
+ :required="wrapperController.required"
9
+ class="label"
10
+ >
11
+ <slot name="label">{{ label }}</slot>
12
+ </UiLabel>
13
+ <slot />
14
+ <UiInfo v-for="{ content, accent } of messages" :key="content" :accent>
15
+ {{ content }}
16
+ </UiInfo>
17
+ </div>
18
+ </template>
19
+
20
+ <script lang="ts" setup>
21
+ import UiInfo, { type InfoAccent } from '@core/components/ui/info/UiInfo.vue'
22
+ import UiLabel, { type LabelAccent } from '@core/components/ui/label/UiLabel.vue'
23
+ import { useMapper } from '@core/composables/mapper.composable'
24
+ import { useRanked } from '@core/composables/ranked.composable.ts'
25
+ import type { MaybeArray } from '@core/types/utility.type'
26
+ import { IK_INPUT_WRAPPER_CONTROLLER } from '@core/utils/injection-keys.util'
27
+ import { toArray } from '@core/utils/to-array.utils'
28
+ import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
29
+ import { useArrayMap } from '@vueuse/core'
30
+ import { provide, reactive, useId } from 'vue'
31
+
32
+ export type InputWrapperMessage = MaybeArray<string | { content: string; accent?: InfoAccent }>
33
+
34
+ export type InputWrapperController = {
35
+ id: string
36
+ labelAccent: LabelAccent
37
+ required: boolean
38
+ }
39
+
40
+ const { message: _message } = defineProps<{
41
+ label?: string
42
+ learnMoreUrl?: string
43
+ icon?: IconDefinition
44
+ message?: InputWrapperMessage
45
+ }>()
46
+
47
+ defineSlots<{
48
+ default(): any
49
+ label?(): any
50
+ }>()
51
+
52
+ const id = useId()
53
+
54
+ const unsortedMessages = useArrayMap(
55
+ () => toArray(_message),
56
+ item => ({
57
+ content: typeof item === 'object' ? item.content : item,
58
+ accent: typeof item === 'object' ? (item.accent ?? 'info') : 'info',
59
+ })
60
+ )
61
+
62
+ const messages = useRanked(unsortedMessages, ({ accent }) => accent, ['danger', 'warning', 'success', 'info'])
63
+
64
+ const labelAccent = useMapper<InfoAccent, LabelAccent>(
65
+ () => messages.value[0]?.accent,
66
+ {
67
+ info: 'neutral',
68
+ success: 'neutral',
69
+ warning: 'warning',
70
+ danger: 'danger',
71
+ },
72
+ 'neutral'
73
+ )
74
+
75
+ const wrapperController = reactive({
76
+ id,
77
+ labelAccent,
78
+ required: false,
79
+ }) satisfies InputWrapperController
80
+
81
+ provide(IK_INPUT_WRAPPER_CONTROLLER, wrapperController)
82
+ </script>
83
+
84
+ <style lang="postcss" scoped>
85
+ .vts-input-wrapper {
86
+ display: flex;
87
+ flex-direction: column;
88
+ gap: 0.4rem;
89
+
90
+ .label {
91
+ min-height: 2.4rem;
92
+ }
93
+ }
94
+ </style>
@@ -74,6 +74,7 @@ const open = (event: MouseEvent) => {
74
74
  nextTick(() => {
75
75
  clearClickOutsideEvent = onClickOutside(menu, () => (isOpen.value = false), {
76
76
  ignore: [event.currentTarget as HTMLElement],
77
+ controls: false,
77
78
  })
78
79
 
79
80
  placementJs(event.currentTarget as HTMLElement, unrefElement(menu), {
@@ -1,8 +1,7 @@
1
1
  <template>
2
- <VtsStateHero v-if="!disabled" :type busy class="vts-loading-hero">
2
+ <VtsStateHero :type busy>
3
3
  {{ $t('loading-in-progress') }}
4
4
  </VtsStateHero>
5
- <slot v-else />
6
5
  </template>
7
6
 
8
7
  <script lang="ts" setup>
@@ -10,6 +9,5 @@ import VtsStateHero, { type StateHeroType } from '@core/components/state-hero/Vt
10
9
 
11
10
  defineProps<{
12
11
  type: StateHeroType
13
- disabled?: boolean
14
12
  }>()
15
13
  </script>
@@ -0,0 +1,214 @@
1
+ <!-- v7 -->
2
+ <template>
3
+ <div v-tooltip="{ selector: '.text-ellipsis' }" :class="className" class="ui-dropdown">
4
+ <UiCheckbox v-if="checkbox" :disabled :model-value="selected" accent="brand" />
5
+ <slot name="icon">
6
+ <VtsIcon :icon accent="current" />
7
+ </slot>
8
+ <div class="text-ellipsis typo-body-bold-small">
9
+ <slot />
10
+ </div>
11
+ <div v-if="info" class="info typo-body-regular-small">{{ info }}</div>
12
+ <VtsIcon
13
+ v-if="subMenuIcon || locked"
14
+ :accent="disabled ? 'current' : 'brand'"
15
+ :icon="locked ? faLock : faAngleRight"
16
+ class="right-icon"
17
+ fixed-width
18
+ />
19
+ </div>
20
+ </template>
21
+
22
+ <script lang="ts" setup>
23
+ import VtsIcon from '@core/components/icon/VtsIcon.vue'
24
+ import UiCheckbox from '@core/components/ui/checkbox/UiCheckbox.vue'
25
+ import { vTooltip } from '@core/directives/tooltip.directive'
26
+ import { toVariants } from '@core/utils/to-variants.util'
27
+ import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
28
+ import { faAngleRight, faLock } from '@fortawesome/free-solid-svg-icons'
29
+ import { computed } from 'vue'
30
+
31
+ export type DropdownAccent = 'normal' | 'brand' | 'success' | 'warning' | 'danger'
32
+
33
+ const {
34
+ accent,
35
+ disabled: _disabled,
36
+ locked,
37
+ selected,
38
+ hover,
39
+ } = defineProps<{
40
+ accent: DropdownAccent
41
+ disabled?: boolean
42
+ locked?: boolean
43
+ selected?: boolean
44
+ hover?: boolean
45
+ checkbox?: boolean
46
+ subMenuIcon?: boolean
47
+ icon?: IconDefinition
48
+ info?: string
49
+ }>()
50
+
51
+ const disabled = computed(() => _disabled || locked)
52
+
53
+ const className = computed(() =>
54
+ toVariants({
55
+ accent,
56
+ disabled: disabled.value,
57
+ selected,
58
+ hover,
59
+ })
60
+ )
61
+ </script>
62
+
63
+ <style lang="postcss" scoped>
64
+ .ui-dropdown {
65
+ display: flex;
66
+ align-items: center;
67
+ padding: 1.2rem;
68
+ gap: 0.8rem;
69
+ height: 4.5rem;
70
+ cursor: default;
71
+
72
+ &.accent--normal {
73
+ color: var(--color-neutral-txt-primary);
74
+ background-color: var(--color-neutral-background-primary);
75
+
76
+ &.selected {
77
+ background-color: var(--color-brand-background-selected);
78
+ }
79
+
80
+ &:is(:hover, .hover) {
81
+ background-color: var(--color-brand-background-hover);
82
+ }
83
+
84
+ &:active {
85
+ background-color: var(--color-brand-background-active);
86
+ }
87
+
88
+ &.disabled {
89
+ color: var(--color-neutral-txt-secondary);
90
+ background-color: var(--color-neutral-background-disabled);
91
+ }
92
+ }
93
+
94
+ &.accent--brand {
95
+ color: var(--color-brand-txt-base);
96
+ background-color: var(--color-neutral-background-primary);
97
+
98
+ &.selected {
99
+ color: var(--color-brand-txt-base);
100
+ background-color: var(--color-brand-background-selected);
101
+ }
102
+
103
+ &:is(:hover, .hover) {
104
+ color: var(--color-brand-txt-hover);
105
+ background-color: var(--color-brand-background-hover);
106
+ }
107
+
108
+ &:active {
109
+ color: var(--color-brand-txt-active);
110
+ background-color: var(--color-brand-background-active);
111
+ }
112
+
113
+ &.disabled {
114
+ color: var(--color-neutral-txt-secondary);
115
+ background-color: var(--color-neutral-background-disabled);
116
+ }
117
+ }
118
+
119
+ &.accent--success {
120
+ color: var(--color-success-txt-base);
121
+ background-color: var(--color-neutral-background-primary);
122
+
123
+ &.selected {
124
+ color: var(--color-success-txt-base);
125
+ background-color: var(--color-success-background-selected);
126
+ }
127
+
128
+ &:is(:hover, .hover) {
129
+ color: var(--color-success-txt-hover);
130
+ background-color: var(--color-success-background-hover);
131
+ }
132
+
133
+ &:active {
134
+ color: var(--color-success-txt-active);
135
+ background-color: var(--color-success-background-active);
136
+ }
137
+
138
+ &.disabled {
139
+ color: var(--color-neutral-txt-secondary);
140
+ background-color: var(--color-neutral-background-disabled);
141
+ }
142
+ }
143
+
144
+ &.accent--warning {
145
+ color: var(--color-warning-txt-base);
146
+ background-color: var(--color-neutral-background-primary);
147
+
148
+ &.selected {
149
+ color: var(--color-warning-txt-base);
150
+ background-color: var(--color-warning-background-selected);
151
+ }
152
+
153
+ &:is(:hover, .hover) {
154
+ color: var(--color-warning-txt-hover);
155
+ background-color: var(--color-warning-background-hover);
156
+ }
157
+
158
+ &:active {
159
+ color: var(--color-warning-txt-active);
160
+ background-color: var(--color-warning-background-active);
161
+ }
162
+
163
+ &.disabled {
164
+ color: var(--color-neutral-txt-secondary);
165
+ background-color: var(--color-neutral-background-disabled);
166
+ }
167
+ }
168
+
169
+ &.accent--danger {
170
+ color: var(--color-danger-txt-base);
171
+ background-color: var(--color-neutral-background-primary);
172
+
173
+ &.selected {
174
+ color: var(--color-danger-txt-base);
175
+ background-color: var(--color-danger-background-selected);
176
+ }
177
+
178
+ &:is(:hover, .hover) {
179
+ color: var(--color-danger-txt-hover);
180
+ background-color: var(--color-danger-background-hover);
181
+ }
182
+
183
+ &:active {
184
+ color: var(--color-danger-txt-active);
185
+ background-color: var(--color-danger-background-active);
186
+ }
187
+
188
+ &.disabled {
189
+ color: var(--color-neutral-txt-secondary);
190
+ background-color: var(--color-neutral-background-disabled);
191
+ }
192
+ }
193
+
194
+ &:focus-visible {
195
+ outline: none;
196
+
197
+ &::before {
198
+ content: '';
199
+ position: absolute;
200
+ inset: 0.2rem;
201
+ border: 0.2rem solid var(--color-info-txt-base);
202
+ border-radius: 0.4rem;
203
+ }
204
+ }
205
+
206
+ .info {
207
+ color: var(--color-neutral-txt-secondary);
208
+ }
209
+
210
+ .right-icon {
211
+ font-size: 1.2rem;
212
+ }
213
+ }
214
+ </style>
@@ -0,0 +1,16 @@
1
+ <template>
2
+ <div class="ui-dropdown-list">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <style lang="postcss" scoped>
8
+ .ui-dropdown-list {
9
+ display: flex;
10
+ flex-direction: column;
11
+ overflow: auto;
12
+ background: var(--color-neutral-background-primary);
13
+ border: 0.1rem solid var(--color-neutral-border);
14
+ border-radius: 0.4rem;
15
+ }
16
+ </style>
@@ -15,8 +15,10 @@ import UiLink from '@core/components/ui/link/UiLink.vue'
15
15
  import { toVariants } from '@core/utils/to-variants.util'
16
16
  import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
17
17
 
18
+ export type LabelAccent = 'neutral' | 'warning' | 'danger'
19
+
18
20
  const { for: htmlFor } = defineProps<{
19
- accent: 'neutral' | 'warning' | 'danger'
21
+ accent: LabelAccent
20
22
  for?: string
21
23
  icon?: IconDefinition
22
24
  required?: boolean
@@ -0,0 +1,108 @@
1
+ <!-- v2 -->
2
+ <template>
3
+ <div class="ui-progress-bar" :class="className">
4
+ <div class="progress-bar">
5
+ <div class="fill" :style="{ width: `${fillWidth}%` }" />
6
+ </div>
7
+ <div v-if="shouldShowSteps" class="steps typo-body-regular-small">
8
+ <span>{{ $n(0, 'percent') }}</span>
9
+ <span v-for="step in steps" :key="step">{{ $n(step, 'percent') }}</span>
10
+ </div>
11
+ <VtsLegendList class="legend">
12
+ <UiLegend :accent :value="Math.round(percentage)" unit="%">{{ legend }}</UiLegend>
13
+ </VtsLegendList>
14
+ </div>
15
+ </template>
16
+
17
+ <script lang="ts" setup>
18
+ import VtsLegendList from '@core/components/legend-list/VtsLegendList.vue'
19
+ import UiLegend from '@core/components/ui/legend/UiLegend.vue'
20
+ import { toVariants } from '@core/utils/to-variants.util'
21
+ import { useClamp, useMax } from '@vueuse/math'
22
+ import { computed } from 'vue'
23
+
24
+ const {
25
+ value: _value,
26
+ max = 100,
27
+ showSteps,
28
+ } = defineProps<{
29
+ legend: string
30
+ value: number
31
+ max?: number
32
+ showSteps?: boolean
33
+ }>()
34
+
35
+ const value = useMax(0, () => _value)
36
+
37
+ const percentage = computed(() => (max <= 0 ? 0 : (value.value / max) * 100))
38
+ const maxPercentage = computed(() => Math.ceil(percentage.value / 100) * 100)
39
+ const fillWidth = useClamp(() => (percentage.value / maxPercentage.value) * 100 || 0, 0, 100)
40
+ const shouldShowSteps = computed(() => showSteps || percentage.value > 100)
41
+ const steps = useMax(1, () => Math.floor(maxPercentage.value / 100))
42
+
43
+ const accent = computed(() => {
44
+ if (percentage.value >= 90) {
45
+ return 'danger'
46
+ }
47
+
48
+ if (percentage.value >= 80) {
49
+ return 'warning'
50
+ }
51
+
52
+ return 'info'
53
+ })
54
+
55
+ const className = computed(() => toVariants({ accent: accent.value }))
56
+ </script>
57
+
58
+ <style lang="postcss" scoped>
59
+ .ui-progress-bar {
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 0.4rem;
63
+
64
+ .progress-bar {
65
+ width: 100%;
66
+ height: 1.2rem;
67
+ border-radius: 0.4rem;
68
+ overflow: hidden;
69
+ background-color: var(--color-neutral-background-disabled);
70
+
71
+ .fill {
72
+ width: 0;
73
+ height: 100%;
74
+ transition: width 0.25s ease-in-out;
75
+ }
76
+ }
77
+
78
+ .steps {
79
+ color: var(--color-neutral-txt-secondary);
80
+ display: flex;
81
+ justify-content: space-between;
82
+ }
83
+
84
+ .legend {
85
+ margin-inline-start: auto;
86
+ }
87
+
88
+ /* ACCENT */
89
+
90
+ &.accent--info {
91
+ .fill {
92
+ background-color: var(--color-info-item-base);
93
+ }
94
+ }
95
+
96
+ &.accent--warning {
97
+ .fill {
98
+ background-color: var(--color-warning-item-base);
99
+ }
100
+ }
101
+
102
+ &.accent--danger {
103
+ .fill {
104
+ background-color: var(--color-danger-item-base);
105
+ }
106
+ }
107
+ }
108
+ </style>
@@ -2,27 +2,27 @@
2
2
  <template>
3
3
  <div class="ui-table-pagination">
4
4
  <div class="buttons-container">
5
- <PaginationButton :disabled="isFirstPage || disabled" :icon="faAngleDoubleLeft" @click="goToFirstPage()" />
6
- <PaginationButton :disabled="isFirstPage || disabled" :icon="faAngleLeft" @click="goToPreviousPage()" />
7
- <PaginationButton :disabled="isLastPage || disabled" :icon="faAngleRight" @click="goToNextPage()" />
8
- <PaginationButton :disabled="isLastPage || disabled" :icon="faAngleDoubleRight" @click="goToLastPage()" />
5
+ <PaginationButton :disabled="isFirstPage" :icon="faAngleDoubleLeft" @click="emit('first')" />
6
+ <PaginationButton :disabled="isFirstPage" :icon="faAngleLeft" @click="emit('previous')" />
7
+ <PaginationButton :disabled="isLastPage" :icon="faAngleRight" @click="emit('next')" />
8
+ <PaginationButton :disabled="isLastPage" :icon="faAngleDoubleRight" @click="emit('last')" />
9
9
  </div>
10
10
  <span class="typo-body-regular-small label">
11
- {{ $t('core.select.n-object-of', { from: startIndex, to: endIndex, total: totalItems }) }}
11
+ {{ $t('core.select.n-object-of', { from, to, total }) }}
12
12
  </span>
13
- <span class="typo-body-regular-small label show">{{ $t('core.show-by') }}</span>
13
+ <span class="typo-body-regular-small label show">{{ $t('core.pagination.show-by') }}</span>
14
14
  <div class="dropdown-wrapper">
15
- <select v-model="pageSize" :disabled class="dropdown typo-body-regular-small" @change="goToFirstPage">
16
- <option v-for="option in pageSizeOptions" :key="option" :value="option" class="typo-body-bold-small">
17
- {{ option }}
15
+ <select v-model="showBy" class="dropdown typo-body-regular-small">
16
+ <option v-for="option in [50, 100, 150, 200, -1]" :key="option" :value="option" class="typo-body-bold-small">
17
+ {{ option === -1 ? $t('core.pagination.all') : option }}
18
18
  </option>
19
19
  </select>
20
- <VtsIcon class="icon" accent="current" :icon="faAngleDown" />
20
+ <VtsIcon :icon="faAngleDown" accent="current" class="icon" />
21
21
  </div>
22
22
  </div>
23
23
  </template>
24
24
 
25
- <script setup lang="ts">
25
+ <script lang="ts" setup>
26
26
  import VtsIcon from '@core/components/icon/VtsIcon.vue'
27
27
  import PaginationButton from '@core/components/ui/table-pagination/PaginationButton.vue'
28
28
  import {
@@ -32,60 +32,26 @@ import {
32
32
  faAngleLeft,
33
33
  faAngleRight,
34
34
  } from '@fortawesome/free-solid-svg-icons'
35
- import { useOffsetPagination } from '@vueuse/core'
36
- import { computed, ref, watch } from 'vue'
37
-
38
- export type PaginationPayload = {
39
- currentPage: number
40
- pageSize: number
41
- startIndex: number
42
- endIndex: number
43
- }
44
35
 
45
- const { totalItems, disabled = false } = defineProps<{
46
- totalItems: number
47
- disabled?: boolean
36
+ defineProps<{
37
+ from: number
38
+ to: number
39
+ total: number
40
+ isFirstPage: boolean
41
+ isLastPage: boolean
48
42
  }>()
49
43
 
50
44
  const emit = defineEmits<{
51
- change: [payload: PaginationPayload]
45
+ first: []
46
+ previous: []
47
+ next: []
48
+ last: []
52
49
  }>()
53
50
 
54
- const pageSize = ref(50)
55
- const pageSizeOptions = [10, 50, 100, 150, 200]
56
- const {
57
- currentPage,
58
- currentPageSize,
59
- pageCount,
60
- isFirstPage,
61
- isLastPage,
62
- prev: goToPreviousPage,
63
- next: goToNextPage,
64
- } = useOffsetPagination({
65
- total: () => totalItems,
66
- pageSize,
67
- })
68
- const startIndex = computed(() => (currentPage.value - 1) * currentPageSize.value + 1)
69
- const endIndex = computed(() => Math.min(currentPage.value * currentPageSize.value, totalItems))
70
-
71
- const goToFirstPage = () => {
72
- currentPage.value = 1
73
- }
74
- const goToLastPage = () => {
75
- currentPage.value = pageCount.value
76
- }
77
-
78
- watch([currentPage, currentPageSize], ([newPage, newPageSize]) => {
79
- emit('change', {
80
- currentPage: newPage,
81
- pageSize: newPageSize,
82
- startIndex: startIndex.value,
83
- endIndex: endIndex.value,
84
- })
85
- })
51
+ const showBy = defineModel<number>('showBy', { default: 50 })
86
52
  </script>
87
53
 
88
- <style scoped lang="postcss">
54
+ <style lang="postcss" scoped>
89
55
  .ui-table-pagination {
90
56
  display: flex;
91
57
  align-items: center;