@xen-orchestra/web-core 0.0.4 → 0.1.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 (37) hide show
  1. package/lib/components/CardNumbers.vue +15 -8
  2. package/lib/components/LegendTitle.vue +9 -7
  3. package/lib/components/backup-item/BackupItem.vue +37 -0
  4. package/lib/components/{StatusPill.vue → backup-state/BackupState.vue} +5 -3
  5. package/lib/components/button/ButtonIcon.vue +23 -2
  6. package/lib/components/console/RemoteConsole.vue +6 -4
  7. package/lib/components/{DonutChart.vue → donut-chart/DonutChart.vue} +28 -24
  8. package/lib/components/donut-chart-with-legend/DonutChartWithLegend.vue +27 -0
  9. package/lib/components/legend/LegendGroup.vue +44 -0
  10. package/lib/components/{UiLegend.vue → legend/LegendItem.vue} +35 -17
  11. package/lib/components/legend/LegendList.vue +28 -0
  12. package/lib/components/{search-bar/SearchBar.vue → query-search-bar/QuerySearchBar.vue} +5 -5
  13. package/lib/components/stacked-bar/StackedBar.vue +9 -15
  14. package/lib/components/stacked-bar/StackedBarSegment.vue +9 -6
  15. package/lib/components/stacked-bar-with-legend/StackedBarWithLegend.vue +47 -0
  16. package/lib/components/state-hero/ComingSoonHero.vue +6 -2
  17. package/lib/components/state-hero/LoadingHero.vue +8 -2
  18. package/lib/components/state-hero/ObjectNotFoundHero.vue +1 -1
  19. package/lib/components/state-hero/StateHero.vue +27 -9
  20. package/lib/components/table/ColumnTitle.vue +2 -2
  21. package/lib/components/tree/TreeItemLabel.vue +1 -11
  22. package/lib/composables/item-counter.composable.md +25 -0
  23. package/lib/composables/item-counter.composable.ts +32 -0
  24. package/lib/composables/route-query/actions/handle-add.ts +9 -0
  25. package/lib/composables/route-query/actions/handle-delete.ts +16 -0
  26. package/lib/composables/route-query/actions/handle-set.ts +17 -0
  27. package/lib/composables/route-query/actions/handle-toggle.ts +18 -0
  28. package/lib/composables/route-query/types.ts +42 -0
  29. package/lib/composables/route-query.composable.ts +42 -0
  30. package/lib/locales/de.json +2 -0
  31. package/lib/locales/en.json +6 -3
  32. package/lib/locales/fa.json +2 -0
  33. package/lib/locales/fr.json +6 -3
  34. package/lib/stores/ui.store.ts +1 -1
  35. package/lib/types/backup.type.ts +11 -0
  36. package/lib/types/utility.type.ts +6 -0
  37. package/package.json +2 -2
@@ -3,12 +3,12 @@
3
3
  <div class="card-numbers" :class="size">
4
4
  <span class="label typo" :class="labelFontClass">{{ label }}</span>
5
5
  <div class="values" :class="size">
6
- <span v-if="size === 'small' && max" class="value typo c2-semi-bold">
7
- {{ $n(valueAsPercentage, 'percent') }}
6
+ <span v-if="percentValue" class="value typo c2-semi-bold">
7
+ {{ percentValue }}
8
8
  </span>
9
9
 
10
10
  <div class="value typo" :class="valueFontClass">
11
- {{ value }}<span class="unit typo" :class="unitFontClass">{{ unit }}</span>
11
+ {{ value ?? '-' }}<span class="unit typo" :class="unitFontClass">{{ unit }}</span>
12
12
  </div>
13
13
  </div>
14
14
  </div>
@@ -16,29 +16,36 @@
16
16
 
17
17
  <script setup lang="ts" generic="TSize extends 'small' | 'medium'">
18
18
  import { computed } from 'vue'
19
+ import { useI18n } from 'vue-i18n'
19
20
 
20
21
  interface CardNumbersProps {
21
22
  label: string
22
- value: number
23
23
  size: TSize
24
+ value?: number
24
25
  unit?: string
25
26
  max?: TSize extends 'small' ? number : never
26
27
  }
27
28
 
28
29
  const props = defineProps<CardNumbersProps>()
29
30
 
31
+ const { n } = useI18n()
32
+
30
33
  const labelFontClass = computed(() => (props.size === 'medium' ? 'c3-semi-bold' : 'c2-semi-bold'))
31
34
 
32
35
  const valueFontClass = computed(() => (props.size === 'medium' ? 'h3-semi-bold' : 'c2-semi-bold'))
33
36
 
34
37
  const unitFontClass = computed(() => (props.size === 'medium' ? 'p2-medium' : 'c2-semi-bold'))
35
38
 
36
- const valueAsPercentage = computed(() => {
37
- if (props.max === undefined) {
38
- return 0
39
+ const percentValue = computed(() => {
40
+ if (props.size !== 'small' || props.max === undefined) {
41
+ return undefined
42
+ }
43
+
44
+ if (props.value === undefined) {
45
+ return n(0, 'percent').replace('0', '-')
39
46
  }
40
47
 
41
- return props.value / props.max
48
+ return n(props.value / props.max, 'percent')
42
49
  })
43
50
  </script>
44
51
 
@@ -2,18 +2,24 @@
2
2
  <template>
3
3
  <div class="legend-title typo c3-semi-bold">
4
4
  <slot />
5
- <UiIcon v-tooltip="iconTooltip ?? false" :icon color="info" class="tooltip-icon" />
5
+ <UiIcon v-tooltip="iconTooltip ?? false" :icon color="info" />
6
6
  </div>
7
7
  </template>
8
8
 
9
- <script setup lang="ts">
9
+ <script lang="ts" setup>
10
10
  import UiIcon from '@core/components/icon/UiIcon.vue'
11
11
  import { vTooltip } from '@core/directives/tooltip.directive'
12
12
  import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
13
13
 
14
- defineProps<{
14
+ export type LegendTitleProps = {
15
15
  iconTooltip?: string
16
16
  icon?: IconDefinition
17
+ }
18
+
19
+ defineProps<LegendTitleProps>()
20
+
21
+ defineSlots<{
22
+ default(): void
17
23
  }>()
18
24
  </script>
19
25
 
@@ -24,8 +30,4 @@ defineProps<{
24
30
  gap: 0.8rem;
25
31
  align-items: center;
26
32
  }
27
-
28
- .tooltip-icon {
29
- font-size: 1.2rem;
30
- }
31
33
  </style>
@@ -0,0 +1,37 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <div class="backup-item">
4
+ <RouterLink :to="backup.route">
5
+ {{ backup.label }}
6
+ </RouterLink>
7
+ <div class="states">
8
+ <BackupState v-for="(state, index) in backup.states" :key="index" :state />
9
+ </div>
10
+ </div>
11
+ </template>
12
+
13
+ <script lang="ts" setup>
14
+ import BackupState from '@core/components/backup-state/BackupState.vue'
15
+ import type { Backup } from '@core/types/backup.type'
16
+
17
+ defineProps<{
18
+ backup: Backup
19
+ }>()
20
+ </script>
21
+
22
+ <style lang="postcss" scoped>
23
+ .backup-item {
24
+ padding: 0.8rem 0.4rem;
25
+ border-top: 0.1rem solid var(--color-grey-500);
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 0.2rem;
29
+ }
30
+
31
+ .states {
32
+ margin-inline-start: auto;
33
+ display: flex;
34
+ align-items: center;
35
+ gap: 0.2rem;
36
+ }
37
+ </style>
@@ -1,16 +1,18 @@
1
+ <!-- v1.0 -->
1
2
  <template>
2
- <UiIcon :color :icon class="status-pill" />
3
+ <UiIcon :color :icon class="backup-state" />
3
4
  </template>
4
5
 
5
6
  <script lang="ts" setup>
6
7
  import UiIcon from '@core/components/icon/UiIcon.vue'
8
+ import type { BackupState } from '@core/types/backup.type'
7
9
  import type { Color } from '@core/types/color.type'
8
10
  import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
9
11
  import { faCheckCircle, faCircleMinus, faCircleXmark } from '@fortawesome/free-solid-svg-icons'
10
12
  import { computed } from 'vue'
11
13
 
12
14
  type Props = {
13
- state: 'success' | 'partial' | 'failure'
15
+ state: BackupState
14
16
  }
15
17
 
16
18
  const props = defineProps<Props>()
@@ -26,7 +28,7 @@ const color = computed(() => states[props.state].color)
26
28
  </script>
27
29
 
28
30
  <style lang="postcss" scoped>
29
- .status-pill {
31
+ .backup-state {
30
32
  font-size: 1rem;
31
33
  }
32
34
  </style>
@@ -10,8 +10,9 @@
10
10
  import UiIcon from '@core/components/icon/UiIcon.vue'
11
11
  import type { Color } from '@core/types/color.type'
12
12
  import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
13
+ import { computed } from 'vue'
13
14
 
14
- withDefaults(
15
+ const props = withDefaults(
15
16
  defineProps<{
16
17
  icon: IconDefinition
17
18
  size?: 'small' | 'medium' | 'large'
@@ -19,9 +20,18 @@ withDefaults(
19
20
  disabled?: boolean
20
21
  active?: boolean
21
22
  dot?: boolean
23
+ targetScale?: number | { x: number; y: number }
22
24
  }>(),
23
- { color: 'info', size: 'medium' }
25
+ { color: 'info', size: 'medium', targetScale: 1 }
24
26
  )
27
+
28
+ const cssTargetScale = computed(() => {
29
+ if (typeof props.targetScale === 'number') {
30
+ return `scale(${props.targetScale})`
31
+ }
32
+
33
+ return `scale(${props.targetScale.x}, ${props.targetScale.y})`
34
+ })
25
35
  </script>
26
36
 
27
37
  <style lang="postcss" scoped>
@@ -196,4 +206,15 @@ withDefaults(
196
206
  right: var(--dot-offset);
197
207
  }
198
208
  }
209
+
210
+ /*
211
+ * Increase the size of the clickable area,
212
+ * without changing the padding of the ButtonIcon component
213
+ */
214
+ .button-icon::after {
215
+ content: '';
216
+ position: absolute;
217
+ inset: 0;
218
+ transform: v-bind(cssTargetScale);
219
+ }
199
220
  </style>
@@ -23,7 +23,7 @@ const consoleContainer = ref<HTMLDivElement | null>(null)
23
23
  let vncClient: VncClient | undefined
24
24
  let nConnectionAttempts = 0
25
25
 
26
- const handleDisconnectionEvent = () => {
26
+ function handleDisconnectionEvent() {
27
27
  clearVncClient()
28
28
 
29
29
  nConnectionAttempts++
@@ -41,9 +41,11 @@ const handleDisconnectionEvent = () => {
41
41
  createVncConnection()
42
42
  }
43
43
 
44
- const handleConnectionEvent = () => (nConnectionAttempts = 0)
44
+ function handleConnectionEvent() {
45
+ nConnectionAttempts = 0
46
+ }
45
47
 
46
- const clearVncClient = () => {
48
+ function clearVncClient() {
47
49
  if (vncClient === undefined) {
48
50
  return
49
51
  }
@@ -55,7 +57,7 @@ const clearVncClient = () => {
55
57
  vncClient = undefined
56
58
  }
57
59
 
58
- const createVncConnection = async () => {
60
+ async function createVncConnection() {
59
61
  if (nConnectionAttempts !== 0) {
60
62
  await promiseTimeout(FIBONACCI_MS_ARRAY[nConnectionAttempts - 1])
61
63
 
@@ -17,51 +17,47 @@
17
17
  </svg>
18
18
  </template>
19
19
 
20
- <script setup lang="ts">
20
+ <script lang="ts" setup>
21
21
  import UiIcon from '@core/components/icon/UiIcon.vue'
22
22
  import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
23
23
  import { computed } from 'vue'
24
24
 
25
- type Segment = {
25
+ export type DonutSegmentColor = 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'disabled'
26
+
27
+ export type DonutSegment = {
26
28
  value: number
27
- color: 'success' | 'warning' | 'error' | 'unknown'
29
+ color: DonutSegmentColor
28
30
  }
29
31
 
30
- type ComputedSegment = {
31
- color: 'success' | 'warning' | 'error' | 'unknown'
32
- percent: number
33
- offset: number
32
+ export type DonutChartProps = {
33
+ segments: DonutSegment[]
34
+ icon?: IconDefinition
34
35
  }
35
36
 
36
- const props = defineProps<{
37
- segments: Segment[]
38
- maxValue?: number
39
- icon?: IconDefinition
40
- }>()
37
+ const props = defineProps<DonutChartProps>()
41
38
 
42
39
  const circumference = Math.PI * 80
43
40
 
44
- const totalValue = computed(() => {
45
- const sumOfValues = props.segments.reduce((acc, segment) => acc + segment.value, 0)
46
- return Math.max(props.maxValue ?? 0, sumOfValues)
47
- })
41
+ const totalValue = computed(() => props.segments.reduce((total, segment) => total + segment.value, 0))
48
42
 
49
- const computedSegments = computed<ComputedSegment[]>(() => {
43
+ const computedSegments = computed(() => {
50
44
  let nextOffset = circumference / 4
45
+
51
46
  return props.segments.map(segment => {
47
+ const offset = nextOffset
52
48
  const percent = (segment.value / totalValue.value) * circumference
53
- const currentSegment = {
49
+ nextOffset -= percent
50
+
51
+ return {
54
52
  color: segment.color,
55
53
  percent,
56
- offset: nextOffset,
54
+ offset,
57
55
  }
58
- nextOffset -= percent
59
- return currentSegment
60
56
  })
61
57
  })
62
58
  </script>
63
59
 
64
- <style scoped lang="postcss">
60
+ <style lang="postcss" scoped>
65
61
  .donut-chart {
66
62
  width: 100px;
67
63
  height: 100px;
@@ -73,6 +69,14 @@ const computedSegments = computed<ComputedSegment[]>(() => {
73
69
  fill: transparent;
74
70
  --stroke-color: var(--color-grey-100);
75
71
 
72
+ &.primary {
73
+ --stroke-color: var(--color-purple-base);
74
+ }
75
+
76
+ &.secondary {
77
+ --stroke-color: var(--color-grey-100);
78
+ }
79
+
76
80
  &.success {
77
81
  --stroke-color: var(--color-green-base);
78
82
  }
@@ -81,11 +85,11 @@ const computedSegments = computed<ComputedSegment[]>(() => {
81
85
  --stroke-color: var(--color-orange-base);
82
86
  }
83
87
 
84
- &.error {
88
+ &.danger {
85
89
  --stroke-color: var(--color-red-base);
86
90
  }
87
91
 
88
- &.unknown {
92
+ &.disabled {
89
93
  --stroke-color: var(--color-grey-400);
90
94
  }
91
95
  }
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <div class="donut-chart-with-legend">
3
+ <DonutChart :icon :segments />
4
+ <LegendGroup :items="segments" :title />
5
+ </div>
6
+ </template>
7
+
8
+ <script lang="ts" setup>
9
+ import DonutChart, { type DonutChartProps } from '@core/components/donut-chart/DonutChart.vue'
10
+ import LegendGroup, { type LegendGroupProps } from '@core/components/legend/LegendGroup.vue'
11
+
12
+ export type DonutChartWithLegendProps = {
13
+ segments: (DonutChartProps['segments'][number] & LegendGroupProps['items'][number])[]
14
+ title?: LegendGroupProps['title']
15
+ icon?: DonutChartProps['icon']
16
+ }
17
+
18
+ defineProps<DonutChartWithLegendProps>()
19
+ </script>
20
+
21
+ <style lang="postcss" scoped>
22
+ .donut-chart-with-legend {
23
+ display: flex;
24
+ align-items: center;
25
+ gap: 3.2rem;
26
+ }
27
+ </style>
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <div class="legend-group">
3
+ <LegendTitle v-if="title" :icon="title.icon" :icon-tooltip="title.iconTooltip">
4
+ {{ title.label }}
5
+ </LegendTitle>
6
+ <LegendList class="list">
7
+ <LegendItem
8
+ v-for="item in items"
9
+ :key="item.label"
10
+ :color="item.color"
11
+ :tooltip="item.tooltip"
12
+ :unit="item.unit"
13
+ :value="item.value"
14
+ >
15
+ {{ item.label }}
16
+ </LegendItem>
17
+ </LegendList>
18
+ </div>
19
+ </template>
20
+
21
+ <script lang="ts" setup>
22
+ import LegendItem, { type LegendItemProps } from '@core/components/legend/LegendItem.vue'
23
+ import LegendList from '@core/components/legend/LegendList.vue'
24
+ import LegendTitle, { type LegendTitleProps } from '@core/components/LegendTitle.vue'
25
+
26
+ export type LegendGroupProps = {
27
+ items: (LegendItemProps & { label: string })[]
28
+ title?: LegendTitleProps & { label: string }
29
+ }
30
+
31
+ defineProps<LegendGroupProps>()
32
+ </script>
33
+
34
+ <style lang="postcss" scoped>
35
+ .legend-group {
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: 0.8rem;
39
+ }
40
+
41
+ .list {
42
+ padding-left: 1.6rem;
43
+ }
44
+ </style>
@@ -1,67 +1,85 @@
1
1
  <!-- v1.0 -->
2
2
  <template>
3
- <li :class="color" class="ui-legend">
3
+ <li :class="color" class="legend-item">
4
4
  <UiIcon :icon="faCircle" class="circle-icon" />
5
5
  <span class="label typo p3-regular"><slot /></span>
6
6
  <UiIcon v-if="tooltip" v-tooltip="tooltip" :icon="faCircleInfo" class="tooltip-icon" color="info" />
7
-
8
- <span class="value-and-unit typo c3-semi-bold">{{ value }} {{ unit }}</span>
7
+ <span v-if="valueLabel" class="value-and-unit typo c3-semi-bold">{{ valueLabel }}</span>
9
8
  </li>
10
9
  </template>
11
10
 
12
- <script setup lang="ts">
11
+ <script lang="ts" setup>
13
12
  import UiIcon from '@core/components/icon/UiIcon.vue'
14
13
  import { vTooltip } from '@core/directives/tooltip.directive'
15
- import { faCircleInfo, faCircle } from '@fortawesome/free-solid-svg-icons'
14
+ import { faCircle, faCircleInfo } from '@fortawesome/free-solid-svg-icons'
15
+ import { computed } from 'vue'
16
16
 
17
- type LegendColor = 'default' | 'success' | 'warning' | 'error' | 'disabled' | 'dark-blue'
17
+ export type LegendItemColor = 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'disabled'
18
18
 
19
- defineProps<{
20
- color: LegendColor
19
+ export type LegendItemProps = {
20
+ color: LegendItemColor
21
21
  value?: number
22
22
  unit?: string
23
23
  tooltip?: string
24
+ }
25
+
26
+ const props = defineProps<LegendItemProps>()
27
+
28
+ defineSlots<{
29
+ default(): void
24
30
  }>()
31
+
32
+ const valueLabel = computed(() => [props.value, props.unit].join(' ').trim())
25
33
  </script>
26
34
 
27
35
  <style lang="postcss" scoped>
28
36
  /* COLOR VARIANTS */
29
- .ui-legend {
37
+ .legend-item {
38
+ &.primary {
39
+ --circle-color: var(--color-purple-base);
40
+ }
41
+
42
+ &.secondary {
43
+ --circle-color: var(--color-grey-100);
44
+ }
45
+
30
46
  &.success {
31
47
  --circle-color: var(--color-green-base);
32
48
  }
49
+
33
50
  &.warning {
34
51
  --circle-color: var(--color-orange-base);
35
52
  }
36
- &.error {
53
+
54
+ &.danger {
37
55
  --circle-color: var(--color-red-base);
38
56
  }
39
- &.default {
40
- --circle-color: var(--color-purple-base);
41
- }
57
+
42
58
  &.disabled {
43
59
  --circle-color: var(--color-grey-300);
44
60
  }
45
- &.dark-blue {
46
- --circle-color: var(--color-grey-100);
47
- }
48
61
  }
62
+
49
63
  /* IMPLEMENTATION */
50
- .ui-legend {
64
+ .legend-item {
51
65
  display: flex;
52
66
  align-items: center;
53
67
  gap: 0.8rem;
54
68
  }
69
+
55
70
  .circle-icon {
56
71
  font-size: 0.8rem;
57
72
  color: var(--circle-color);
58
73
  }
74
+
59
75
  .tooltip-icon {
60
76
  font-size: 1.2rem;
61
77
  }
78
+
62
79
  .label {
63
80
  color: var(--color-grey-000);
64
81
  }
82
+
65
83
  .value-and-unit {
66
84
  color: var(--color-grey-300);
67
85
  }
@@ -0,0 +1,28 @@
1
+ <template>
2
+ <ul class="legend-list" :class="{ horizontal }">
3
+ <slot />
4
+ </ul>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ defineProps<{
9
+ horizontal?: boolean
10
+ }>()
11
+
12
+ defineSlots<{
13
+ default(): void
14
+ }>()
15
+ </script>
16
+
17
+ <style lang="postcss" scoped>
18
+ .legend-list {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: 0.4rem;
22
+
23
+ &.horizontal {
24
+ flex-direction: row;
25
+ gap: 4rem;
26
+ }
27
+ }
28
+ </style>
@@ -1,21 +1,21 @@
1
- <!-- v1.1 -->
1
+ <!-- v1.2 -->
2
2
  <template>
3
3
  <form class="search-bar" @submit.prevent="emit('search', value)">
4
4
  <label v-if="uiStore.isDesktop" :for="id" class="typo p2-regular label">
5
- {{ $t('core.search-bar.label') }}
5
+ {{ $t('core.query-search-bar.label') }}
6
6
  </label>
7
7
  <UiInput
8
8
  :id
9
9
  v-model="value"
10
- :aria-label="uiStore.isMobile ? $t('core.search-bar.label') : undefined"
10
+ :aria-label="uiStore.isMobile ? $t('core.query-search-bar.label') : undefined"
11
11
  :icon="uiStore.isDesktop ? faMagnifyingGlass : undefined"
12
- :placeholder="$t('core.search-bar.placeholder')"
12
+ :placeholder="$t('core.query-search-bar.placeholder')"
13
13
  />
14
14
  <template v-if="uiStore.isDesktop">
15
15
  <UiButton type="submit">{{ $t('core.search') }}</UiButton>
16
16
  <Divider type="stretch" />
17
17
  <UiButton v-tooltip="$t('coming-soon')" level="secondary" :left-icon="faFilter" disabled>
18
- {{ $t('core.search-bar.use-query-builder') }}
18
+ {{ $t('core.query-search-bar.use-query-builder') }}
19
19
  </UiButton>
20
20
  </template>
21
21
  <template v-else>
@@ -2,39 +2,33 @@
2
2
  <template>
3
3
  <div class="stacked-bar">
4
4
  <StackedBarSegment
5
- v-for="(segment, index) in computedSegments"
5
+ v-for="(segment, index) in segments"
6
6
  :key="index"
7
7
  :color="segment.color"
8
- :percentage="segment.percentage"
8
+ :percentage="(segment.value / max) * 100"
9
9
  />
10
10
  </div>
11
11
  </template>
12
12
 
13
13
  <script lang="ts" setup>
14
- import StackedBarSegment from '@core/components/stacked-bar/StackedBarSegment.vue'
15
- import type { Color } from '@core/types/color.type'
14
+ import StackedBarSegment, { type StackedBarSegmentProps } from '@core/components/stacked-bar/StackedBarSegment.vue'
16
15
  import { computed } from 'vue'
17
16
 
18
17
  type Segment = {
19
18
  value: number
20
- color: Color
19
+ color: StackedBarSegmentProps['color']
21
20
  }
22
21
 
23
- const props = defineProps<{
22
+ export type StackedBarProps = {
24
23
  segments: Segment[]
25
24
  maxValue?: number
26
- }>()
25
+ }
27
26
 
28
- const totalValue = computed(() => props.segments.reduce((acc, bar) => acc + bar.value, 0))
27
+ const props = defineProps<StackedBarProps>()
29
28
 
30
- const max = computed(() => Math.max(props.maxValue ?? 0, totalValue.value))
29
+ const totalValue = computed(() => props.segments.reduce((acc, segment) => acc + segment.value, 0))
31
30
 
32
- const computedSegments = computed(() => {
33
- return props.segments.map(segment => ({
34
- ...segment,
35
- percentage: (segment.value / max.value) * 100,
36
- }))
37
- })
31
+ const max = computed(() => Math.max(props.maxValue ?? 0, totalValue.value))
38
32
  </script>
39
33
 
40
34
  <style lang="postcss" scoped>
@@ -13,15 +13,18 @@
13
13
 
14
14
  <script lang="ts" setup>
15
15
  import { vTooltip } from '@core/directives/tooltip.directive'
16
- import type { Color } from '@core/types/color.type'
17
16
  import { hasEllipsis } from '@core/utils/has-ellipsis.util'
18
17
  import { useResizeObserver } from '@vueuse/core'
19
18
  import { ref } from 'vue'
20
19
 
21
- defineProps<{
22
- color: Color
20
+ export type StackedBarSegmentColor = 'primary' | 'success' | 'warning' | 'danger'
21
+
22
+ export type StackedBarSegmentProps = {
23
+ color: StackedBarSegmentColor
23
24
  percentage: number
24
- }>()
25
+ }
26
+
27
+ defineProps<StackedBarSegmentProps>()
25
28
 
26
29
  const hidden = ref(false)
27
30
  const ellipsisElement = ref<HTMLElement | null>(null)
@@ -34,7 +37,7 @@ useResizeObserver(ellipsisElement, ([entry]) => {
34
37
  <style lang="postcss" scoped>
35
38
  /* COLOR VARIANT */
36
39
  .stacked-bar-segment {
37
- &.info {
40
+ &.primary {
38
41
  --background-color: var(--color-purple-base);
39
42
  }
40
43
 
@@ -46,7 +49,7 @@ useResizeObserver(ellipsisElement, ([entry]) => {
46
49
  --background-color: var(--color-orange-base);
47
50
  }
48
51
 
49
- &.error {
52
+ &.danger {
50
53
  --background-color: var(--color-red-base);
51
54
  }
52
55
  }
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <div class="stacked-bar-with-legend">
3
+ <StackedBar :segments :max-value="maxValue" />
4
+ <LegendList class="list" :horizontal="uiStore.isDesktop">
5
+ <LegendItem
6
+ v-for="segment in segments"
7
+ :key="segment.label"
8
+ :color="segment.color"
9
+ :tooltip="segment.tooltip"
10
+ :unit="segment.unit"
11
+ :value="segment.value"
12
+ >
13
+ {{ segment.label }}
14
+ </LegendItem>
15
+ </LegendList>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import LegendItem, { type LegendItemProps } from '@core/components/legend/LegendItem.vue'
21
+ import LegendList from '@core/components/legend/LegendList.vue'
22
+ import StackedBar, { type StackedBarProps } from '@core/components/stacked-bar/StackedBar.vue'
23
+ import { useUiStore } from '@core/stores/ui.store'
24
+
25
+ type Segment = StackedBarProps['segments'][number] & LegendItemProps & { label: string }
26
+
27
+ export type StackedBarWithLegendProps = {
28
+ segments: Segment[]
29
+ maxValue?: StackedBarProps['maxValue']
30
+ }
31
+
32
+ defineProps<StackedBarWithLegendProps>()
33
+
34
+ const uiStore = useUiStore()
35
+ </script>
36
+
37
+ <style scoped lang="postcss">
38
+ .stacked-bar-with-legend {
39
+ display: flex;
40
+ flex-direction: column;
41
+ gap: 0.4rem;
42
+ }
43
+
44
+ .list {
45
+ margin-inline-start: auto;
46
+ }
47
+ </style>
@@ -1,9 +1,13 @@
1
1
  <template>
2
- <StateHero image="under-construction" class="coming-soon-hero">
2
+ <StateHero :type class="coming-soon-hero" image="under-construction">
3
3
  {{ $t('coming-soon') }}
4
4
  </StateHero>
5
5
  </template>
6
6
 
7
7
  <script lang="ts" setup>
8
- import StateHero from '@core/components/state-hero/StateHero.vue'
8
+ import StateHero, { type StateHeroType } from '@core/components/state-hero/StateHero.vue'
9
+
10
+ defineProps<{
11
+ type: StateHeroType
12
+ }>()
9
13
  </script>
@@ -1,9 +1,15 @@
1
1
  <template>
2
- <StateHero busy class="loading-hero">
2
+ <StateHero v-if="!disabled" :type busy class="loading-hero">
3
3
  {{ $t('loading-in-progress') }}
4
4
  </StateHero>
5
+ <slot v-else />
5
6
  </template>
6
7
 
7
8
  <script lang="ts" setup>
8
- import StateHero from '@core/components/state-hero/StateHero.vue'
9
+ import StateHero, { type StateHeroType } from '@core/components/state-hero/StateHero.vue'
10
+
11
+ defineProps<{
12
+ type: StateHeroType
13
+ disabled?: boolean
14
+ }>()
9
15
  </script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <StateHero image="no-result" class="object-not-found-hero">
2
+ <StateHero class="object-not-found-hero" image="no-result" type="page">
3
3
  {{ $t('object-not-found', { id }) }}
4
4
  </StateHero>
5
5
  </template>
@@ -1,8 +1,8 @@
1
1
  <template>
2
- <div class="state-hero">
2
+ <div :class="type" class="state-hero">
3
3
  <UiSpinner v-if="busy" class="spinner" />
4
4
  <img v-else-if="imageSrc" :src="imageSrc" alt="" class="image" />
5
- <p v-if="$slots.default" class="typo h2-black text">
5
+ <p v-if="$slots.default" :class="typoClass" class="typo text">
6
6
  <slot />
7
7
  </p>
8
8
  </div>
@@ -12,42 +12,60 @@
12
12
  import UiSpinner from '@core/components/UiSpinner.vue'
13
13
  import { computed } from 'vue'
14
14
 
15
+ export type StateHeroType = 'page' | 'card'
16
+
15
17
  const props = defineProps<{
18
+ type: StateHeroType
16
19
  busy?: boolean
17
20
  image?: 'no-result' | 'under-construction' // TODO: 'offline' | 'no-data' | 'not-found' | 'all-good' | 'all-done' | 'error'
18
21
  }>()
19
22
 
23
+ const typoClass = computed(() => (props.type === 'page' ? 'h2-black' : 'h4-medium'))
24
+
20
25
  const imageSrc = computed(() => {
21
- try {
22
- return new URL(`../../assets/${props.image}.svg`, import.meta.url).href
23
- } catch {
26
+ if (!props.image) {
24
27
  return undefined
25
28
  }
29
+
30
+ return new URL(`../../assets/${props.image}.svg`, import.meta.url).href
26
31
  })
27
32
  </script>
28
33
 
29
34
  <style lang="postcss" scoped>
35
+ .state-hero {
36
+ &.page {
37
+ --image-width: 90%;
38
+ --spinner-size: 10rem;
39
+ --gap: 8.2rem;
40
+ }
41
+
42
+ &.card {
43
+ --image-width: 70%;
44
+ --spinner-size: 6rem;
45
+ --gap: 2rem;
46
+ }
47
+ }
48
+
30
49
  .state-hero {
31
50
  flex: 1;
32
51
  display: flex;
33
52
  flex-direction: column;
34
53
  align-items: center;
35
54
  justify-content: center;
36
- gap: 4.2rem;
55
+ gap: var(--gap);
37
56
  }
38
57
 
39
58
  .image {
40
- width: 90%;
59
+ width: var(--image-width);
41
60
  max-width: 55rem;
42
61
  }
43
62
 
44
63
  .spinner {
45
64
  color: var(--color-purple-base);
46
- font-size: 10rem;
65
+ font-size: var(--spinner-size);
47
66
  }
48
67
 
49
68
  .text {
50
69
  color: var(--color-purple-base);
51
- margin-top: 4rem;
52
70
  }
53
71
  </style>
@@ -76,12 +76,12 @@ const interactions = computed<Interaction[]>(() => [
76
76
 
77
77
  const tableName = inject<string>('tableName')
78
78
 
79
+ const columnName = `${tableName}__${props.id}`
80
+
79
81
  const currentInteraction = computed(() =>
80
82
  interactions.value.find(interaction => router.currentRoute.value.query[columnName] === interaction.id)
81
83
  )
82
84
 
83
- const columnName = `${tableName}__${props.id}`
84
-
85
85
  const updateInteraction = (interaction: Interaction) => {
86
86
  router.replace({
87
87
  query: {
@@ -20,6 +20,7 @@
20
20
  class="toggle"
21
21
  :icon="isExpanded ? faAngleDown : faAngleRight"
22
22
  size="small"
23
+ :target-scale="{ x: 1.5, y: 2 }"
23
24
  @click="emit('toggle')"
24
25
  />
25
26
  <div v-else class="h-line" />
@@ -130,15 +131,4 @@ const depth = inject(IK_TREE_LIST_DEPTH, 0)
130
131
  border-bottom: 0.1rem solid var(--color-purple-base);
131
132
  margin-left: -0.4rem;
132
133
  }
133
-
134
- /*
135
- * Increase the size of the clickable area,
136
- * without changing the padding of the ButtonIcon component
137
- */
138
- .toggle::after {
139
- content: '';
140
- position: absolute;
141
- inset: 0;
142
- transform: scale(1.5, 2);
143
- }
144
134
  </style>
@@ -0,0 +1,25 @@
1
+ # `useItemCounter`
2
+
3
+ This composable is meant to count items based on given filters then output the categorized count.
4
+
5
+ ## Usage
6
+
7
+ ```typescript
8
+ const collection = [
9
+ { id: 'a', size: 'S' },
10
+ { id: 'b', size: 'S' },
11
+ { id: 'c', size: 'XL' },
12
+ { id: 'd', size: 'M' },
13
+ { id: 'e', size: 'L' },
14
+ { id: 'f', size: 'S' },
15
+ ]
16
+
17
+ const count = useItemCounter(collection, {
18
+ small: item => item.size === 'S',
19
+ medium: item => item.size === 'M',
20
+ })
21
+
22
+ console.log(count.small) // 3
23
+ console.log(count.medium) // 1
24
+ console.log(count.$other) // 2
25
+ ```
@@ -0,0 +1,32 @@
1
+ import { computed, type MaybeRefOrGetter, toValue } from 'vue'
2
+
3
+ type Counters<TConfig> = { [K in keyof TConfig | '$other']: number }
4
+
5
+ export function useItemCounter<TItem, TConfig extends Record<string, (item: TItem) => boolean>>(
6
+ items: MaybeRefOrGetter<TItem[]>,
7
+ config: TConfig
8
+ ) {
9
+ const keys = Object.keys(config) as (keyof TConfig)[]
10
+
11
+ return computed(() => {
12
+ const counters = Object.fromEntries(keys.concat('$other').map(key => [key, 0])) as Counters<TConfig>
13
+
14
+ for (const item of toValue(items)) {
15
+ let matched = false
16
+
17
+ for (const key of keys) {
18
+ if (config[key](item)) {
19
+ matched = true
20
+ counters[key] += 1
21
+ break
22
+ }
23
+ }
24
+
25
+ if (!matched) {
26
+ counters.$other += 1
27
+ }
28
+ }
29
+
30
+ return counters
31
+ })
32
+ }
@@ -0,0 +1,9 @@
1
+ import type { WritableComputedRef } from 'vue'
2
+
3
+ export function handleAdd(source: WritableComputedRef<any>, value: any) {
4
+ if (Array.isArray(source.value)) {
5
+ source.value = [...source.value, value]
6
+ } else if (source.value instanceof Set) {
7
+ source.value = new Set(source.value).add(value)
8
+ }
9
+ }
@@ -0,0 +1,16 @@
1
+ import type { WritableComputedRef } from 'vue'
2
+
3
+ export function handleDelete(source: WritableComputedRef<any>, value: any) {
4
+ if (Array.isArray(source.value)) {
5
+ source.value = [...source.value].splice(value, 1)
6
+ } else if (source.value instanceof Set) {
7
+ source.value = new Set(source.value)
8
+ source.value.delete(value)
9
+ } else if (source.value instanceof Map) {
10
+ source.value = new Map(source.value)
11
+ source.value.delete(value)
12
+ } else if (typeof source.value === 'object' && source.value !== null) {
13
+ source.value = { ...source.value }
14
+ delete source.value[value]
15
+ }
16
+ }
@@ -0,0 +1,17 @@
1
+ import type { WritableComputedRef } from 'vue'
2
+
3
+ export function handleSet(source: WritableComputedRef<any>, key: any, value: any) {
4
+ if (Array.isArray(source.value)) {
5
+ source.value = [...source.value]
6
+ source.value[key] = value
7
+ } else if (source.value instanceof Map) {
8
+ source.value = new Map(source.value)
9
+ source.value.set(key, value)
10
+ } else if (typeof source.value === 'object') {
11
+ if (source.value === null) {
12
+ return
13
+ }
14
+
15
+ source.value = { ...source.value, [key]: value }
16
+ }
17
+ }
@@ -0,0 +1,18 @@
1
+ import type { WritableComputedRef } from 'vue'
2
+
3
+ export function handleToggle(source: WritableComputedRef<any>, valueOrState?: any, state?: any) {
4
+ if (source.value instanceof Set) {
5
+ const shouldAdd = state ?? !source.value.has(valueOrState)
6
+ const newSource = new Set(source.value)
7
+
8
+ if (shouldAdd) {
9
+ newSource.add(valueOrState)
10
+ } else {
11
+ newSource.delete(valueOrState)
12
+ }
13
+
14
+ source.value = newSource
15
+ } else if (typeof source.value === 'boolean') {
16
+ source.value = valueOrState ?? !source.value
17
+ }
18
+ }
@@ -0,0 +1,42 @@
1
+ import type { EmptyObject } from '@core/types/utility.type'
2
+ import type { WritableComputedRef } from 'vue'
3
+
4
+ export type Options = {
5
+ defaultQuery?: string
6
+ }
7
+
8
+ export type Transformers<TData> = {
9
+ toData: (value: string) => TData
10
+ toQuery: (value: TData) => string
11
+ }
12
+
13
+ export type SetActions<TValue> = {
14
+ add: (value: TValue) => void
15
+ delete: (value: TValue) => void
16
+ toggle: (value: TValue, state?: boolean) => void
17
+ }
18
+
19
+ export type MapActions<TKey, TValue> = { set: (key: TKey, value: TValue) => void; delete: (key: TKey) => void }
20
+
21
+ export type ArrayActions<TValue> = {
22
+ add: (value: TValue) => void
23
+ delete: (index: number) => void
24
+ set: (index: number, value: TValue) => void
25
+ }
26
+
27
+ export type BooleanActions = { toggle: (value?: boolean) => void }
28
+
29
+ export type Actions = SetActions<any> & MapActions<any, any> & ArrayActions<any> & BooleanActions
30
+
31
+ export type GuessActions<TData> =
32
+ TData extends Set<infer TValue>
33
+ ? SetActions<TValue>
34
+ : TData extends (infer TValue)[]
35
+ ? ArrayActions<TValue>
36
+ : TData extends boolean
37
+ ? BooleanActions
38
+ : TData extends Map<infer TKey, infer TValue> | Record<infer TKey, infer TValue>
39
+ ? MapActions<TKey, TValue>
40
+ : EmptyObject
41
+
42
+ export type RouteQuery<TData> = WritableComputedRef<TData> & GuessActions<TData>
@@ -0,0 +1,42 @@
1
+ import { handleAdd } from '@core/composables/route-query/actions/handle-add'
2
+ import { handleDelete } from '@core/composables/route-query/actions/handle-delete'
3
+ import { handleSet } from '@core/composables/route-query/actions/handle-set'
4
+ import { handleToggle } from '@core/composables/route-query/actions/handle-toggle'
5
+ import type { Actions, Options, RouteQuery, Transformers } from '@core/composables/route-query/types'
6
+ import { extendRef } from '@vueuse/core'
7
+ import { computed } from 'vue'
8
+ import { useRoute, useRouter } from 'vue-router'
9
+
10
+ export function useRouteQuery<TData extends string>(name: string): RouteQuery<TData>
11
+ export function useRouteQuery<TData extends string>(name: string, options: Options): RouteQuery<TData>
12
+ export function useRouteQuery<TData>(name: string, options: Options & Transformers<TData>): RouteQuery<TData>
13
+ export function useRouteQuery<TData>(name: string, options: Partial<Options & Transformers<TData>> = {}) {
14
+ const router = useRouter()
15
+ const route = useRoute()
16
+
17
+ const {
18
+ defaultQuery = '',
19
+ toData = (query: string) => query as TData,
20
+ toQuery = (data: TData) => data as string,
21
+ } = options
22
+
23
+ const source = computed<TData>({
24
+ get() {
25
+ return toData((route.query[name] as string | undefined) ?? defaultQuery)
26
+ },
27
+ set(newData) {
28
+ const query = toQuery(newData)
29
+
30
+ void router.replace({ query: { ...route.query, [name]: query === defaultQuery ? undefined : query } })
31
+ },
32
+ })
33
+
34
+ const actions: Actions = {
35
+ add: handleAdd.bind(null, source),
36
+ delete: handleDelete.bind(null, source),
37
+ set: handleSet.bind(null, source),
38
+ toggle: handleToggle.bind(null, source),
39
+ }
40
+
41
+ return extendRef(source, actions) as unknown as RouteQuery<TData>
42
+ }
@@ -26,7 +26,9 @@
26
26
  "n-vms": "1 VM | {n} VMs",
27
27
  "network": "Netzwerk",
28
28
  "object-not-found": "Objekt {id} wurde nicht gefunden…",
29
+ "patches": "Patches",
29
30
  "power-on-for-console": "Konsole ist nach Start der VM verfügbar",
31
+ "running-vm": "VM eingeschalten | VMs eingeschalten",
30
32
  "stats": "Statistiken",
31
33
  "storage": "Speicher",
32
34
  "support-name": "{name} pro support",
@@ -20,9 +20,9 @@
20
20
 
21
21
  "core.search": "Search",
22
22
 
23
- "core.search-bar.label": "Search Engine",
24
- "core.search-bar.placeholder": "Write your query…",
25
- "core.search-bar.use-query-builder": "Use query builder",
23
+ "core.query-search-bar.label": "Search Engine",
24
+ "core.query-search-bar.placeholder": "Write your query…",
25
+ "core.query-search-bar.use-query-builder": "Use query builder",
26
26
 
27
27
  "core.select.all": "Select all",
28
28
  "core.select.none": "Select none",
@@ -48,7 +48,9 @@
48
48
  "n-vms": "1 VM | {n} VMs",
49
49
  "network": "Network",
50
50
  "object-not-found": "Object {id} can't be found…",
51
+ "patches": "Patches",
51
52
  "power-on-for-console": "Power on your VM to access its console",
53
+ "running-vm": "Running VM | Running VMs",
52
54
  "see-all": "See all",
53
55
  "stats": "Stats",
54
56
  "storage": "Storage",
@@ -64,5 +66,6 @@
64
66
  "tasks.quick-view.failed": "Failed",
65
67
  "tasks.quick-view.in-progress": "In progress",
66
68
 
69
+ "total": "Total",
67
70
  "vms": "VMs"
68
71
  }
@@ -37,7 +37,9 @@
37
37
  "log-out": "خروج",
38
38
  "master": "میزبان اصلی",
39
39
  "network": "شبکه",
40
+ "patches": "وصله ها",
40
41
  "power-on-for-console": "ماشین مجازی خود را روشن کنید تا به کنسول آن دسترسی داشته باشید",
42
+ "running-vm": "ماشین های مجازی در حال اجرا | ماشین مجازی در حال اجرا",
41
43
  "stats": "آمار",
42
44
  "storage": "ذخیره سازی",
43
45
  "support-name": "پشتیبانی حرفه ای {name}",
@@ -20,9 +20,9 @@
20
20
 
21
21
  "core.search": "Rechercher",
22
22
 
23
- "core.search-bar.label": "Moteur de recherche",
24
- "core.search-bar.placeholder": "Écrivez votre requête…",
25
- "core.search-bar.use-query-builder": "Utiliser le constructeur de requête",
23
+ "core.query-search-bar.label": "Moteur de recherche",
24
+ "core.query-search-bar.placeholder": "Écrivez votre requête…",
25
+ "core.query-search-bar.use-query-builder": "Utiliser le constructeur de requête",
26
26
 
27
27
  "core.select.all": "Tout sélectionner",
28
28
  "core.select.none": "Tout désélectionner",
@@ -48,7 +48,9 @@
48
48
  "n-vms": "1 VM | {n} VMs",
49
49
  "network": "Réseau",
50
50
  "object-not-found": "L'objet {id} est introuvable…",
51
+ "patches": "Patches",
51
52
  "power-on-for-console": "Allumez votre VM pour accéder à sa console",
53
+ "running-vm": "VM en cours d'exécution | VMs en cours d'exécution",
52
54
  "see-all": "Voir tout",
53
55
  "stats": "Stats",
54
56
  "storage": "Stockage",
@@ -64,5 +66,6 @@
64
66
  "tasks.quick-view.failed": "Échouées",
65
67
  "tasks.quick-view.in-progress": "En cours",
66
68
 
69
+ "total": "Total",
67
70
  "vms": "VMs"
68
71
  }
@@ -6,7 +6,7 @@ import { useRoute, useRouter } from 'vue-router'
6
6
  export const useUiStore = defineStore('ui', () => {
7
7
  const currentHostOpaqueRef = ref()
8
8
 
9
- const { store: colorMode } = useColorMode({ initialValue: 'dark' })
9
+ const { store: colorMode } = useColorMode({ initialValue: 'auto' })
10
10
 
11
11
  const { desktop: isDesktop } = useBreakpoints({
12
12
  desktop: 1024,
@@ -0,0 +1,11 @@
1
+ import type { RouteLocationRaw } from 'vue-router'
2
+
3
+ export type BackupState = 'success' | 'failure' | 'partial'
4
+
5
+ export type BackupStates = BackupState[]
6
+
7
+ export type Backup = {
8
+ label: string
9
+ route: RouteLocationRaw
10
+ states: BackupStates
11
+ }
@@ -1,3 +1,9 @@
1
1
  export type MaybeArray<T> = T | T[]
2
2
 
3
3
  export type VoidFunction = () => void
4
+
5
+ declare const __brand: unique symbol
6
+
7
+ export type Branded<TBrand extends string, TType = string> = TType & { [__brand]: TBrand }
8
+
9
+ export type EmptyObject = Record<string, never>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xen-orchestra/web-core",
3
3
  "type": "module",
4
- "version": "0.0.4",
4
+ "version": "0.1.0",
5
5
  "private": false,
6
6
  "exports": {
7
7
  "./*": {
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/lodash-es": "^4.17.12",
33
- "@types/novnc__novnc": "^1.3.5",
33
+ "@types/novnc__novnc": "^1.5.0",
34
34
  "@vue/tsconfig": "^0.5.1",
35
35
  "pinia": "^2.1.7",
36
36
  "vue": "^3.4.13",