@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.
- package/lib/components/CardNumbers.vue +15 -8
- package/lib/components/LegendTitle.vue +9 -7
- package/lib/components/backup-item/BackupItem.vue +37 -0
- package/lib/components/{StatusPill.vue → backup-state/BackupState.vue} +5 -3
- package/lib/components/button/ButtonIcon.vue +23 -2
- package/lib/components/console/RemoteConsole.vue +6 -4
- package/lib/components/{DonutChart.vue → donut-chart/DonutChart.vue} +28 -24
- package/lib/components/donut-chart-with-legend/DonutChartWithLegend.vue +27 -0
- package/lib/components/legend/LegendGroup.vue +44 -0
- package/lib/components/{UiLegend.vue → legend/LegendItem.vue} +35 -17
- package/lib/components/legend/LegendList.vue +28 -0
- package/lib/components/{search-bar/SearchBar.vue → query-search-bar/QuerySearchBar.vue} +5 -5
- package/lib/components/stacked-bar/StackedBar.vue +9 -15
- package/lib/components/stacked-bar/StackedBarSegment.vue +9 -6
- package/lib/components/stacked-bar-with-legend/StackedBarWithLegend.vue +47 -0
- package/lib/components/state-hero/ComingSoonHero.vue +6 -2
- package/lib/components/state-hero/LoadingHero.vue +8 -2
- package/lib/components/state-hero/ObjectNotFoundHero.vue +1 -1
- package/lib/components/state-hero/StateHero.vue +27 -9
- package/lib/components/table/ColumnTitle.vue +2 -2
- package/lib/components/tree/TreeItemLabel.vue +1 -11
- package/lib/composables/item-counter.composable.md +25 -0
- package/lib/composables/item-counter.composable.ts +32 -0
- package/lib/composables/route-query/actions/handle-add.ts +9 -0
- package/lib/composables/route-query/actions/handle-delete.ts +16 -0
- package/lib/composables/route-query/actions/handle-set.ts +17 -0
- package/lib/composables/route-query/actions/handle-toggle.ts +18 -0
- package/lib/composables/route-query/types.ts +42 -0
- package/lib/composables/route-query.composable.ts +42 -0
- package/lib/locales/de.json +2 -0
- package/lib/locales/en.json +6 -3
- package/lib/locales/fa.json +2 -0
- package/lib/locales/fr.json +6 -3
- package/lib/stores/ui.store.ts +1 -1
- package/lib/types/backup.type.ts +11 -0
- package/lib/types/utility.type.ts +6 -0
- 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="
|
|
7
|
-
{{
|
|
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
|
|
37
|
-
if (props.max === undefined) {
|
|
38
|
-
return
|
|
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"
|
|
5
|
+
<UiIcon v-tooltip="iconTooltip ?? false" :icon color="info" />
|
|
6
6
|
</div>
|
|
7
7
|
</template>
|
|
8
8
|
|
|
9
|
-
<script
|
|
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
|
-
|
|
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="
|
|
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:
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
function handleConnectionEvent() {
|
|
45
|
+
nConnectionAttempts = 0
|
|
46
|
+
}
|
|
45
47
|
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
25
|
+
export type DonutSegmentColor = 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'disabled'
|
|
26
|
+
|
|
27
|
+
export type DonutSegment = {
|
|
26
28
|
value: number
|
|
27
|
-
color:
|
|
29
|
+
color: DonutSegmentColor
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
type
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
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
|
-
|
|
49
|
+
nextOffset -= percent
|
|
50
|
+
|
|
51
|
+
return {
|
|
54
52
|
color: segment.color,
|
|
55
53
|
percent,
|
|
56
|
-
offset
|
|
54
|
+
offset,
|
|
57
55
|
}
|
|
58
|
-
nextOffset -= percent
|
|
59
|
-
return currentSegment
|
|
60
56
|
})
|
|
61
57
|
})
|
|
62
58
|
</script>
|
|
63
59
|
|
|
64
|
-
<style
|
|
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
|
-
&.
|
|
88
|
+
&.danger {
|
|
85
89
|
--stroke-color: var(--color-red-base);
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
&.
|
|
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="
|
|
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
|
|
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 {
|
|
14
|
+
import { faCircle, faCircleInfo } from '@fortawesome/free-solid-svg-icons'
|
|
15
|
+
import { computed } from 'vue'
|
|
16
16
|
|
|
17
|
-
type
|
|
17
|
+
export type LegendItemColor = 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'disabled'
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
color:
|
|
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
|
-
.
|
|
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
|
-
|
|
53
|
+
|
|
54
|
+
&.danger {
|
|
37
55
|
--circle-color: var(--color-red-base);
|
|
38
56
|
}
|
|
39
|
-
|
|
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
|
-
.
|
|
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
|
+
<!-- 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
|
|
5
|
+
v-for="(segment, index) in segments"
|
|
6
6
|
:key="index"
|
|
7
7
|
:color="segment.color"
|
|
8
|
-
:percentage="segment.
|
|
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:
|
|
19
|
+
color: StackedBarSegmentProps['color']
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
export type StackedBarProps = {
|
|
24
23
|
segments: Segment[]
|
|
25
24
|
maxValue?: number
|
|
26
|
-
}
|
|
25
|
+
}
|
|
27
26
|
|
|
28
|
-
const
|
|
27
|
+
const props = defineProps<StackedBarProps>()
|
|
29
28
|
|
|
30
|
-
const
|
|
29
|
+
const totalValue = computed(() => props.segments.reduce((acc, segment) => acc + segment.value, 0))
|
|
31
30
|
|
|
32
|
-
const
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
&.
|
|
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
|
-
&.
|
|
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
|
|
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,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
|
|
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
|
-
|
|
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:
|
|
55
|
+
gap: var(--gap);
|
|
37
56
|
}
|
|
38
57
|
|
|
39
58
|
.image {
|
|
40
|
-
width:
|
|
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:
|
|
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
|
+
}
|
package/lib/locales/de.json
CHANGED
|
@@ -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",
|
package/lib/locales/en.json
CHANGED
|
@@ -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
|
}
|
package/lib/locales/fa.json
CHANGED
|
@@ -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}",
|
package/lib/locales/fr.json
CHANGED
|
@@ -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
|
}
|
package/lib/stores/ui.store.ts
CHANGED
|
@@ -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: '
|
|
9
|
+
const { store: colorMode } = useColorMode({ initialValue: 'auto' })
|
|
10
10
|
|
|
11
11
|
const { desktop: isDesktop } = useBreakpoints({
|
|
12
12
|
desktop: 1024,
|
|
@@ -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
|
+
"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.
|
|
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",
|