@xen-orchestra/web-core 0.6.0 → 0.8.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 (39) hide show
  1. package/lib/assets/css/typography/_style.pcss +1 -0
  2. package/lib/assets/not-found.svg +129 -0
  3. package/lib/components/card/VtsCardRowKeyValue.vue +48 -0
  4. package/lib/components/layout/VtsLayoutSidebar.vue +1 -1
  5. package/lib/components/state-hero/VtsErrorNoDataHero.vue +11 -0
  6. package/lib/components/state-hero/VtsNoSelectionHero.vue +13 -0
  7. package/lib/components/state-hero/VtsObjectNotFoundHero.vue +3 -2
  8. package/lib/components/state-hero/VtsPageNotFoundHero.vue +30 -0
  9. package/lib/components/state-hero/VtsStateHero.vue +31 -3
  10. package/lib/components/ui/donut-chart/UiDonutChart.vue +2 -2
  11. package/lib/components/ui/dropdown-button/UiDropdownButton.vue +81 -0
  12. package/lib/components/ui/link/UiLink.vue +75 -0
  13. package/lib/components/ui/tag/UiTagsList.vue +14 -0
  14. package/lib/composables/link-component.composable.ts +53 -0
  15. package/lib/locales/cs.json +1 -0
  16. package/lib/locales/de.json +3 -0
  17. package/lib/locales/en.json +5 -0
  18. package/lib/locales/fa.json +3 -0
  19. package/lib/locales/fr.json +5 -0
  20. package/lib/packages/job/README.md +130 -0
  21. package/lib/packages/job/define-job-arg.ts +12 -0
  22. package/lib/packages/job/define-job.ts +130 -0
  23. package/lib/packages/job/index.ts +4 -0
  24. package/lib/packages/job/job-error.ts +14 -0
  25. package/lib/packages/job/use-job-store.ts +44 -0
  26. package/lib/packages/menu/README.md +194 -0
  27. package/lib/packages/menu/action.ts +101 -0
  28. package/lib/packages/menu/base.ts +26 -0
  29. package/lib/packages/menu/context.ts +27 -0
  30. package/lib/packages/menu/index.ts +10 -0
  31. package/lib/packages/menu/job.ts +15 -0
  32. package/lib/packages/menu/link.ts +56 -0
  33. package/lib/packages/menu/menu.ts +50 -0
  34. package/lib/packages/menu/router-link.ts +51 -0
  35. package/lib/packages/menu/structure.ts +88 -0
  36. package/lib/packages/menu/toggle-target.ts +59 -0
  37. package/lib/packages/menu/toggle-trigger.ts +72 -0
  38. package/lib/packages/menu/toggle.ts +43 -0
  39. package/package.json +2 -1
@@ -0,0 +1,26 @@
1
+ import type { MenuAction, MenuLink, MenuRouterLink, MenuToggleTrigger, Menu } from '@core/packages/menu'
2
+ import { computed } from 'vue'
3
+
4
+ export type MenuItem = MenuAction | MenuLink | MenuRouterLink | MenuToggleTrigger
5
+
6
+ export abstract class BaseItem {
7
+ id = Symbol('Menu Item')
8
+
9
+ protected constructor(public menu: Menu) {
10
+ menu.addItem(this as any)
11
+ }
12
+
13
+ get isActive() {
14
+ return computed(() => this.menu.context.activeItemId.value === this.id)
15
+ }
16
+
17
+ activate() {
18
+ this.menu.context.activeItemId.value = this.id
19
+ }
20
+
21
+ deactivate() {
22
+ if (this.isActive.value) {
23
+ this.menu.context.activeItemId.value = undefined
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,27 @@
1
+ import { uniqueId } from '@core/utils/unique-id.util'
2
+ import { useEventListener } from '@vueuse/core'
3
+ import { type Ref, ref } from 'vue'
4
+
5
+ export class MenuContext {
6
+ id: string
7
+
8
+ activeItemId: Ref<symbol | undefined> = ref()
9
+
10
+ constructor() {
11
+ this.id = uniqueId()
12
+
13
+ useEventListener(window, 'click', (event: PointerEvent) => {
14
+ if (this.hasSameController(event)) {
15
+ return
16
+ }
17
+
18
+ this.activeItemId.value = undefined
19
+ })
20
+ }
21
+
22
+ hasSameController(event: PointerEvent) {
23
+ return Array.from(window.document.querySelectorAll(`[data-menu-id="${this.id}"]`)).some(
24
+ el => el === event.target || event.composedPath().includes(el)
25
+ )
26
+ }
27
+ }
@@ -0,0 +1,10 @@
1
+ export * from './base'
2
+ export * from './action'
3
+ export * from './context'
4
+ export * from './link'
5
+ export * from './menu'
6
+ export * from './router-link'
7
+ export * from './structure'
8
+ export * from './toggle'
9
+ export * from './toggle-target'
10
+ export * from './toggle-trigger'
@@ -0,0 +1,15 @@
1
+ import type { Job } from '@core/packages/job'
2
+ import { action } from '@core/packages/menu/action'
3
+ import type { MenuLike } from '@core/packages/menu/menu'
4
+ import { parseConfigHolder } from '@core/packages/menu/structure'
5
+
6
+ export function job(job: Job<any>) {
7
+ return action(() => job.run(), {
8
+ busy: job.isRunning,
9
+ disabled: job.errorMessage,
10
+ })
11
+ }
12
+
13
+ export function useMenuJob(config: { job: Job<any>; parent: MenuLike }) {
14
+ return parseConfigHolder(config.parent, job(config.job))
15
+ }
@@ -0,0 +1,56 @@
1
+ import { BaseItem, type Menu, type MenuLike, parseConfigHolder } from '@core/packages/menu'
2
+ import { computed, type MaybeRefOrGetter, reactive, toValue } from 'vue'
3
+
4
+ export interface MenuLinkConfig {
5
+ href: MaybeRefOrGetter<string>
6
+ rel?: MaybeRefOrGetter<string>
7
+ target?: MaybeRefOrGetter<string>
8
+ }
9
+
10
+ export class MenuLinkConfigHolder {
11
+ constructor(public config: MenuLinkConfig) {}
12
+ }
13
+
14
+ export interface MenuLinkProps {
15
+ as: 'a'
16
+ href: string
17
+ rel: string
18
+ target: string
19
+ onClick: () => void
20
+ onMouseenter: () => void
21
+ 'data-menu-id': string
22
+ }
23
+
24
+ export class MenuLink extends BaseItem {
25
+ constructor(
26
+ public menu: Menu,
27
+ public config: MenuLinkConfig
28
+ ) {
29
+ super(menu)
30
+ }
31
+
32
+ get props(): MenuLinkProps {
33
+ return reactive({
34
+ as: 'a',
35
+ onMouseenter: () => this.activate(),
36
+ onClick: () => this.deactivate(),
37
+ href: computed(() => toValue(this.config.href)),
38
+ rel: computed(() => toValue(this.config.rel) ?? 'noreferrer noopener'),
39
+ target: computed(() => toValue(this.config.target) ?? '_blank'),
40
+ 'data-menu-id': this.menu.context.id,
41
+ })
42
+ }
43
+ }
44
+
45
+ export function link(href: MaybeRefOrGetter<string>, config: Omit<MenuLinkConfig, 'href'> = {}) {
46
+ return new MenuLinkConfigHolder({
47
+ ...config,
48
+ href,
49
+ })
50
+ }
51
+
52
+ export function useMenuLink(config: MenuLinkConfig & { parent: MenuLike }) {
53
+ const { parent, href, ...configRest } = config
54
+
55
+ return parseConfigHolder(parent, link(href, configRest))
56
+ }
@@ -0,0 +1,50 @@
1
+ import {
2
+ MenuAction,
3
+ MenuContext,
4
+ MenuLink,
5
+ MenuRouterLink,
6
+ type MenuStructure,
7
+ MenuToggleTrigger,
8
+ type ParseStructure,
9
+ parseStructure,
10
+ } from '@core/packages/menu'
11
+ import { extendRef } from '@vueuse/core'
12
+ import { computed, type ComputedRef, type MaybeRefOrGetter, shallowReactive, type ShallowReactive } from 'vue'
13
+
14
+ export const MENU_SYMBOL = Symbol('Submenu')
15
+
16
+ export type WithMenu = { [MENU_SYMBOL]: Menu }
17
+
18
+ export type MenuLike = Menu | WithMenu
19
+
20
+ type MenuItem = MenuAction | MenuLink | MenuRouterLink | MenuToggleTrigger
21
+
22
+ export class Menu {
23
+ items: ShallowReactive<Map<symbol, MenuItem>> = shallowReactive(new Map())
24
+
25
+ isActive = computed(() => Array.from(this.items.values()).some(item => item.isActive.value))
26
+
27
+ constructor(public context: MenuContext) {}
28
+
29
+ addItem(item: MenuItem) {
30
+ this.items.set(item.id, item)
31
+ }
32
+ }
33
+
34
+ export function useMenu<const TStructure extends MenuStructure>(
35
+ structure: MaybeRefOrGetter<TStructure>
36
+ ): ComputedRef<ParseStructure<TStructure> & WithMenu> & WithMenu {
37
+ const context = new MenuContext()
38
+
39
+ const menu = new Menu(context)
40
+
41
+ return extendRef(
42
+ computed(() => {
43
+ return {
44
+ ...parseStructure(menu, structure),
45
+ [MENU_SYMBOL]: menu,
46
+ }
47
+ }),
48
+ { [MENU_SYMBOL]: menu }
49
+ )
50
+ }
@@ -0,0 +1,51 @@
1
+ import { BaseItem, type Menu, type MenuLike, parseConfigHolder } from '@core/packages/menu'
2
+ import { computed, markRaw, type MaybeRefOrGetter, reactive, toValue } from 'vue'
3
+ import { type RouteLocationRaw, RouterLink } from 'vue-router'
4
+
5
+ export interface MenuRouterLinkConfig {
6
+ to: MaybeRefOrGetter<RouteLocationRaw>
7
+ }
8
+
9
+ export class MenuRouterLinkConfigHolder {
10
+ constructor(public config: MenuRouterLinkConfig) {}
11
+ }
12
+
13
+ export interface MenuRouterLinkProps {
14
+ as: typeof RouterLink
15
+ to: RouteLocationRaw
16
+ onClick: () => void
17
+ onMouseenter: () => void
18
+ 'data-menu-id': string
19
+ }
20
+
21
+ export class MenuRouterLink extends BaseItem {
22
+ constructor(
23
+ public menu: Menu,
24
+ public config: MenuRouterLinkConfig
25
+ ) {
26
+ super(menu)
27
+ }
28
+
29
+ get props(): MenuRouterLinkProps {
30
+ return reactive({
31
+ as: markRaw(RouterLink),
32
+ onMouseenter: () => this.activate(),
33
+ onClick: () => this.deactivate(),
34
+ to: computed(() => toValue(this.config.to)),
35
+ 'data-menu-id': this.menu.context.id,
36
+ })
37
+ }
38
+ }
39
+
40
+ export function routerLink(to: MaybeRefOrGetter<RouteLocationRaw>, config: Omit<MenuRouterLinkConfig, 'to'> = {}) {
41
+ return new MenuRouterLinkConfigHolder({
42
+ ...config,
43
+ to,
44
+ })
45
+ }
46
+
47
+ export function useMenuRouterLink(config: MenuRouterLinkConfig & { parent: MenuLike }) {
48
+ const { parent, to, ...configRest } = config
49
+
50
+ return parseConfigHolder(parent, routerLink(to, configRest))
51
+ }
@@ -0,0 +1,88 @@
1
+ import {
2
+ Menu,
3
+ MENU_SYMBOL,
4
+ MenuAction,
5
+ type MenuActionProps,
6
+ MenuActionConfigHolder,
7
+ type MenuLike,
8
+ MenuLink,
9
+ type MenuLinkProps,
10
+ MenuLinkConfigHolder,
11
+ MenuRouterLink,
12
+ type MenuRouterLinkProps,
13
+ MenuRouterLinkConfigHolder,
14
+ type MenuToggleProps,
15
+ MenuToggleConfigHolder,
16
+ MenuToggleTarget,
17
+ MenuToggleTrigger,
18
+ } from '@core/packages/menu'
19
+ import { type MaybeRefOrGetter, toValue } from 'vue'
20
+
21
+ export type MenuStructure = {
22
+ [K: string]: ConfigHolder
23
+ }
24
+
25
+ export type ConfigHolder =
26
+ | MenuActionConfigHolder
27
+ | MenuLinkConfigHolder
28
+ | MenuRouterLinkConfigHolder
29
+ | MenuToggleConfigHolder<any>
30
+
31
+ export type ParseConfigHolder<TConfigHolder extends ConfigHolder> = TConfigHolder extends MenuActionConfigHolder
32
+ ? MenuActionProps
33
+ : TConfigHolder extends MenuLinkConfigHolder
34
+ ? MenuLinkProps
35
+ : TConfigHolder extends MenuRouterLinkConfigHolder
36
+ ? MenuRouterLinkProps
37
+ : TConfigHolder extends MenuToggleConfigHolder<infer TItems>
38
+ ? MenuToggleProps<TItems>
39
+ : never
40
+
41
+ export type ParseStructure<TStructure extends MenuStructure> = {
42
+ [K in keyof TStructure]: ParseConfigHolder<TStructure[K]>
43
+ }
44
+
45
+ export function parseConfigHolder<TConfigHolder extends ConfigHolder>(
46
+ menuLike: MenuLike,
47
+ configHolder: TConfigHolder
48
+ ): ParseConfigHolder<TConfigHolder> {
49
+ const menu = menuLike instanceof Menu ? menuLike : menuLike[MENU_SYMBOL]
50
+
51
+ if (configHolder instanceof MenuActionConfigHolder) {
52
+ return new MenuAction(menu, configHolder.config).props as ParseConfigHolder<TConfigHolder>
53
+ }
54
+
55
+ if (configHolder instanceof MenuLinkConfigHolder) {
56
+ return new MenuLink(menu, configHolder.config).props as ParseConfigHolder<TConfigHolder>
57
+ }
58
+
59
+ if (configHolder instanceof MenuRouterLinkConfigHolder) {
60
+ return new MenuRouterLink(menu, configHolder.config).props as ParseConfigHolder<TConfigHolder>
61
+ }
62
+
63
+ if (configHolder instanceof MenuToggleConfigHolder) {
64
+ const trigger = new MenuToggleTrigger(menu, configHolder.config)
65
+ const target = new MenuToggleTarget(trigger, configHolder.config)
66
+
67
+ return {
68
+ $trigger: trigger.props,
69
+ $target: target.props,
70
+ $isOpen: trigger.isOpen,
71
+ [MENU_SYMBOL]: trigger.subMenu,
72
+ ...parseStructure(trigger.subMenu, configHolder.config.items),
73
+ } as ParseConfigHolder<TConfigHolder>
74
+ }
75
+
76
+ throw new Error('Unsupported config')
77
+ }
78
+
79
+ export function parseStructure<TStructure extends MenuStructure>(
80
+ menu: Menu,
81
+ structure: MaybeRefOrGetter<TStructure>
82
+ ): ParseStructure<TStructure> {
83
+ return Object.fromEntries(
84
+ Object.entries(toValue(structure)).map(([key, configHolder]) => {
85
+ return [key, parseConfigHolder(menu, configHolder)]
86
+ })
87
+ ) as ParseStructure<TStructure>
88
+ }
@@ -0,0 +1,59 @@
1
+ import type { MenuToggleTrigger } from '@core/packages/menu'
2
+ import { autoUpdate, flip, type Placement, shift, useFloating, type UseFloatingReturn } from '@floating-ui/vue'
3
+ import { unrefElement } from '@vueuse/core'
4
+ import { computed, reactive, ref, type Ref, toValue, type UnwrapRef } from 'vue'
5
+
6
+ export interface MenuToggleTargetConfig {
7
+ placement?: Placement
8
+ }
9
+
10
+ export interface MenuToggleTargetProps {
11
+ ref: (el: any) => void
12
+ style: {
13
+ display: string | undefined
14
+ zIndex: number
15
+ } & UnwrapRef<UseFloatingReturn['floatingStyles']>
16
+ 'data-menu-id': string
17
+ }
18
+
19
+ export class MenuToggleTarget {
20
+ element: Ref<HTMLElement | null> = ref(null)
21
+
22
+ styles: UseFloatingReturn['floatingStyles']
23
+
24
+ constructor(
25
+ public trigger: MenuToggleTrigger,
26
+ public config: MenuToggleTargetConfig
27
+ ) {
28
+ const { floatingStyles } = useFloating(trigger.element, this.element, {
29
+ placement: computed(() => toValue(config.placement) ?? 'bottom-start'),
30
+ open: trigger.isOpen,
31
+ whileElementsMounted: autoUpdate,
32
+ middleware: [shift(), flip()],
33
+ })
34
+
35
+ this.styles = floatingStyles
36
+ }
37
+
38
+ get props(): MenuToggleTargetProps {
39
+ return reactive({
40
+ as: 'button',
41
+ type: 'button',
42
+ ref: (element: HTMLElement | null | undefined) => {
43
+ const newElement = unrefElement(element)
44
+
45
+ if (newElement !== this.element.value) {
46
+ this.element.value = newElement ?? null
47
+ }
48
+ },
49
+ style: computed(() => {
50
+ return {
51
+ display: this.trigger.isOpen.value ? undefined : 'none',
52
+ zIndex: 9999,
53
+ ...this.styles.value,
54
+ }
55
+ }),
56
+ 'data-menu-id': this.trigger.menu.context.id,
57
+ })
58
+ }
59
+ }
@@ -0,0 +1,72 @@
1
+ import { BaseItem, Menu } from '@core/packages/menu'
2
+ import { unrefElement, whenever } from '@vueuse/core'
3
+ import { computed, type ComputedRef, type MaybeRefOrGetter, reactive, ref, type Ref, toValue } from 'vue'
4
+
5
+ export interface MenuToggleTriggerConfig {
6
+ behavior?: MaybeRefOrGetter<'click' | 'mouseenter'>
7
+ }
8
+
9
+ export interface MenuToggleTriggerProps {
10
+ ref: (el: any) => void
11
+ as: 'button'
12
+ type: 'button'
13
+ submenu: true
14
+ onClick: () => void
15
+ onMouseenter: () => void
16
+ selected: boolean
17
+ 'data-menu-id': string
18
+ }
19
+
20
+ export class MenuToggleTrigger extends BaseItem {
21
+ element: Ref<HTMLElement | null> = ref(null)
22
+
23
+ subMenu: Menu
24
+
25
+ isSelfOpen = ref(false)
26
+
27
+ isOpen = computed(() => this.isSelfOpen.value || this.subMenu.isActive.value)
28
+
29
+ constructor(
30
+ public menu: Menu,
31
+ public config: MenuToggleTriggerConfig
32
+ ) {
33
+ super(menu)
34
+ this.subMenu = new Menu(menu.context)
35
+
36
+ whenever(
37
+ () => !this.isActive.value && !this.subMenu.isActive.value,
38
+ () => {
39
+ this.isSelfOpen.value = false
40
+ }
41
+ )
42
+ }
43
+
44
+ get isActive(): ComputedRef<boolean> {
45
+ return computed(() => super.isActive.value || this.subMenu?.isActive.value)
46
+ }
47
+
48
+ get props(): MenuToggleTriggerProps {
49
+ return reactive({
50
+ ref: (element: HTMLElement | null | undefined) => {
51
+ const newElement = unrefElement(element)
52
+
53
+ if (newElement !== this.element.value) {
54
+ this.element.value = newElement ?? null
55
+ }
56
+ },
57
+ as: 'button',
58
+ type: 'button',
59
+ submenu: true,
60
+ onClick: () => (this.isSelfOpen.value = !this.isOpen.value),
61
+ onMouseenter: () => {
62
+ this.activate()
63
+
64
+ if (toValue(this.config.behavior) === 'mouseenter') {
65
+ this.isSelfOpen.value = true
66
+ }
67
+ },
68
+ selected: computed(() => this.isOpen.value),
69
+ 'data-menu-id': this.menu.context.id,
70
+ })
71
+ }
72
+ }
@@ -0,0 +1,43 @@
1
+ import {
2
+ Menu,
3
+ MenuContext,
4
+ type MenuLike,
5
+ type MenuStructure,
6
+ type MenuToggleTargetConfig,
7
+ type MenuToggleTargetProps,
8
+ type MenuToggleTriggerConfig,
9
+ type MenuToggleTriggerProps,
10
+ parseConfigHolder,
11
+ type ParseStructure,
12
+ type WithMenu,
13
+ } from '@core/packages/menu'
14
+ import { computed, type ComputedRef, type MaybeRefOrGetter } from 'vue'
15
+
16
+ export type MenuToggleConfig<TStructure extends MenuStructure> = MenuToggleTriggerConfig &
17
+ MenuToggleTargetConfig & {
18
+ items?: MaybeRefOrGetter<TStructure>
19
+ }
20
+
21
+ export class MenuToggleConfigHolder<TStructure extends MenuStructure> {
22
+ constructor(public config: MenuToggleConfig<TStructure>) {}
23
+ }
24
+
25
+ export type MenuToggleProps<TStructure extends MenuStructure> = WithMenu &
26
+ ParseStructure<TStructure> & {
27
+ $trigger: MenuToggleTriggerProps
28
+ $target: MenuToggleTargetProps
29
+ $isOpen: ComputedRef<boolean>
30
+ }
31
+
32
+ export function toggle<const TStructure extends MenuStructure>(config: MenuToggleConfig<TStructure>) {
33
+ return new MenuToggleConfigHolder(config)
34
+ }
35
+
36
+ export function useMenuToggle<const TStructure extends MenuStructure>(
37
+ config: MenuToggleConfig<TStructure> & { parent?: MenuLike } = {}
38
+ ) {
39
+ const { parent, ...toggleConfig } = config
40
+ const menu = parent ?? new Menu(new MenuContext())
41
+
42
+ return computed(() => parseConfigHolder(menu, toggle<TStructure>(toggleConfig)))
43
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xen-orchestra/web-core",
3
3
  "type": "module",
4
- "version": "0.6.0",
4
+ "version": "0.8.0",
5
5
  "private": false,
6
6
  "exports": {
7
7
  "./*": {
@@ -10,6 +10,7 @@
10
10
  }
11
11
  },
12
12
  "dependencies": {
13
+ "@floating-ui/vue": "^1.1.5",
13
14
  "@fontsource/poppins": "^5.0.14",
14
15
  "@fortawesome/fontawesome-common-types": "^6.5.1",
15
16
  "@fortawesome/free-regular-svg-icons": "^6.5.1",