@xen-orchestra/web-core 0.16.0 → 0.18.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.
Files changed (44) hide show
  1. package/lib/components/console/VtsRemoteConsole.vue +1 -1
  2. package/lib/components/data-table/VtsDataTable.vue +13 -14
  3. package/lib/components/input-wrapper/VtsInputWrapper.vue +94 -0
  4. package/lib/components/{charts/LinearChart.md → linear-chart/VtsLinearChart.md} +3 -3
  5. package/lib/components/{charts/LinearChart.vue → linear-chart/VtsLinearChart.vue} +12 -11
  6. package/lib/components/menu/MenuList.vue +1 -0
  7. package/lib/components/quick-info-card/VtsQuickInfoCard.vue +29 -0
  8. package/lib/components/quick-info-column/VtsQuickInfoColumn.vue +13 -0
  9. package/lib/components/quick-info-row/VtsQuickInfoRow.vue +40 -0
  10. package/lib/components/state-hero/VtsLoadingHero.vue +1 -3
  11. package/lib/components/ui/card/UiCard.vue +12 -0
  12. package/lib/components/ui/dropdown/UiDropdown.vue +214 -0
  13. package/lib/components/ui/dropdown/UiDropdownList.vue +16 -0
  14. package/lib/components/ui/label/UiLabel.vue +3 -1
  15. package/lib/components/ui/progress-bar/UiProgressBar.vue +108 -0
  16. package/lib/components/ui/radio-button/UiRadioButton.vue +1 -1
  17. package/lib/components/ui/table-pagination/UiTablePagination.vue +23 -57
  18. package/lib/components/ui/toaster/UiToaster.vue +6 -2
  19. package/lib/composables/chart-theme.composable.ts +2 -2
  20. package/lib/composables/mapper.composable.md +74 -0
  21. package/lib/composables/mapper.composable.ts +18 -0
  22. package/lib/composables/pagination.composable.md +32 -0
  23. package/lib/composables/pagination.composable.ts +91 -0
  24. package/lib/composables/ranked.composable.md +118 -0
  25. package/lib/composables/ranked.composable.ts +37 -0
  26. package/lib/composables/relative-time.composable.md +18 -0
  27. package/lib/composables/relative-time.composable.ts +66 -0
  28. package/lib/i18n.ts +12 -0
  29. package/lib/locales/cs.json +49 -17
  30. package/lib/locales/de.json +233 -24
  31. package/lib/locales/en.json +69 -3
  32. package/lib/locales/es.json +42 -10
  33. package/lib/locales/fa.json +208 -6
  34. package/lib/locales/fr.json +68 -2
  35. package/lib/locales/it.json +178 -0
  36. package/lib/locales/ru.json +91 -0
  37. package/lib/locales/sv.json +25 -0
  38. package/lib/locales/uk.json +1 -0
  39. package/lib/utils/if-else.utils.ts +1 -1
  40. package/lib/utils/injection-keys.util.ts +3 -2
  41. package/lib/utils/time.util.ts +18 -0
  42. package/package.json +22 -22
  43. package/lib/components/dropdown/DropdownItem.vue +0 -163
  44. package/lib/components/dropdown/DropdownList.vue +0 -31
@@ -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>
@@ -2,12 +2,12 @@
2
2
 
3
3
  ```vue
4
4
  <template>
5
- <LinearChart :data="data" :value-formatter="customValueFormatter" />
5
+ <VtsLinearChart :data :value-formatter="customValueFormatter" />
6
6
  </template>
7
7
 
8
8
  <script lang="ts" setup>
9
- import type { LinearChartData } from '@/types/chart'
10
- import LinearChart from '@/components/charts/LinearChart.vue'
9
+ import type { LinearChartData } from '@core/types/chart'
10
+ import VstLinearChart from '@core/components/linear-chart/VtsLinearChart.vue'
11
11
 
12
12
  const data: LinearChartData = [
13
13
  {
@@ -1,20 +1,23 @@
1
1
  <template>
2
- <VueCharts :option autoresize class="chart" />
2
+ <VueCharts :option autoresize class="vts-linear-chart" />
3
3
  </template>
4
4
 
5
5
  <script lang="ts" setup>
6
6
  import type { LinearChartData, ValueFormatter } from '@core/types/chart'
7
- import { IK_CHART_VALUE_FORMATTER } from '@core/utils/injection-keys.util'
8
7
  import { utcFormat } from 'd3-time-format'
9
8
  import type { EChartsOption } from 'echarts'
10
9
  import { LineChart } from 'echarts/charts'
11
10
  import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
12
11
  import { use } from 'echarts/core'
13
12
  import { CanvasRenderer } from 'echarts/renderers'
14
- import { computed, provide } from 'vue'
13
+ import { computed } from 'vue'
15
14
  import VueCharts from 'vue-echarts'
16
15
 
17
- const props = defineProps<{
16
+ const {
17
+ data,
18
+ valueFormatter: _valueFormatter,
19
+ maxValue,
20
+ } = defineProps<{
18
21
  data: LinearChartData
19
22
  valueFormatter?: ValueFormatter
20
23
  maxValue?: number
@@ -23,7 +26,7 @@ const props = defineProps<{
23
26
  const Y_AXIS_MAX_VALUE = 200
24
27
 
25
28
  const valueFormatter = computed<ValueFormatter>(() => {
26
- const formatter = props.valueFormatter
29
+ const formatter = _valueFormatter
27
30
 
28
31
  return value => {
29
32
  if (formatter === undefined) {
@@ -34,13 +37,11 @@ const valueFormatter = computed<ValueFormatter>(() => {
34
37
  }
35
38
  })
36
39
 
37
- provide(IK_CHART_VALUE_FORMATTER, valueFormatter)
38
-
39
40
  use([CanvasRenderer, LineChart, GridComponent, TooltipComponent, LegendComponent])
40
41
 
41
42
  const option = computed<EChartsOption>(() => ({
42
43
  legend: {
43
- data: props.data.map(series => series.label),
44
+ data: data.map(series => series.label),
44
45
  },
45
46
  tooltip: {
46
47
  valueFormatter: v => valueFormatter.value(v as number),
@@ -58,9 +59,9 @@ const option = computed<EChartsOption>(() => ({
58
59
  axisLabel: {
59
60
  formatter: valueFormatter.value,
60
61
  },
61
- max: props.maxValue ?? Y_AXIS_MAX_VALUE,
62
+ max: maxValue ?? Y_AXIS_MAX_VALUE,
62
63
  },
63
- series: props.data.map((series, index) => ({
64
+ series: data.map((series, index) => ({
64
65
  type: 'line',
65
66
  name: series.label,
66
67
  zlevel: index + 1,
@@ -70,7 +71,7 @@ const option = computed<EChartsOption>(() => ({
70
71
  </script>
71
72
 
72
73
  <style lang="postcss" scoped>
73
- .chart {
74
+ .vts-linear-chart {
74
75
  width: 100%;
75
76
  height: 30rem;
76
77
  }
@@ -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), {
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <UiCard class="vts-quick-info-card">
3
+ <UiCardTitle>{{ $t('quick-info') }}</UiCardTitle>
4
+ <VtsLoadingHero v-if="loading" type="card" />
5
+ <div v-else class="info-container">
6
+ <slot />
7
+ </div>
8
+ </UiCard>
9
+ </template>
10
+
11
+ <script lang="ts" setup>
12
+ import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
13
+ import UiCard from '@core/components/ui/card/UiCard.vue'
14
+ import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
15
+
16
+ defineProps<{
17
+ loading: boolean
18
+ }>()
19
+ </script>
20
+
21
+ <style lang="postcss" scoped>
22
+ .vts-quick-info-card {
23
+ .info-container {
24
+ display: grid;
25
+ grid-template-columns: repeat(auto-fit, minmax(25rem, 1fr));
26
+ gap: 2.4rem;
27
+ }
28
+ }
29
+ </style>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <div class="vts-quick-info-column">
3
+ <slot />
4
+ </div>
5
+ </template>
6
+
7
+ <style lang="postcss" scoped>
8
+ .vts-quick-info-column {
9
+ display: flex;
10
+ flex-direction: column;
11
+ gap: 0.8rem;
12
+ }
13
+ </style>
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <div class="vts-quick-info-row">
3
+ <span class="typo-body-bold">
4
+ <slot name="label">
5
+ {{ label }}
6
+ </slot>
7
+ </span>
8
+ <span v-tooltip class="typo-body-regular value text-ellipsis">
9
+ <slot name="value">
10
+ {{ value }}
11
+ </slot>
12
+ </span>
13
+ </div>
14
+ </template>
15
+
16
+ <script lang="ts" setup>
17
+ import { vTooltip } from '@core/directives/tooltip.directive'
18
+
19
+ defineProps<{
20
+ label?: string
21
+ value?: string
22
+ }>()
23
+
24
+ defineSlots<{
25
+ label?(): any
26
+ value?(): any
27
+ }>()
28
+ </script>
29
+
30
+ <style lang="postcss" scoped>
31
+ .vts-quick-info-row {
32
+ display: flex;
33
+ align-items: center;
34
+ gap: 1rem;
35
+
36
+ .value:empty::before {
37
+ content: '-';
38
+ }
39
+ }
40
+ </style>
@@ -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>
@@ -24,6 +24,18 @@ defineSlots<{
24
24
  border: 0.1rem solid var(--color-neutral-border);
25
25
  border-radius: 0.8rem;
26
26
 
27
+ &:has(> .vts-linear-chart) {
28
+ padding-inline: 0;
29
+
30
+ > *:not(.vts-linear-chart) {
31
+ padding-inline: 2.4rem;
32
+ }
33
+
34
+ :deep(.vts-linear-chart) {
35
+ padding-inline-end: 2.4rem;
36
+ }
37
+ }
38
+
27
39
  &.horizontal {
28
40
  flex-direction: row;
29
41
  }
@@ -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