@xen-orchestra/web-core 0.10.0 → 0.11.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.
@@ -0,0 +1,33 @@
1
+ # LinearChart component
2
+
3
+ ```vue
4
+ <template>
5
+ <LinearChart :data="data" :value-formatter="customValueFormatter" />
6
+ </template>
7
+
8
+ <script lang="ts" setup>
9
+ import type { LinearChartData } from '@/types/chart'
10
+ import LinearChart from '@/components/charts/LinearChart.vue'
11
+
12
+ const data: LinearChartData = [
13
+ {
14
+ label: 'First series',
15
+ data: [
16
+ { timestamp: 1670478371123, value: 1234 },
17
+ { timestamp: 1670478519751, value: 1234 },
18
+ ],
19
+ },
20
+ {
21
+ label: 'Second series',
22
+ data: [
23
+ { timestamp: 1670478519751, value: 1234 },
24
+ { timestamp: 167047555000, value: 1234 },
25
+ ],
26
+ },
27
+ ]
28
+
29
+ const customValueFormatter = (value: number) => {
30
+ return `${value} (Doubled: ${value * 2})`
31
+ }
32
+ </script>
33
+ ```
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <VueCharts :option autoresize class="chart" />
3
+ </template>
4
+
5
+ <script lang="ts" setup>
6
+ import type { LinearChartData, ValueFormatter } from '@core/types/chart'
7
+ import { IK_CHART_VALUE_FORMATTER } from '@core/utils/injection-keys.util'
8
+ import { utcFormat } from 'd3-time-format'
9
+ import type { EChartsOption } from 'echarts'
10
+ import { LineChart } from 'echarts/charts'
11
+ import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
12
+ import { use } from 'echarts/core'
13
+ import { CanvasRenderer } from 'echarts/renderers'
14
+ import { computed, provide } from 'vue'
15
+ import VueCharts from 'vue-echarts'
16
+
17
+ const props = defineProps<{
18
+ data: LinearChartData
19
+ valueFormatter?: ValueFormatter
20
+ maxValue?: number
21
+ }>()
22
+
23
+ const Y_AXIS_MAX_VALUE = 200
24
+
25
+ const valueFormatter = computed<ValueFormatter>(() => {
26
+ const formatter = props.valueFormatter
27
+
28
+ return value => {
29
+ if (formatter === undefined) {
30
+ return value.toString()
31
+ }
32
+
33
+ return formatter(value)
34
+ }
35
+ })
36
+
37
+ provide(IK_CHART_VALUE_FORMATTER, valueFormatter)
38
+
39
+ use([CanvasRenderer, LineChart, GridComponent, TooltipComponent, LegendComponent])
40
+
41
+ const option = computed<EChartsOption>(() => ({
42
+ legend: {
43
+ data: props.data.map(series => series.label),
44
+ },
45
+ tooltip: {
46
+ valueFormatter: v => valueFormatter.value(v as number),
47
+ },
48
+ xAxis: {
49
+ type: 'time',
50
+ axisLabel: {
51
+ formatter: (timestamp: number) => utcFormat('%a\n%I:%M\n%p')(new Date(timestamp)),
52
+ showMaxLabel: false,
53
+ showMinLabel: false,
54
+ },
55
+ },
56
+ yAxis: {
57
+ type: 'value',
58
+ axisLabel: {
59
+ formatter: valueFormatter.value,
60
+ },
61
+ max: props.maxValue ?? Y_AXIS_MAX_VALUE,
62
+ },
63
+ series: props.data.map((series, index) => ({
64
+ type: 'line',
65
+ name: series.label,
66
+ zlevel: index + 1,
67
+ data: series.data.map(item => [item.timestamp, item.value]),
68
+ })),
69
+ }))
70
+ </script>
71
+
72
+ <style lang="postcss" scoped>
73
+ .chart {
74
+ width: 100%;
75
+ height: 30rem;
76
+ }
77
+ </style>
@@ -9,7 +9,11 @@ import UiInfo, { type InfoAccent } from '@core/components/ui/info/UiInfo.vue'
9
9
  import { computed, type ComputedRef } from 'vue'
10
10
  import { useI18n } from 'vue-i18n'
11
11
 
12
- type ConnectionStatus = 'connected' | 'disconnected' | 'partially-connected' | 'disconnected-from-physical-device'
12
+ export type ConnectionStatus =
13
+ | 'connected'
14
+ | 'disconnected'
15
+ | 'partially-connected'
16
+ | 'disconnected-from-physical-device'
13
17
  type ConnectionStatusesMap = Record<ConnectionStatus, { text: string; accent: InfoAccent }>
14
18
 
15
19
  const { status } = defineProps<{
@@ -1,9 +1,7 @@
1
1
  <template>
2
2
  <UiCardTitle>{{ $t('console-actions') }}</UiCardTitle>
3
3
  <UiButton
4
- v-tooltip="toggleFullScreen === undefined ? $t('coming-soon') : undefined"
5
4
  class="button"
6
- :disabled="toggleFullScreen === undefined"
7
5
  accent="info"
8
6
  variant="tertiary"
9
7
  size="medium"
@@ -13,9 +11,7 @@
13
11
  {{ $t(isFullscreen ? 'exit-fullscreen' : 'fullscreen') }}
14
12
  </UiButton>
15
13
  <UiButton
16
- v-tooltip="openInNewTab === undefined ? $t('coming-soon') : undefined"
17
14
  class="button"
18
- :disabled="openInNewTab === undefined"
19
15
  accent="info"
20
16
  variant="tertiary"
21
17
  size="medium"
@@ -25,12 +21,10 @@
25
21
  {{ $t('open-console-in-new-tab') }}
26
22
  </UiButton>
27
23
  <UiButton
28
- v-tooltip="sendCtrlAltDel === undefined ? $t('coming-soon') : undefined"
29
24
  class="button"
30
25
  accent="info"
31
26
  variant="tertiary"
32
27
  size="medium"
33
- :disabled="sendCtrlAltDel === undefined"
34
28
  :left-icon="faKeyboard"
35
29
  @click="sendCtrlAltDel"
36
30
  >
@@ -41,21 +35,41 @@
41
35
  <script lang="ts" setup>
42
36
  import UiButton from '@core/components/ui/button/UiButton.vue'
43
37
  import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
44
- import { vTooltip } from '@core/directives/tooltip.directive'
38
+ import { useUiStore } from '@core/stores/ui.store'
45
39
  import {
46
40
  faArrowUpRightFromSquare,
47
41
  faDownLeftAndUpRightToCenter,
48
42
  faKeyboard,
49
43
  faUpRightAndDownLeftFromCenter,
50
44
  } from '@fortawesome/free-solid-svg-icons'
45
+ import { useActiveElement, useMagicKeys, whenever } from '@vueuse/core'
46
+ import { logicAnd } from '@vueuse/math'
47
+ import { computed } from 'vue'
48
+ import { useRouter } from 'vue-router'
51
49
 
52
- // temporary undefined for xo6
53
50
  defineProps<{
54
- openInNewTab?: () => void
55
- toggleFullScreen?: () => void
56
- sendCtrlAltDel?: () => void
57
- isFullscreen?: boolean
51
+ sendCtrlAltDel: () => void
58
52
  }>()
53
+
54
+ const router = useRouter()
55
+ const uiStore = useUiStore()
56
+
57
+ const isFullscreen = computed(() => !uiStore.hasUi)
58
+
59
+ const openInNewTab = () => {
60
+ const routeData = router.resolve({ query: { ui: '0' } })
61
+ window.open(routeData.href, '_blank')
62
+ }
63
+
64
+ const toggleFullScreen = () => {
65
+ uiStore.hasUi = !uiStore.hasUi
66
+ }
67
+ const { escape } = useMagicKeys()
68
+ const activeElement = useActiveElement()
69
+ const canClose = computed(
70
+ () => (activeElement.value == null || activeElement.value.tagName !== 'CANVAS') && !uiStore.hasUi
71
+ )
72
+ whenever(logicAnd(escape, canClose), toggleFullScreen)
59
73
  </script>
60
74
 
61
75
  <style lang="postcss" scoped>
@@ -1,51 +1,61 @@
1
1
  <template>
2
2
  <div :class="uiStore.isMobile ? 'mobile' : undefined" class="vts-remote-console">
3
- <div ref="consoleContainer" class="console" />
3
+ <VtsLoadingHero :disabled="isReady" type="panel" />
4
+ <div ref="console-container" class="console" />
4
5
  </div>
5
6
  </template>
6
7
 
7
8
  <script lang="ts" setup>
9
+ import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
8
10
  import { useUiStore } from '@core/stores/ui.store'
9
11
  import VncClient from '@novnc/novnc/lib/rfb'
12
+ import { whenever } from '@vueuse/core'
10
13
  import { promiseTimeout } from '@vueuse/shared'
11
14
  import { fibonacci } from 'iterable-backoff'
12
- import { onBeforeUnmount, ref, watchEffect } from 'vue'
15
+ import { onBeforeUnmount, ref, useTemplateRef, watchEffect } from 'vue'
13
16
 
14
17
  const props = defineProps<{
15
18
  url: URL
19
+ isConsoleAvailable: boolean
16
20
  }>()
17
21
 
22
+ const uiStore = useUiStore()
23
+
18
24
  const N_TOTAL_TRIES = 8
19
25
  const FIBONACCI_MS_ARRAY: number[] = Array.from(fibonacci().toMs().take(N_TOTAL_TRIES))
20
26
 
21
- const consoleContainer = ref<HTMLDivElement | null>(null)
27
+ const consoleContainer = useTemplateRef<HTMLDivElement | null>('console-container')
28
+ const isReady = ref(false)
22
29
 
23
30
  let vncClient: VncClient | undefined
24
31
  let nConnectionAttempts = 0
25
32
 
26
33
  function handleDisconnectionEvent() {
27
34
  clearVncClient()
35
+ if (props.isConsoleAvailable) {
36
+ nConnectionAttempts++
28
37
 
29
- nConnectionAttempts++
38
+ if (nConnectionAttempts > N_TOTAL_TRIES) {
39
+ console.error('The number of reconnection attempts has been exceeded for:', props.url)
40
+ return
41
+ }
30
42
 
31
- if (nConnectionAttempts > N_TOTAL_TRIES) {
32
- console.error('The number of reconnection attempts has been exceeded for:', props.url)
33
- return
43
+ console.error(
44
+ `Connection lost for the remote console: ${props.url}. New attempt in ${
45
+ FIBONACCI_MS_ARRAY[nConnectionAttempts - 1]
46
+ }ms`
47
+ )
48
+ createVncConnection()
34
49
  }
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
50
  }
43
51
 
44
52
  function handleConnectionEvent() {
45
53
  nConnectionAttempts = 0
54
+ isReady.value = true
46
55
  }
47
56
 
48
57
  function clearVncClient() {
58
+ isReady.value = false
49
59
  if (vncClient === undefined) {
50
60
  return
51
61
  }
@@ -75,10 +85,34 @@ async function createVncConnection() {
75
85
 
76
86
  vncClient.addEventListener('disconnect', handleDisconnectionEvent)
77
87
  vncClient.addEventListener('connect', handleConnectionEvent)
88
+ const canvas = consoleContainer.value?.querySelector('canvas') as HTMLCanvasElement | null
89
+ if (canvas !== null) {
90
+ // Todo: See with Clémence to specify the desired focus behavior
91
+ canvas.setAttribute('tabindex', '0')
92
+ canvas.addEventListener('focus', () => {
93
+ canvas.classList.add('focused')
94
+ })
95
+
96
+ canvas.addEventListener('blur', () => {
97
+ canvas.classList.remove('focused')
98
+ })
99
+ }
78
100
  }
79
101
 
102
+ whenever(
103
+ () => uiStore.hasUi,
104
+ () => {
105
+ if (!vncClient) {
106
+ return
107
+ }
108
+ // the state is changed from false to true for xo-lite to trigger the change
109
+ vncClient.scaleViewport = false
110
+ vncClient.scaleViewport = true
111
+ }
112
+ )
113
+
80
114
  watchEffect(() => {
81
- if (consoleContainer.value === null) {
115
+ if (consoleContainer.value === null || !props.isConsoleAvailable) {
82
116
  return
83
117
  }
84
118
 
@@ -92,7 +126,9 @@ onBeforeUnmount(() => {
92
126
  clearVncClient()
93
127
  })
94
128
 
95
- const uiStore = useUiStore()
129
+ defineExpose({
130
+ sendCtrlAltDel: () => vncClient?.sendCtrlAltDel(),
131
+ })
96
132
  </script>
97
133
 
98
134
  <style lang="postcss" scoped>
@@ -113,6 +149,12 @@ const uiStore = useUiStore()
113
149
  /* Required because the library adds "margin: auto" to the canvas which makes the canvas centered in space and not aligned to the rest of the layout */
114
150
  :deep(canvas) {
115
151
  margin: 0 auto !important;
152
+ cursor: default !important;
153
+ border: 6px solid transparent;
154
+
155
+ &.focused {
156
+ border: var(--color-success-txt-base) 6px solid;
157
+ }
116
158
  }
117
159
  }
118
160
  </style>
@@ -0,0 +1,58 @@
1
+ <template>
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" />
15
+ </div>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import VtsErrorNoDataHero from '@core/components/state-hero/VtsErrorNoDataHero.vue'
20
+ import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
21
+ import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
22
+ import VtsTable from '@core/components/table/VtsTable.vue'
23
+
24
+ defineProps<{
25
+ isReady?: boolean
26
+ hasError?: boolean
27
+ noDataMessage?: string
28
+ }>()
29
+
30
+ defineSlots<{
31
+ thead(): any
32
+ tbody(): any
33
+ }>()
34
+ </script>
35
+
36
+ <style lang="postcss" scoped>
37
+ .table-container {
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: 0.8rem;
41
+
42
+ :deep(tbody) tr {
43
+ &:hover {
44
+ cursor: pointer;
45
+ background-color: var(--color-info-background-hover);
46
+ }
47
+ &:active {
48
+ background-color: var(--color-info-background-active);
49
+ }
50
+ &.selected {
51
+ background-color: var(--color-info-background-selected);
52
+ }
53
+ &:last-child {
54
+ border-bottom: 0.1rem solid var(--color-neutral-border);
55
+ }
56
+ }
57
+ }
58
+ </style>
@@ -11,7 +11,7 @@
11
11
  >
12
12
  <slot />
13
13
  </MenuTrigger>
14
- <MenuList v-else :disabled="isDisabled" border>
14
+ <MenuList v-else :disabled="isDisabled">
15
15
  <template #trigger="{ open, isOpen }">
16
16
  <MenuTrigger :active="isOpen" :busy="isBusy" :disabled="isDisabled" :icon @click="open">
17
17
  <slot />
@@ -27,28 +27,24 @@
27
27
  import VtsIcon from '@core/components/icon/VtsIcon.vue'
28
28
  import MenuList from '@core/components/menu/MenuList.vue'
29
29
  import MenuTrigger from '@core/components/menu/MenuTrigger.vue'
30
- import { useContext } from '@core/composables/context.composable'
31
- import { DisabledContext } from '@core/context'
30
+ import { useDisabled } from '@core/composables/disabled.composable'
32
31
  import { IK_CLOSE_MENU, IK_MENU_HORIZONTAL } from '@core/utils/injection-keys.util'
33
32
  import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
34
33
  import { faAngleDown, faAngleRight } from '@fortawesome/free-solid-svg-icons'
35
34
  import { computed, inject, ref } from 'vue'
36
35
 
37
- const props = withDefaults(
38
- defineProps<{
39
- icon?: IconDefinition
40
- onClick?: () => any
41
- disabled?: boolean
42
- busy?: boolean
43
- }>(),
44
- { disabled: undefined }
45
- )
36
+ const props = defineProps<{
37
+ icon?: IconDefinition
38
+ onClick?: () => any
39
+ disabled?: boolean
40
+ busy?: boolean
41
+ }>()
46
42
 
47
43
  const isParentHorizontal = inject(
48
44
  IK_MENU_HORIZONTAL,
49
45
  computed(() => false)
50
46
  )
51
- const isDisabled = useContext(DisabledContext, () => props.disabled)
47
+ const isDisabled = useDisabled(() => props.disabled)
52
48
 
53
49
  const submenuIcon = computed(() => (isParentHorizontal.value ? faAngleDown : faAngleRight))
54
50
 
@@ -2,15 +2,20 @@
2
2
  <template>
3
3
  <slot :is-open="isOpen" :open="open" name="trigger" />
4
4
  <Teleport :disabled="!shouldTeleport" to="body">
5
- <ul v-if="!hasTrigger || isOpen" ref="menu" :class="{ horizontal, border }" class="menu-list" v-bind="$attrs">
5
+ <ul
6
+ v-if="!hasTrigger || isOpen"
7
+ ref="menu"
8
+ :class="{ horizontal, border: !noBorder }"
9
+ class="menu-list"
10
+ v-bind="$attrs"
11
+ >
6
12
  <slot />
7
13
  </ul>
8
14
  </Teleport>
9
15
  </template>
10
16
 
11
17
  <script lang="ts" setup>
12
- import { useContext } from '@core/composables/context.composable'
13
- import { DisabledContext } from '@core/context'
18
+ import { useDisabled } from '@core/composables/disabled.composable'
14
19
  import { IK_CLOSE_MENU, IK_MENU_HORIZONTAL, IK_MENU_TELEPORTED } from '@core/utils/injection-keys.util'
15
20
  import { onClickOutside, unrefElement, whenever } from '@vueuse/core'
16
21
  import placementJs, { type Options } from 'placement.js'
@@ -20,15 +25,12 @@ defineOptions({
20
25
  inheritAttrs: false,
21
26
  })
22
27
 
23
- const props = withDefaults(
24
- defineProps<{
25
- horizontal?: boolean
26
- border?: boolean
27
- disabled?: boolean
28
- placement?: Options['placement']
29
- }>(),
30
- { disabled: undefined }
31
- )
28
+ const props = defineProps<{
29
+ horizontal?: boolean
30
+ noBorder?: boolean
31
+ disabled?: boolean
32
+ placement?: Options['placement']
33
+ }>()
32
34
 
33
35
  const slots = useSlots()
34
36
  const isOpen = ref(false)
@@ -42,7 +44,7 @@ provide(
42
44
  computed(() => props.horizontal ?? false)
43
45
  )
44
46
 
45
- useContext(DisabledContext, () => props.disabled)
47
+ useDisabled(() => props.disabled)
46
48
 
47
49
  let clearClickOutsideEvent: (() => void) | undefined
48
50
 
@@ -6,8 +6,7 @@
6
6
  </template>
7
7
 
8
8
  <script lang="ts" setup>
9
- import { useContext } from '@core/composables/context.composable'
10
- import { DisabledContext } from '@core/context'
9
+ import { useDisabled } from '@core/composables/disabled.composable'
11
10
  import { useUiStore } from '@core/stores/ui.store'
12
11
  import { storeToRefs } from 'pinia'
13
12
  import { computed } from 'vue'
@@ -18,12 +17,12 @@ const props = withDefaults(
18
17
  active?: boolean
19
18
  tag?: string
20
19
  }>(),
21
- { tag: 'span', disabled: undefined }
20
+ { tag: 'span' }
22
21
  )
23
22
 
24
23
  const { isMobile } = storeToRefs(useUiStore())
25
24
 
26
- const isDisabled = useContext(DisabledContext, () => props.disabled)
25
+ const isDisabled = useDisabled(() => props.disabled)
27
26
 
28
27
  const classNames = computed(() => {
29
28
  return [
@@ -6,17 +6,13 @@
6
6
  </template>
7
7
 
8
8
  <script lang="ts" setup>
9
- import { useContext } from '@core/composables/context.composable'
10
- import { DisabledContext } from '@core/context'
9
+ import { useDisabled } from '@core/composables/disabled.composable'
11
10
 
12
- const props = withDefaults(
13
- defineProps<{
14
- disabled?: boolean
15
- }>(),
16
- { disabled: undefined }
17
- )
11
+ const props = defineProps<{
12
+ disabled?: boolean
13
+ }>()
18
14
 
19
- useContext(DisabledContext, () => props.disabled)
15
+ useDisabled(() => props.disabled)
20
16
  </script>
21
17
 
22
18
  <style lang="postcss" scoped>
@@ -1,6 +1,6 @@
1
1
  <!-- v1.0 -->
2
2
  <template>
3
- <MenuList :disabled placement="bottom-start" border>
3
+ <MenuList :disabled placement="bottom-start">
4
4
  <template #trigger="{ open, isOpen }">
5
5
  <th
6
6
  :class="{ interactive, disabled, focus: isOpen }"
@@ -7,15 +7,14 @@
7
7
 
8
8
  <script lang="ts" setup>
9
9
  import UiUserLogo from '@core/components/ui/user-logo/UiUserLogo.vue'
10
- import { useContext } from '@core/composables/context.composable'
11
- import { DisabledContext } from '@core/context'
10
+ import { useDisabled } from '@core/composables/disabled.composable'
12
11
 
13
12
  defineProps<{
14
13
  size: 'small' | 'medium'
15
14
  selected?: boolean
16
15
  }>()
17
16
 
18
- const isDisabled = useContext(DisabledContext)
17
+ const isDisabled = useDisabled()
19
18
  </script>
20
19
 
21
20
  <style lang="postcss" scoped>
@@ -9,8 +9,7 @@
9
9
 
10
10
  <script lang="ts" setup>
11
11
  import VtsIcon from '@core/components/icon/VtsIcon.vue'
12
- import { useContext } from '@core/composables/context.composable'
13
- import { DisabledContext } from '@core/context'
12
+ import { useDisabled } from '@core/composables/disabled.composable'
14
13
  import { toVariants } from '@core/utils/to-variants.util'
15
14
  import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
16
15
  import { faLock } from '@fortawesome/free-solid-svg-icons'
@@ -20,26 +19,21 @@ type ButtonVariant = 'primary' | 'secondary' | 'tertiary'
20
19
  type ButtonAccent = 'info' | 'success' | 'warning' | 'danger'
21
20
  type ButtonSize = 'small' | 'medium' | 'large'
22
21
 
23
- const props = withDefaults(
24
- defineProps<{
25
- variant: ButtonVariant
26
- accent: ButtonAccent
27
- size: ButtonSize
28
- busy?: boolean
29
- disabled?: boolean
30
- lockIcon?: boolean
31
- leftIcon?: IconDefinition
32
- }>(),
33
- {
34
- disabled: undefined,
35
- }
36
- )
22
+ const props = defineProps<{
23
+ variant: ButtonVariant
24
+ accent: ButtonAccent
25
+ size: ButtonSize
26
+ busy?: boolean
27
+ disabled?: boolean
28
+ lockIcon?: boolean
29
+ leftIcon?: IconDefinition
30
+ }>()
37
31
 
38
32
  defineSlots<{
39
33
  default(): any
40
34
  }>()
41
35
 
42
- const isDisabled = useContext(DisabledContext, () => props.disabled)
36
+ const isDisabled = useDisabled(() => props.disabled)
43
37
 
44
38
  const fontClasses = {
45
39
  small: 'typo p3-medium',