@xen-orchestra/web-core 0.0.1 → 0.0.3

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 (38) hide show
  1. package/env.d.ts +2 -0
  2. package/lib/assets/no-result.svg +81 -0
  3. package/lib/assets/under-construction.svg +195 -0
  4. package/lib/components/CardNumbers.vue +94 -0
  5. package/lib/components/DonutChart.vue +92 -0
  6. package/lib/components/LegendTitle.vue +31 -0
  7. package/lib/components/StatusPill.vue +2 -2
  8. package/lib/components/UiCard.vue +29 -0
  9. package/lib/components/UiLegend.vue +68 -0
  10. package/lib/components/UiSeparator.vue +11 -0
  11. package/lib/components/card/CardSubtitle.vue +24 -0
  12. package/lib/components/card/CardTitle.vue +52 -0
  13. package/lib/components/chip/UiChip.vue +2 -2
  14. package/lib/components/console/RemoteConsole.vue +119 -0
  15. package/lib/components/dropdown/DropdownTitle.vue +7 -3
  16. package/lib/components/icon/VmIcon.vue +1 -1
  17. package/lib/components/menu/MenuItem.vue +1 -1
  18. package/lib/components/menu/MenuList.vue +5 -5
  19. package/lib/components/stacked-bar/StackedBar.vue +49 -0
  20. package/lib/components/stacked-bar/StackedBarSegment.vue +73 -0
  21. package/lib/components/state-hero/ComingSoonHero.vue +9 -0
  22. package/lib/components/state-hero/LoadingHero.vue +9 -0
  23. package/lib/components/state-hero/ObjectNotFoundHero.vue +13 -0
  24. package/lib/components/state-hero/StateHero.vue +53 -0
  25. package/lib/components/table/ColumnTitle.vue +150 -0
  26. package/lib/components/table/UiTable.vue +58 -0
  27. package/lib/components/tooltip/TooltipList.vue +0 -2
  28. package/lib/components/tree/TreeItemLabel.vue +4 -0
  29. package/lib/composables/tree/types.ts +1 -1
  30. package/lib/i18n.ts +33 -1
  31. package/lib/layouts/CoreLayout.vue +6 -97
  32. package/lib/locales/de.json +31 -4
  33. package/lib/locales/en.json +45 -13
  34. package/lib/locales/fr.json +45 -13
  35. package/lib/types/iterable-backoff.d.ts +1 -0
  36. package/lib/types/utility.type.ts +2 -0
  37. package/lib/utils/if-else.utils.ts +3 -3
  38. package/package.json +19 -6
@@ -0,0 +1,24 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <div class="card-subtitle">
4
+ <span class="typo h7-semi-bold"><slot /></span>
5
+ <span v-if="$slots.info" class="typo p3-medium"><slot name="info" /></span>
6
+ </div>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ defineSlots<{
11
+ default: () => void
12
+ info: () => void
13
+ }>()
14
+ </script>
15
+
16
+ <style lang="postcss" scoped>
17
+ .card-subtitle {
18
+ color: var(--color-purple-base);
19
+ border-bottom: 0.1rem solid var(--color-purple-base);
20
+ display: flex;
21
+ justify-content: space-between;
22
+ gap: 0.8rem;
23
+ }
24
+ </style>
@@ -0,0 +1,52 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <div class="card-title">
4
+ <div class="main-content">
5
+ <div class="title typo h6-medium"><slot /></div>
6
+
7
+ <div v-if="$slots.info" class="info typo h7-semi-bold"><slot name="info" /></div>
8
+ </div>
9
+ <p v-if="$slots.description" class="description typo p3-regular">
10
+ <slot name="description" />
11
+ </p>
12
+ </div>
13
+ </template>
14
+
15
+ <script lang="ts" setup>
16
+ defineSlots<{
17
+ default: () => void
18
+ info: () => void
19
+ description: () => void
20
+ }>()
21
+ </script>
22
+
23
+ <style lang="postcss" scoped>
24
+ .card-title {
25
+ display: flex;
26
+ flex-direction: column;
27
+ }
28
+
29
+ .main-content {
30
+ display: flex;
31
+ gap: 1.6rem;
32
+ align-items: center;
33
+ }
34
+
35
+ .title {
36
+ display: flex;
37
+ align-items: center;
38
+ gap: 1.6rem;
39
+ }
40
+
41
+ .info {
42
+ margin-left: auto;
43
+ display: flex;
44
+ align-items: center;
45
+ gap: 0.8rem;
46
+ color: var(--color-purple-base);
47
+ }
48
+
49
+ .description {
50
+ color: var(--color-grey-300);
51
+ }
52
+ </style>
@@ -25,8 +25,8 @@ withDefaults(
25
25
  )
26
26
 
27
27
  const emit = defineEmits<{
28
- (event: 'edit'): void
29
- (event: 'remove'): void
28
+ edit: []
29
+ remove: []
30
30
  }>()
31
31
  </script>
32
32
 
@@ -0,0 +1,119 @@
1
+ <template>
2
+ <div :class="uiStore.isMobile ? 'mobile' : undefined" class="remote-console">
3
+ <div ref="consoleContainer" class="console" />
4
+ </div>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import { useUiStore } from '@core/stores/ui.store'
9
+ import VncClient from '@novnc/novnc/core/rfb'
10
+ import { promiseTimeout } from '@vueuse/shared'
11
+ import { fibonacci } from 'iterable-backoff'
12
+ import { onBeforeUnmount, ref, watchEffect } from 'vue'
13
+
14
+ const props = defineProps<{
15
+ url: URL
16
+ }>()
17
+
18
+ const N_TOTAL_TRIES = 8
19
+ const FIBONACCI_MS_ARRAY: number[] = Array.from(fibonacci().toMs().take(N_TOTAL_TRIES))
20
+
21
+ const consoleContainer = ref<HTMLDivElement | null>(null)
22
+
23
+ let vncClient: VncClient | undefined
24
+ let nConnectionAttempts = 0
25
+
26
+ const handleDisconnectionEvent = () => {
27
+ clearVncClient()
28
+
29
+ nConnectionAttempts++
30
+
31
+ if (nConnectionAttempts > N_TOTAL_TRIES) {
32
+ console.error('The number of reconnection attempts has been exceeded for:', props.url)
33
+ return
34
+ }
35
+
36
+ console.error(
37
+ `Connection lost for the remote console: ${props.url}. New attempt in ${
38
+ FIBONACCI_MS_ARRAY[nConnectionAttempts - 1]
39
+ }ms`
40
+ )
41
+ createVncConnection()
42
+ }
43
+
44
+ const handleConnectionEvent = () => (nConnectionAttempts = 0)
45
+
46
+ const clearVncClient = () => {
47
+ if (vncClient === undefined) {
48
+ return
49
+ }
50
+
51
+ vncClient.removeEventListener('disconnect', handleDisconnectionEvent)
52
+ vncClient.removeEventListener('connect', handleConnectionEvent)
53
+ vncClient.disconnect()
54
+
55
+ vncClient = undefined
56
+ }
57
+
58
+ const createVncConnection = async () => {
59
+ if (nConnectionAttempts !== 0) {
60
+ await promiseTimeout(FIBONACCI_MS_ARRAY[nConnectionAttempts - 1])
61
+
62
+ if (vncClient !== undefined) {
63
+ // New VNC Client may have been created in the meantime
64
+ return
65
+ }
66
+ }
67
+
68
+ vncClient = new VncClient(consoleContainer.value!, props.url.toString(), {
69
+ wsProtocols: ['binary'],
70
+ })
71
+ vncClient.scaleViewport = true
72
+ vncClient.background = 'transparent'
73
+
74
+ vncClient.addEventListener('disconnect', handleDisconnectionEvent)
75
+ vncClient.addEventListener('connect', handleConnectionEvent)
76
+ }
77
+
78
+ watchEffect(() => {
79
+ if (consoleContainer.value === null) {
80
+ return
81
+ }
82
+
83
+ nConnectionAttempts = 0
84
+
85
+ clearVncClient()
86
+ createVncConnection()
87
+ })
88
+
89
+ onBeforeUnmount(() => {
90
+ clearVncClient()
91
+ })
92
+
93
+ const uiStore = useUiStore()
94
+ </script>
95
+
96
+ <style lang="postcss" scoped>
97
+ .remote-console {
98
+ --padding: 0.8rem;
99
+ --height: 80rem;
100
+ --width: 100%;
101
+
102
+ &.mobile {
103
+ --padding: 0.8rem 0;
104
+ --height: 100%;
105
+ --width: 95vw;
106
+ }
107
+ }
108
+
109
+ .remote-console {
110
+ padding: var(--padding);
111
+ height: var(--height);
112
+ width: var(--width);
113
+ max-width: 100%;
114
+ }
115
+
116
+ .console {
117
+ height: 100%;
118
+ }
119
+ </style>
@@ -9,8 +9,12 @@
9
9
  <slot />
10
10
  </div>
11
11
  <div v-if="onToggleSelectAll" class="buttons">
12
- <span v-if="selected !== 'all'" @click="emit('toggleSelectAll', true)">Select all</span>
13
- <span v-if="selected !== 'none'" @click="emit('toggleSelectAll', false)">Deselect all</span>
12
+ <span v-if="selected !== 'all'" @click="emit('toggleSelectAll', true)">
13
+ {{ $t('core.select.all') }}
14
+ </span>
15
+ <span v-if="selected !== 'none'" @click="emit('toggleSelectAll', false)">
16
+ {{ $t('core.select.none') }}
17
+ </span>
14
18
  </div>
15
19
  </div>
16
20
  </template>
@@ -29,7 +33,7 @@ withDefaults(
29
33
  )
30
34
 
31
35
  const emit = defineEmits<{
32
- (event: 'toggleSelectAll', value: boolean): void
36
+ toggleSelectAll: [value: boolean]
33
37
  }>()
34
38
  </script>
35
39
 
@@ -7,8 +7,8 @@
7
7
  </template>
8
8
 
9
9
  <script lang="ts" setup>
10
- import PowerStateIcon from '@core/components/PowerStateIcon.vue'
11
10
  import UiIcon from '@core/components/icon/UiIcon.vue'
11
+ import PowerStateIcon from '@core/components/PowerStateIcon.vue'
12
12
  import type { POWER_STATE } from '@core/types/power-state.type'
13
13
  import { faDisplay } from '@fortawesome/free-solid-svg-icons'
14
14
  import { FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
@@ -25,8 +25,8 @@
25
25
 
26
26
  <script lang="ts" setup>
27
27
  import UiIcon from '@core/components/icon/UiIcon.vue'
28
- import MenuTrigger from '@core/components/menu/MenuTrigger.vue'
29
28
  import MenuList from '@core/components/menu/MenuList.vue'
29
+ import MenuTrigger from '@core/components/menu/MenuTrigger.vue'
30
30
  import { useContext } from '@core/composables/context.composable'
31
31
  import { DisabledContext } from '@core/context'
32
32
  import { IK_CLOSE_MENU, IK_MENU_HORIZONTAL } from '@core/utils/injection-keys.util'
@@ -12,9 +12,13 @@
12
12
  import { useContext } from '@core/composables/context.composable'
13
13
  import { DisabledContext } from '@core/context'
14
14
  import { IK_CLOSE_MENU, IK_MENU_HORIZONTAL, IK_MENU_TELEPORTED } from '@core/utils/injection-keys.util'
15
+ import { onClickOutside, unrefElement, whenever } from '@vueuse/core'
15
16
  import placementJs, { type Options } from 'placement.js'
16
17
  import { computed, inject, nextTick, provide, ref, useSlots } from 'vue'
17
- import { onClickOutside, unrefElement, whenever } from '@vueuse/core'
18
+
19
+ defineOptions({
20
+ inheritAttrs: false,
21
+ })
18
22
 
19
23
  const props = withDefaults(
20
24
  defineProps<{
@@ -26,10 +30,6 @@ const props = withDefaults(
26
30
  { disabled: undefined }
27
31
  )
28
32
 
29
- defineOptions({
30
- inheritAttrs: false,
31
- })
32
-
33
33
  const slots = useSlots()
34
34
  const isOpen = ref(false)
35
35
  const menu = ref()
@@ -0,0 +1,49 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <div class="stacked-bar">
4
+ <StackedBarSegment
5
+ v-for="(segment, index) in computedSegments"
6
+ :key="index"
7
+ :color="segment.color"
8
+ :percentage="segment.percentage"
9
+ />
10
+ </div>
11
+ </template>
12
+
13
+ <script lang="ts" setup>
14
+ import StackedBarSegment from '@core/components/stacked-bar/StackedBarSegment.vue'
15
+ import type { Color } from '@core/types/color.type'
16
+ import { computed } from 'vue'
17
+
18
+ type Segment = {
19
+ value: number
20
+ color: Color
21
+ }
22
+
23
+ const props = defineProps<{
24
+ segments: Segment[]
25
+ maxValue?: number
26
+ }>()
27
+
28
+ const totalValue = computed(() => props.segments.reduce((acc, bar) => acc + bar.value, 0))
29
+
30
+ const max = computed(() => Math.max(props.maxValue ?? 0, totalValue.value))
31
+
32
+ const computedSegments = computed(() => {
33
+ return props.segments.map(segment => ({
34
+ ...segment,
35
+ percentage: (segment.value / max.value) * 100,
36
+ }))
37
+ })
38
+ </script>
39
+
40
+ <style lang="postcss" scoped>
41
+ .stacked-bar {
42
+ width: 100%;
43
+ display: flex;
44
+ height: 4rem;
45
+ border-radius: 0.8rem;
46
+ overflow: hidden;
47
+ background-color: var(--background-color-purple-10);
48
+ }
49
+ </style>
@@ -0,0 +1,73 @@
1
+ <template>
2
+ <div
3
+ v-tooltip="{ selector: '.ellipsis' }"
4
+ :class="color"
5
+ :style="{ width: percentage + '%' }"
6
+ class="stacked-bar-segment typo c4-semi-bold"
7
+ >
8
+ <div ref="ellipsisElement" :class="{ hidden }" class="ellipsis">
9
+ {{ $n(percentage / 100, 'percent') }}
10
+ </div>
11
+ </div>
12
+ </template>
13
+
14
+ <script lang="ts" setup>
15
+ import { vTooltip } from '@core/directives/tooltip.directive'
16
+ import type { Color } from '@core/types/color.type'
17
+ import { hasEllipsis } from '@core/utils/has-ellipsis.util'
18
+ import { useResizeObserver } from '@vueuse/core'
19
+ import { ref } from 'vue'
20
+
21
+ defineProps<{
22
+ color: Color
23
+ percentage: number
24
+ }>()
25
+
26
+ const hidden = ref(false)
27
+ const ellipsisElement = ref<HTMLElement | null>(null)
28
+
29
+ useResizeObserver(ellipsisElement, ([entry]) => {
30
+ hidden.value = hasEllipsis(entry.target)
31
+ })
32
+ </script>
33
+
34
+ <style lang="postcss" scoped>
35
+ /* COLOR VARIANT */
36
+ .stacked-bar-segment {
37
+ &.info {
38
+ --background-color: var(--color-purple-base);
39
+ }
40
+
41
+ &.success {
42
+ --background-color: var(--color-green-base);
43
+ }
44
+
45
+ &.warning {
46
+ --background-color: var(--color-orange-base);
47
+ }
48
+
49
+ &.error {
50
+ --background-color: var(--color-red-base);
51
+ }
52
+ }
53
+
54
+ /* IMPLEMENTATION */
55
+ .stacked-bar-segment {
56
+ display: flex;
57
+ justify-content: center;
58
+ align-items: center;
59
+ white-space: nowrap;
60
+ color: var(--color-grey-600);
61
+ background-color: var(--background-color);
62
+ }
63
+
64
+ .ellipsis {
65
+ overflow: hidden;
66
+ white-space: nowrap;
67
+ min-width: 0;
68
+ }
69
+
70
+ .hidden {
71
+ visibility: hidden;
72
+ }
73
+ </style>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <StateHero image="under-construction" class="coming-soon-hero">
3
+ {{ $t('coming-soon') }}
4
+ </StateHero>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import StateHero from '@core/components/state-hero/StateHero.vue'
9
+ </script>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <StateHero busy class="loading-hero">
3
+ {{ $t('loading-in-progress') }}
4
+ </StateHero>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import StateHero from '@core/components/state-hero/StateHero.vue'
9
+ </script>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <StateHero image="no-result" class="object-not-found-hero">
3
+ {{ $t('object-not-found', { id }) }}
4
+ </StateHero>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import StateHero from '@core/components/state-hero/StateHero.vue'
9
+
10
+ defineProps<{
11
+ id: string
12
+ }>()
13
+ </script>
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <div class="state-hero">
3
+ <UiSpinner v-if="busy" class="spinner" />
4
+ <img v-else-if="imageSrc" :src="imageSrc" alt="" class="image" />
5
+ <p v-if="$slots.default" class="typo h2-black text">
6
+ <slot />
7
+ </p>
8
+ </div>
9
+ </template>
10
+
11
+ <script lang="ts" setup>
12
+ import UiSpinner from '@core/components/UiSpinner.vue'
13
+ import { computed } from 'vue'
14
+
15
+ const props = defineProps<{
16
+ busy?: boolean
17
+ image?: 'no-result' | 'under-construction' // TODO: 'offline' | 'no-data' | 'not-found' | 'all-good' | 'all-done' | 'error'
18
+ }>()
19
+
20
+ const imageSrc = computed(() => {
21
+ try {
22
+ return new URL(`../../assets/${props.image}.svg`, import.meta.url).href
23
+ } catch {
24
+ return undefined
25
+ }
26
+ })
27
+ </script>
28
+
29
+ <style lang="postcss" scoped>
30
+ .state-hero {
31
+ flex: 1;
32
+ display: flex;
33
+ flex-direction: column;
34
+ align-items: center;
35
+ justify-content: center;
36
+ gap: 4.2rem;
37
+ }
38
+
39
+ .image {
40
+ width: 90%;
41
+ max-width: 55rem;
42
+ }
43
+
44
+ .spinner {
45
+ color: var(--color-purple-base);
46
+ font-size: 10rem;
47
+ }
48
+
49
+ .text {
50
+ color: var(--color-purple-base);
51
+ margin-top: 4rem;
52
+ }
53
+ </style>
@@ -0,0 +1,150 @@
1
+ <!-- v1.0 -->
2
+ <template>
3
+ <MenuList :disabled placement="bottom-start" shadow>
4
+ <template #trigger="{ open, isOpen }">
5
+ <th
6
+ :class="{ interactive, disabled, focus: isOpen }"
7
+ class="column-header"
8
+ @click="ev => (interactive ? open(ev) : noop())"
9
+ >
10
+ <div class="content">
11
+ <span class="label">
12
+ <UiIcon :icon />
13
+ <slot />
14
+ </span>
15
+ <UiIcon :icon="currentInteraction?.icon" />
16
+ </div>
17
+ </th>
18
+ </template>
19
+ <MenuItem
20
+ v-for="interaction in interactions"
21
+ :key="interaction.id"
22
+ v-tooltip="$t('coming-soon')"
23
+ :disabled="interaction.disabled"
24
+ :on-click="() => updateInteraction(interaction)"
25
+ >
26
+ <UiIcon :icon="interaction.icon" />{{ interaction.label }}
27
+ <i v-if="currentInteraction?.id === interaction.id" class="current-interaction typo p3-regular-italic">
28
+ {{ $t('core.current').toLowerCase() }}
29
+ </i>
30
+ </MenuItem>
31
+ </MenuList>
32
+ </template>
33
+
34
+ <script lang="ts" setup>
35
+ import UiIcon from '@core/components/icon/UiIcon.vue'
36
+ import MenuItem from '@core/components/menu/MenuItem.vue'
37
+ import MenuList from '@core/components/menu/MenuList.vue'
38
+ import { vTooltip } from '@core/directives/tooltip.directive'
39
+ import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
40
+ import { faArrowDown, faArrowUp, faEyeSlash, faFilter, faLayerGroup } from '@fortawesome/free-solid-svg-icons'
41
+ import { noop } from '@vueuse/core'
42
+ import { computed, inject } from 'vue'
43
+ import { useI18n } from 'vue-i18n'
44
+ import { useRouter } from 'vue-router'
45
+
46
+ type InteractionId = 'sort-asc' | 'sort-desc' | 'group' | 'filter' | 'hide'
47
+ type Interaction = {
48
+ disabled?: boolean
49
+ id: InteractionId
50
+ icon: IconDefinition
51
+ label: string
52
+ }
53
+
54
+ const props = withDefaults(
55
+ defineProps<{
56
+ id?: string
57
+ icon?: IconDefinition
58
+ interactive?: boolean
59
+ disabled?: boolean
60
+ }>(),
61
+ {
62
+ disabled: false,
63
+ interactive: true,
64
+ }
65
+ )
66
+ const { t } = useI18n()
67
+ const router = useRouter()
68
+
69
+ const interactions = computed<Interaction[]>(() => [
70
+ { id: 'sort-asc', icon: faArrowDown, label: t('core.sort.ascending'), disabled: true },
71
+ { id: 'sort-desc', icon: faArrowUp, label: t('core.sort.descending'), disabled: true },
72
+ { id: 'group', icon: faLayerGroup, label: t('core.group'), disabled: true },
73
+ { id: 'filter', icon: faFilter, label: t('core.filter'), disabled: true },
74
+ { id: 'hide', icon: faEyeSlash, label: t('core.hide'), disabled: true },
75
+ ])
76
+
77
+ const tableName = inject<string>('tableName')
78
+
79
+ const currentInteraction = computed(() =>
80
+ interactions.value.find(interaction => router.currentRoute.value.query[columnName] === interaction.id)
81
+ )
82
+
83
+ const columnName = `${tableName}__${props.id}`
84
+
85
+ const updateInteraction = (interaction: Interaction) => {
86
+ router.replace({
87
+ query: {
88
+ [columnName]: interaction.id,
89
+ },
90
+ })
91
+ }
92
+ </script>
93
+
94
+ <style lang="postcss" scoped>
95
+ /* COLOR VARIANTS */
96
+ .column-header.interactive {
97
+ --color: var(--color-purple-base);
98
+ --background-color: var(--background-color-primary);
99
+
100
+ &.focus {
101
+ --color: var(--color-purple-base);
102
+ --background-color: var(--background-color-purple-10);
103
+ }
104
+
105
+ &:hover {
106
+ --color: var(--color-purple-d20);
107
+ --background-color: var(--background-color-purple-20);
108
+ }
109
+
110
+ &:active {
111
+ --color: var(--color-purple-d40);
112
+ --background-color: var(--background-color-purple-30);
113
+ }
114
+
115
+ &.disabled {
116
+ --color: var(--color-grey-400);
117
+ --background-color: var(--background-color-secondary);
118
+ }
119
+ }
120
+ /* IMPLEMENTATION */
121
+ .column-header.interactive {
122
+ cursor: pointer;
123
+ color: var(--color);
124
+ background-color: var(--background-color);
125
+ &.disabled {
126
+ cursor: not-allowed;
127
+ }
128
+ }
129
+
130
+ .current-interaction {
131
+ color: var(--color-grey-300);
132
+ }
133
+
134
+ .content {
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: space-between;
138
+ gap: 1rem;
139
+ }
140
+
141
+ .label {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 1rem;
145
+ }
146
+
147
+ .filter-icon {
148
+ cursor: pointer;
149
+ }
150
+ </style>