mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-09-10 22:14:48 -05:00
feat(web): new REST API and XO objects configuration system (#7918)
Major revamp of the REST API configuration to get a single source of truth + stronger typing + support for single objects.
This commit is contained in:
committed by
GitHub
parent
79ce97b3fd
commit
f78a140fbb
20
@xen-orchestra/web/docs/rest-api.md
Normal file
20
@xen-orchestra/web/docs/rest-api.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# REST API
|
||||
|
||||
## How to handle a new record type from the REST API?
|
||||
|
||||
1. Create the new record type in the `types/xo/<name>.type.ts`
|
||||
|
||||
Define the `id` as a branded property (e.g.: `id: Branded<'vm'>`)
|
||||
|
||||
Define the relations with `id` property of the relational record (e.g.: `pool: XoPool['id']`)
|
||||
|
||||
2. Add the REST API configuration for this record in `utils/xo-api-definition.util.ts`
|
||||
3. Create the store in `stores/xo-rest-api/<name>.store.ts`
|
||||
|
||||
For a basic store:
|
||||
|
||||
```typescript
|
||||
const config = createXoStoreConfig('<name>')
|
||||
|
||||
return createSubscribableStoreContext(config, {})
|
||||
```
|
||||
@@ -23,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Host } from '@/types/host.type'
|
||||
import type { XoHost } from '@/types/xo/host.type'
|
||||
import type { HostState } from '@core/types/object-icon.type'
|
||||
import HeadBar from '@core/components/head-bar/HeadBar.vue'
|
||||
import ObjectIcon from '@core/components/icon/ObjectIcon.vue'
|
||||
@@ -31,6 +31,6 @@ import TabItem from '@core/components/tab/TabItem.vue'
|
||||
import TabList from '@core/components/tab/TabList.vue'
|
||||
|
||||
defineProps<{
|
||||
host: Host
|
||||
host: XoHost
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Pool } from '@/types/pool.type'
|
||||
import type { XoPool } from '@/types/xo/pool.type'
|
||||
import HeadBar from '@core/components/head-bar/HeadBar.vue'
|
||||
import TabItem from '@core/components/tab/TabItem.vue'
|
||||
import TabList from '@core/components/tab/TabList.vue'
|
||||
import { faCity } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
defineProps<{
|
||||
pool: Pool
|
||||
pool: XoPool
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useHostStore } from '@/stores/xo-rest-api/host.store'
|
||||
import { HOST_POWER_STATE } from '@/types/host.type'
|
||||
import { HOST_POWER_STATE } from '@/types/xo/host.type'
|
||||
import type { DonutChartWithLegendProps } from '@core/components/donut-chart-with-legend/DonutChartWithLegend.vue'
|
||||
import CardTitle from '@core/components/card/CardTitle.vue'
|
||||
import CardNumbers from '@core/components/CardNumbers.vue'
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
|
||||
import { VM_POWER_STATE } from '@/types/vm.type'
|
||||
import { VM_POWER_STATE } from '@/types/xo/vm.type'
|
||||
import CardTitle from '@core/components/card/CardTitle.vue'
|
||||
import CardNumbers from '@core/components/CardNumbers.vue'
|
||||
import DonutChartWithLegend, {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<script lang="ts" setup>
|
||||
import VmTreeList from '@/components/tree/VmTreeList.vue'
|
||||
import { useHostStore } from '@/stores/xo-rest-api/host.store'
|
||||
import type { HostBranch } from '@/types/host.type'
|
||||
import type { HostBranch } from '@/types/tree.type'
|
||||
import UiIcon from '@core/components/icon/UiIcon.vue'
|
||||
import TreeItem from '@core/components/tree/TreeItem.vue'
|
||||
import TreeItemLabel from '@core/components/tree/TreeItemLabel.vue'
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import HostTreeItem from '@/components/tree/HostTreeItem.vue'
|
||||
import type { HostBranch } from '@/types/host.type'
|
||||
import type { HostBranch } from '@/types/tree.type'
|
||||
|
||||
defineProps<{
|
||||
branches: HostBranch[]
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
<script lang="ts" setup>
|
||||
import HostTreeList from '@/components/tree/HostTreeList.vue'
|
||||
import VmTreeList from '@/components/tree/VmTreeList.vue'
|
||||
import type { HostBranch } from '@/types/host.type'
|
||||
import type { PoolBranch } from '@/types/pool.type'
|
||||
import type { VmLeaf } from '@/types/vm.type'
|
||||
import type { HostBranch, PoolBranch, VmLeaf } from '@/types/tree.type'
|
||||
import TreeItem from '@core/components/tree/TreeItem.vue'
|
||||
import TreeItemLabel from '@core/components/tree/TreeItemLabel.vue'
|
||||
import TreeList from '@core/components/tree/TreeList.vue'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PoolTreeItem from '@/components/tree/PoolTreeItem.vue'
|
||||
import type { PoolBranch } from '@/types/pool.type'
|
||||
import type { PoolBranch } from '@/types/tree.type'
|
||||
import TreeList from '@core/components/tree/TreeList.vue'
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { VmLeaf } from '@/types/vm.type'
|
||||
import type { VmLeaf } from '@/types/tree.type'
|
||||
import type { POWER_STATE } from '@core/types/power-state.type'
|
||||
import ObjectIcon from '@core/components/icon/ObjectIcon.vue'
|
||||
import TreeItem from '@core/components/tree/TreeItem.vue'
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import VmTreeItem from '@/components/tree/VmTreeItem.vue'
|
||||
import type { VmLeaf } from '@/types/vm.type'
|
||||
import type { VmLeaf } from '@/types/tree.type'
|
||||
|
||||
defineProps<{
|
||||
leaves: VmLeaf[]
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
</RouterView>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import HostHeader from '@/components/host/HostHeader.vue'
|
||||
import { useHostStore } from '@/stores/xo-rest-api/host.store'
|
||||
import type { RecordId } from '@/types/xo-object.type'
|
||||
import type { XoHost } from '@/types/xo/host.type'
|
||||
import LoadingHero from '@core/components/state-hero/LoadingHero.vue'
|
||||
import ObjectNotFoundHero from '@core/components/state-hero/ObjectNotFoundHero.vue'
|
||||
import { computed } from 'vue'
|
||||
@@ -20,5 +20,5 @@ const route = useRoute<'/host/[id]'>()
|
||||
|
||||
const { isReady, get } = useHostStore().subscribe()
|
||||
|
||||
const host = computed(() => get(route.params.id as RecordId<'host'>))
|
||||
const host = computed(() => get(route.params.id as XoHost['id']))
|
||||
</script>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
|
||||
import type { Host } from '@/types/host.type'
|
||||
import type { XoHost } from '@/types/xo/host.type'
|
||||
import type { VmState } from '@core/types/object-icon.type'
|
||||
import CellObject from '@core/components/cell-object/CellObject.vue'
|
||||
import CellText from '@core/components/cell-text/CellText.vue'
|
||||
@@ -45,7 +45,7 @@ import { faAlignLeft, faDesktop } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
host: Host
|
||||
host: XoHost
|
||||
}>()
|
||||
|
||||
const { isReady, vmsByHost } = useVmStore().subscribe()
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
</RouterView>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import PoolHeader from '@/components/pool/PoolHeader.vue'
|
||||
import { usePoolStore } from '@/stores/xo-rest-api/pool.store'
|
||||
import type { RecordId } from '@/types/xo-object.type'
|
||||
import type { XoPool } from '@/types/xo/pool.type'
|
||||
import LoadingHero from '@core/components/state-hero/LoadingHero.vue'
|
||||
import ObjectNotFoundHero from '@core/components/state-hero/ObjectNotFoundHero.vue'
|
||||
import { computed } from 'vue'
|
||||
@@ -20,5 +20,5 @@ const route = useRoute<'/pool/[id]'>()
|
||||
|
||||
const { isReady, get } = usePoolStore().subscribe()
|
||||
|
||||
const pool = computed(() => get(route.params.id as RecordId<'pool'>))
|
||||
const pool = computed(() => get(route.params.id as XoPool['id']))
|
||||
</script>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useHostStore } from '@/stores/xo-rest-api/host.store'
|
||||
import type { Pool } from '@/types/pool.type'
|
||||
import type { XoPool } from '@/types/xo/pool.type'
|
||||
import type { HostState } from '@core/types/object-icon.type'
|
||||
import CellObject from '@core/components/cell-object/CellObject.vue'
|
||||
import CellText from '@core/components/cell-text/CellText.vue'
|
||||
@@ -45,7 +45,7 @@ import { faAlignLeft, faServer } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
pool: Pool
|
||||
pool: XoPool
|
||||
}>()
|
||||
|
||||
const { isReady, hostsByPool } = useHostStore().subscribe()
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
|
||||
import type { Pool } from '@/types/pool.type'
|
||||
import type { XoPool } from '@/types/xo/pool.type'
|
||||
import type { VmState } from '@core/types/object-icon.type'
|
||||
import CellObject from '@core/components/cell-object/CellObject.vue'
|
||||
import CellText from '@core/components/cell-text/CellText.vue'
|
||||
@@ -45,7 +45,7 @@ import { faAlignLeft, faDesktop } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
pool: Pool
|
||||
pool: XoPool
|
||||
}>()
|
||||
|
||||
const { isReady, records } = useVmStore().subscribe()
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<script lang="ts" setup>
|
||||
import VmHeader from '@/components/vm/VmHeader.vue'
|
||||
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
|
||||
import type { RecordId } from '@/types/xo-object.type'
|
||||
import type { XoVm } from '@/types/xo/vm.type'
|
||||
import type { VmState } from '@core/types/object-icon.type'
|
||||
import LoadingHero from '@core/components/state-hero/LoadingHero.vue'
|
||||
import ObjectNotFoundHero from '@core/components/state-hero/ObjectNotFoundHero.vue'
|
||||
@@ -21,7 +21,7 @@ const route = useRoute<'/vm/[id]'>()
|
||||
|
||||
const { isReady, get: getVm } = useVmStore().subscribe()
|
||||
|
||||
const vm = computed(() => getVm(route.params.id as RecordId<'VM'>))
|
||||
const vm = computed(() => getVm(route.params.id as XoVm['id']))
|
||||
|
||||
const powerState = computed(() => vm.value?.power_state.toLocaleLowerCase() as VmState | undefined)
|
||||
</script>
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Vm } from '@/types/vm.type'
|
||||
import type { XoVm } from '@/types/xo/vm.type'
|
||||
import RemoteConsole from '@core/components/console/RemoteConsole.vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
vm: Vm
|
||||
vm: XoVm
|
||||
}>()
|
||||
|
||||
const url = computed(() => new URL(`/api/consoles/${props.vm.id}`, window.location.origin.replace(/^http/, 'ws')))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { usePoolStore } from '@/stores/xo-rest-api/pool.store'
|
||||
import type { Host } from '@/types/host.type'
|
||||
import type { RecordId } from '@/types/xo-object.type'
|
||||
import type { XoHost } from '@/types/xo/host.type'
|
||||
import type { XoPool } from '@/types/xo/pool.type'
|
||||
import { createXoStoreConfig } from '@/utils/create-xo-store-config.util'
|
||||
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
|
||||
import { sortByNameLabel } from '@core/utils/sort-by-name-label.util'
|
||||
@@ -16,11 +16,10 @@ export const useHostStore = defineStore('host', () => {
|
||||
sortBy: sortByNameLabel,
|
||||
})
|
||||
|
||||
const isMasterHost = (hostId: RecordId<'host'>) =>
|
||||
!!deps.poolStore.$context.records.find(pool => pool.master === hostId)
|
||||
const isMasterHost = (hostId: XoHost['id']) => !!deps.poolStore.$context.records.find(pool => pool.master === hostId)
|
||||
|
||||
const hostsByPool = computed(() => {
|
||||
const hostsByPoolMap = new Map<RecordId<'pool'>, Host[]>()
|
||||
const hostsByPoolMap = new Map<XoPool['id'], XoHost[]>()
|
||||
|
||||
baseContext.records.value.forEach(host => {
|
||||
const poolId = host.$pool
|
||||
|
||||
@@ -1,36 +1,15 @@
|
||||
import type { Vm } from '@/types/vm.type'
|
||||
import { VM_POWER_STATE } from '@/types/vm.type'
|
||||
import type { RecordId } from '@/types/xo-object.type'
|
||||
import type { XoHost } from '@/types/xo/host.type'
|
||||
import type { XoPool } from '@/types/xo/pool.type'
|
||||
import type { XoVm } from '@/types/xo/vm.type'
|
||||
import { VM_POWER_STATE } from '@/types/xo/vm.type'
|
||||
import { createXoStoreConfig } from '@/utils/create-xo-store-config.util'
|
||||
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
|
||||
import { sortByNameLabel } from '@core/utils/sort-by-name-label.util'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
function createVmsMap<THostLess extends boolean>(vms: Vm[], hostLess: THostLess) {
|
||||
const vmsMap = new Map<THostLess extends true ? RecordId<'pool'> : RecordId<'host'>, Vm[]>()
|
||||
|
||||
vms.forEach(vm => {
|
||||
const hasHost = vm.$container !== vm.$pool
|
||||
|
||||
if (hasHost && hostLess) {
|
||||
return
|
||||
}
|
||||
|
||||
const id = vm.$container as THostLess extends true ? RecordId<'pool'> : RecordId<'host'>
|
||||
|
||||
if (!vmsMap.has(id)) {
|
||||
vmsMap.set(id, [])
|
||||
}
|
||||
|
||||
vmsMap.get(id)!.push(vm)
|
||||
})
|
||||
|
||||
return vmsMap
|
||||
}
|
||||
|
||||
export const useVmStore = defineStore('vm', () => {
|
||||
const { context: baseContext, ...configRest } = createXoStoreConfig('VM', {
|
||||
const { context: baseContext, ...configRest } = createXoStoreConfig('vm', {
|
||||
sortBy: sortByNameLabel,
|
||||
})
|
||||
|
||||
@@ -49,3 +28,25 @@ export const useVmStore = defineStore('vm', () => {
|
||||
|
||||
return createSubscribableStoreContext({ context, ...configRest }, {})
|
||||
})
|
||||
|
||||
function createVmsMap<THostLess extends boolean>(vms: XoVm[], hostLess: THostLess) {
|
||||
const vmsMap = new Map<THostLess extends true ? XoPool['id'] : XoHost['id'], XoVm[]>()
|
||||
|
||||
vms.forEach(vm => {
|
||||
const hasHost = vm.$container !== vm.$pool
|
||||
|
||||
if (hasHost && hostLess) {
|
||||
return
|
||||
}
|
||||
|
||||
const id = vm.$container as THostLess extends true ? XoPool['id'] : XoHost['id']
|
||||
|
||||
if (!vmsMap.has(id)) {
|
||||
vmsMap.set(id, [])
|
||||
}
|
||||
|
||||
vmsMap.get(id)!.push(vm)
|
||||
})
|
||||
|
||||
return vmsMap
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { VmLeaf } from '@/types/vm.type'
|
||||
import type { RecordId } from '@/types/xo-object.type'
|
||||
import type { Branch } from '@core/composables/tree/branch'
|
||||
|
||||
export enum HOST_POWER_STATE {
|
||||
HALTED = 'Halted',
|
||||
RUNNING = 'Running',
|
||||
UNKNOWN = 'Unknown',
|
||||
}
|
||||
|
||||
export type Host = {
|
||||
id: RecordId<'host'>
|
||||
type: 'host'
|
||||
$pool: RecordId<'pool'>
|
||||
_xapiRef: string
|
||||
address: string
|
||||
enabled: boolean
|
||||
name_label: string
|
||||
name_description: string
|
||||
power_state: HOST_POWER_STATE
|
||||
residentVms: RecordId<'VM'>[]
|
||||
}
|
||||
|
||||
export type HostBranch = Branch<Host, VmLeaf, 'host'>
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { HostBranch } from '@/types/host.type'
|
||||
import type { VmLeaf } from '@/types/vm.type'
|
||||
import type { RecordId } from '@/types/xo-object.type'
|
||||
import type { Branch } from '@core/composables/tree/branch'
|
||||
|
||||
export type Pool = {
|
||||
id: RecordId<'pool'>
|
||||
type: 'pool'
|
||||
$pool: RecordId<'pool'>
|
||||
master: RecordId<'host'>
|
||||
name_description: string
|
||||
name_label: string
|
||||
_xapiRef: string
|
||||
}
|
||||
|
||||
export type PoolBranch = Branch<Pool, HostBranch | VmLeaf, 'pool'>
|
||||
11
@xen-orchestra/web/src/types/tree.type.ts
Normal file
11
@xen-orchestra/web/src/types/tree.type.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { XoHost } from '@/types/xo/host.type'
|
||||
import type { XoPool } from '@/types/xo/pool.type'
|
||||
import type { XoVm } from '@/types/xo/vm.type'
|
||||
import type { Branch } from '@core/composables/tree/branch'
|
||||
import type { Leaf } from '@core/composables/tree/leaf'
|
||||
|
||||
export type VmLeaf = Leaf<XoVm, 'vm'>
|
||||
|
||||
export type HostBranch = Branch<XoHost, VmLeaf, 'host'>
|
||||
|
||||
export type PoolBranch = Branch<XoPool, HostBranch | VmLeaf, 'pool'>
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { Host } from '@/types/host.type'
|
||||
import type { Pool } from '@/types/pool.type'
|
||||
import type { Task } from '@/types/task.type'
|
||||
import type { Vm } from '@/types/vm.type'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
|
||||
declare const __brand: unique symbol
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export type RecordId<Type extends XoObjectType> = string & { [__brand]: `${Type}Id` }
|
||||
|
||||
export type XoObject = Vm | Host | Pool | Task
|
||||
|
||||
export type XoObjectType = XoObject['type']
|
||||
|
||||
export type XoObjectTypeToXoObject<TType extends XoObjectType> = {
|
||||
[TObject in XoObject as TObject['type']]: TObject
|
||||
}[TType]
|
||||
|
||||
export type XoObjectContext<TXoObject extends XoObject> = {
|
||||
records: ComputedRef<TXoObject[]>
|
||||
get: (id: TXoObject['id']) => TXoObject | undefined
|
||||
has: (id: TXoObject['id']) => boolean
|
||||
isFetching: Readonly<Ref<boolean>>
|
||||
isReady: Readonly<Ref<boolean>>
|
||||
lastError: Readonly<Ref<string | undefined>>
|
||||
hasError: ComputedRef<boolean>
|
||||
}
|
||||
22
@xen-orchestra/web/src/types/xo/host.type.ts
Normal file
22
@xen-orchestra/web/src/types/xo/host.type.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { XoPool } from '@/types/xo/pool.type'
|
||||
import type { XoVm } from '@/types/xo/vm.type'
|
||||
import type { Branded } from '@core/types/utility.type'
|
||||
|
||||
export enum HOST_POWER_STATE {
|
||||
HALTED = 'Halted',
|
||||
RUNNING = 'Running',
|
||||
UNKNOWN = 'Unknown',
|
||||
}
|
||||
|
||||
export type XoHost = {
|
||||
id: Branded<'host'>
|
||||
type: 'host'
|
||||
$pool: XoPool['id']
|
||||
_xapiRef: string
|
||||
address: string
|
||||
enabled: boolean
|
||||
name_label: string
|
||||
name_description: string
|
||||
power_state: HOST_POWER_STATE
|
||||
residentVms: XoVm['id'][]
|
||||
}
|
||||
67
@xen-orchestra/web/src/types/xo/index.ts
Normal file
67
@xen-orchestra/web/src/types/xo/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { xoApiDefinition } from '@/utils/xo-api-definition.util'
|
||||
import type { ComputedRef, Ref } from 'vue'
|
||||
|
||||
type XoApiDefinition = typeof xoApiDefinition
|
||||
|
||||
type XoRecordMapping = {
|
||||
[K in keyof XoApiDefinition]: {
|
||||
type: XoApiDefinition[K]['type']
|
||||
definition: XoApiDefinition[K]['handler'] extends (data: any) => infer T ? T : never
|
||||
}
|
||||
}
|
||||
|
||||
export type ApiDefinition = Record<
|
||||
string,
|
||||
{
|
||||
type: 'single' | 'collection'
|
||||
path: string
|
||||
fields: string
|
||||
handler: (data: any) => any
|
||||
}
|
||||
>
|
||||
|
||||
// SINGLE RECORD
|
||||
|
||||
type XoSingleRecordMapping = {
|
||||
[K in keyof XoRecordMapping as XoRecordMapping[K]['type'] extends 'single'
|
||||
? K
|
||||
: never]: XoRecordMapping[K]['definition']
|
||||
}
|
||||
|
||||
export type XoSingleRecordType = keyof XoSingleRecordMapping
|
||||
|
||||
export type XoSingleRecord = XoSingleRecordMapping[XoSingleRecordType]
|
||||
|
||||
export type XoSingleRecordContext<TRecord extends XoSingleRecord> = {
|
||||
record: ComputedRef<TRecord>
|
||||
isFetching: Readonly<Ref<boolean>>
|
||||
isReady: Readonly<Ref<boolean>>
|
||||
lastError: Readonly<Ref<string | undefined>>
|
||||
hasError: ComputedRef<boolean>
|
||||
}
|
||||
|
||||
export type TypeToSingleRecord<TType extends XoSingleRecordType> = XoSingleRecordMapping[TType]
|
||||
|
||||
// COLLECTION RECORD
|
||||
|
||||
type XoCollectionRecordMapping = {
|
||||
[K in keyof XoRecordMapping as XoRecordMapping[K]['type'] extends 'collection'
|
||||
? K
|
||||
: never]: XoRecordMapping[K]['definition']
|
||||
}
|
||||
|
||||
export type XoCollectionRecordType = keyof XoCollectionRecordMapping
|
||||
|
||||
export type XoCollectionRecord = XoCollectionRecordMapping[XoCollectionRecordType]
|
||||
|
||||
export type XoCollectionRecordContext<TRecord extends XoCollectionRecord> = {
|
||||
records: ComputedRef<TRecord[]>
|
||||
get: (id: TRecord['id']) => TRecord | undefined
|
||||
has: (id: TRecord['id']) => boolean
|
||||
isFetching: Readonly<Ref<boolean>>
|
||||
isReady: Readonly<Ref<boolean>>
|
||||
lastError: Readonly<Ref<string | undefined>>
|
||||
hasError: ComputedRef<boolean>
|
||||
}
|
||||
|
||||
export type TypeToCollectionRecord<TType extends XoCollectionRecordType> = XoCollectionRecordMapping[TType]
|
||||
12
@xen-orchestra/web/src/types/xo/pool.type.ts
Normal file
12
@xen-orchestra/web/src/types/xo/pool.type.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { XoHost } from '@/types/xo/host.type'
|
||||
import type { Branded } from '@core/types/utility.type'
|
||||
|
||||
export type XoPool = {
|
||||
id: Branded<'pool'>
|
||||
type: 'pool'
|
||||
$pool: XoPool['id']
|
||||
master: XoHost['id']
|
||||
name_description: string
|
||||
name_label: string
|
||||
_xapiRef: string
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { RecordId } from '@/types/xo-object.type'
|
||||
import type { Branded } from '@core/types/utility.type'
|
||||
|
||||
export type Task = {
|
||||
id: RecordId<'task'>
|
||||
export type XoTask = {
|
||||
id: Branded<'task'>
|
||||
type: 'task'
|
||||
start: number
|
||||
end: number | undefined
|
||||
@@ -10,5 +10,5 @@ export type Task = {
|
||||
name: string
|
||||
}
|
||||
status: 'pending' | 'success' | 'failure' | 'interrupted'
|
||||
tasks?: Task[]
|
||||
tasks?: XoTask[]
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { RecordId } from '@/types/xo-object.type'
|
||||
import type { Leaf } from '@core/composables/tree/leaf'
|
||||
import type { XoHost } from '@/types/xo/host.type'
|
||||
import type { XoPool } from '@/types/xo/pool.type'
|
||||
import type { Branded } from '@core/types/utility.type'
|
||||
|
||||
export enum VM_POWER_STATE {
|
||||
HALTED = 'Halted',
|
||||
@@ -8,11 +9,11 @@ export enum VM_POWER_STATE {
|
||||
SUSPENDED = 'Suspended',
|
||||
}
|
||||
|
||||
export type Vm = {
|
||||
id: RecordId<'VM'>
|
||||
export type XoVm = {
|
||||
id: Branded<'vm'>
|
||||
type: 'VM'
|
||||
$container: RecordId<'host'> | RecordId<'pool'>
|
||||
$pool: RecordId<'pool'>
|
||||
$container: XoPool['id'] | XoHost['id']
|
||||
$pool: XoPool['id']
|
||||
_xapiRef: string
|
||||
name_label: string
|
||||
name_description: string
|
||||
@@ -21,5 +22,3 @@ export type Vm = {
|
||||
mainIpAddress: string
|
||||
other: { disable_pv_vnc: string }
|
||||
}
|
||||
|
||||
export type VmLeaf = Leaf<Vm, 'vm'>
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Task } from '@/types/task.type'
|
||||
import type { XoTask } from '@/types/xo/task.type'
|
||||
import type { Task as CoreTask } from '@core/types/task.type'
|
||||
|
||||
export const convertTaskToCore = (task: Task): CoreTask => ({
|
||||
export const convertTaskToCore = (task: XoTask): CoreTask => ({
|
||||
id: task.id,
|
||||
status: task.status === 'interrupted' ? 'failure' : task.status,
|
||||
name: task.properties.name,
|
||||
|
||||
@@ -1,90 +1,93 @@
|
||||
import type { XoObject, XoObjectContext, XoObjectType, XoObjectTypeToXoObject } from '@/types/xo-object.type'
|
||||
import { restApiConfig } from '@/utils/rest-api-config.util'
|
||||
import type {
|
||||
TypeToCollectionRecord,
|
||||
TypeToSingleRecord,
|
||||
XoCollectionRecord,
|
||||
XoCollectionRecordContext,
|
||||
XoCollectionRecordType,
|
||||
XoSingleRecord,
|
||||
XoSingleRecordContext,
|
||||
XoSingleRecordType,
|
||||
} from '@/types/xo'
|
||||
import { xoApiDefinition } from '@/utils/xo-api-definition.util'
|
||||
import type { SubscribableStoreConfig } from '@core/types/subscribable-store.type'
|
||||
import type { VoidFunction } from '@core/types/utility.type'
|
||||
import { toArray } from '@core/utils/to-array.utils'
|
||||
import { noop, useFetch, useIntervalFn } from '@vueuse/core'
|
||||
import { computed, readonly, ref, shallowReactive } from 'vue'
|
||||
|
||||
type SingleOptions = {
|
||||
pollInterval?: number
|
||||
}
|
||||
|
||||
type CollectionOptions<TRecord extends XoCollectionRecord> = {
|
||||
sortBy?: (a: TRecord, b: TRecord) => number
|
||||
pollInterval?: number
|
||||
}
|
||||
|
||||
export function createXoStoreConfig<
|
||||
TType extends XoObjectType,
|
||||
TXoObjectInput extends XoObjectTypeToXoObject<TType>,
|
||||
TBeforeAdd extends ((input: TXoObjectInput) => undefined | XoObject) | undefined = (
|
||||
input: TXoObjectInput
|
||||
) => TXoObjectInput,
|
||||
TXoObject extends XoObject = TBeforeAdd extends (input: TXoObjectInput) => any
|
||||
? NonNullable<ReturnType<TBeforeAdd>>
|
||||
: TXoObjectInput,
|
||||
>(
|
||||
type: TType,
|
||||
options?: {
|
||||
sortBy?: (a: TXoObject, b: TXoObject) => number
|
||||
beforeAdd?: TBeforeAdd
|
||||
pollInterval?: number
|
||||
}
|
||||
): SubscribableStoreConfig<XoObjectContext<TXoObject>> {
|
||||
const recordsById = shallowReactive(new Map<TXoObject['id'], TXoObject>())
|
||||
const records = computed(() => {
|
||||
const results = Array.from(recordsById.values())
|
||||
TType extends XoSingleRecordType,
|
||||
TRecord extends XoSingleRecord = TypeToSingleRecord<TType>,
|
||||
>(type: TType, options?: SingleOptions): SubscribableStoreConfig<XoSingleRecordContext<TRecord>>
|
||||
|
||||
if (options?.sortBy) {
|
||||
results.sort(options.sortBy)
|
||||
}
|
||||
export function createXoStoreConfig<
|
||||
TType extends XoCollectionRecordType,
|
||||
TRecord extends XoCollectionRecord = TypeToCollectionRecord<TType>,
|
||||
>(type: TType, options?: CollectionOptions<TRecord>): SubscribableStoreConfig<XoCollectionRecordContext<TRecord>>
|
||||
|
||||
return results
|
||||
})
|
||||
export function createXoStoreConfig(
|
||||
type: XoSingleRecordType | XoCollectionRecordType,
|
||||
options?: any
|
||||
): SubscribableStoreConfig<any> {
|
||||
const singleRecordId = Symbol('singleRecordId')
|
||||
|
||||
const get = (id: TXoObject['id']) => recordsById.get(id)
|
||||
const apiDefinition = xoApiDefinition[type]
|
||||
|
||||
const has = (id: TXoObject['id']) => recordsById.has(id)
|
||||
const isCollection = apiDefinition.type === 'collection'
|
||||
|
||||
const recordsById = shallowReactive(new Map())
|
||||
|
||||
const urlParams = new URLSearchParams(`fields=${apiDefinition.fields}`)
|
||||
|
||||
const url = `/rest/v0/${apiDefinition.path}?${urlParams.toString()}`
|
||||
|
||||
const isReady = ref(false)
|
||||
|
||||
const urlParams = new URLSearchParams(`fields=${restApiConfig[type].fields}`)
|
||||
const { isFetching, error, execute, canAbort, abort, data } = useFetch(url, {
|
||||
immediate: false,
|
||||
beforeFetch({ options }) {
|
||||
options.credentials = 'include'
|
||||
|
||||
const { isFetching, error, execute, canAbort, abort, data } = useFetch(
|
||||
`/rest/v0/${restApiConfig[type].path}?${urlParams.toString()}`,
|
||||
{
|
||||
immediate: false,
|
||||
beforeFetch({ options }) {
|
||||
options.credentials = 'include'
|
||||
|
||||
return { options }
|
||||
},
|
||||
}
|
||||
)
|
||||
return { options }
|
||||
},
|
||||
})
|
||||
.get()
|
||||
.json<TXoObjectInput[]>()
|
||||
.json()
|
||||
|
||||
const hasError = computed(() => !!error.value)
|
||||
|
||||
const handleRecordAdded = (record: TXoObjectInput) => {
|
||||
const recordToAdd = options?.beforeAdd ? options.beforeAdd(record) : record
|
||||
|
||||
if (recordToAdd === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
recordsById.set(record.id, recordToAdd as TXoObject)
|
||||
}
|
||||
|
||||
const handleRecordsLoaded = (records: TXoObjectInput[] | null) => {
|
||||
recordsById.clear()
|
||||
|
||||
if (!records) {
|
||||
return
|
||||
}
|
||||
|
||||
records.forEach(record => handleRecordAdded(record))
|
||||
isReady.value = true
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
await execute()
|
||||
|
||||
handleRecordsLoaded(data.value)
|
||||
recordsById.clear()
|
||||
|
||||
if (!data.value) {
|
||||
return
|
||||
}
|
||||
toArray(data.value).forEach(item => {
|
||||
const recordToAdd = apiDefinition.handler(item) as any
|
||||
|
||||
if (recordToAdd === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
recordsById.set(isCollection ? recordToAdd.id : singleRecordId, recordToAdd)
|
||||
})
|
||||
|
||||
isReady.value = true
|
||||
}
|
||||
|
||||
let startSubscription: VoidFunction = () => loadData()
|
||||
|
||||
let stopSubscription: VoidFunction = noop
|
||||
|
||||
if (options?.pollInterval !== undefined) {
|
||||
@@ -109,20 +112,38 @@ export function createXoStoreConfig<
|
||||
isReady.value = false
|
||||
}
|
||||
|
||||
const context = {
|
||||
records,
|
||||
get,
|
||||
has,
|
||||
isFetching: readonly(isFetching),
|
||||
isReady: readonly(isReady),
|
||||
lastError: readonly(error),
|
||||
hasError,
|
||||
} as XoObjectContext<TXoObject>
|
||||
|
||||
return {
|
||||
context,
|
||||
context: {
|
||||
...createRecordContext(isCollection, recordsById, singleRecordId, options),
|
||||
isFetching: readonly(isFetching),
|
||||
isReady: readonly(isReady),
|
||||
lastError: readonly(error),
|
||||
hasError,
|
||||
},
|
||||
onSubscribe,
|
||||
onUnsubscribe,
|
||||
isEnabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
function createRecordContext(isCollection: boolean, recordsById: Map<any, any>, singleRecordId: any, options: any) {
|
||||
return isCollection
|
||||
? {
|
||||
records: computed(() => {
|
||||
const results = Array.from(recordsById.values())
|
||||
|
||||
const sortBy = (options as CollectionOptions<any>)?.sortBy
|
||||
|
||||
if (sortBy) {
|
||||
results.sort(sortBy)
|
||||
}
|
||||
|
||||
return results
|
||||
}),
|
||||
get: (id: any) => recordsById.get(id),
|
||||
has: (id: any) => recordsById.has(id),
|
||||
}
|
||||
: {
|
||||
record: computed(() => recordsById.get(singleRecordId)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { XoObjectType } from '@/types/xo-object.type'
|
||||
|
||||
export const restApiConfig: Record<XoObjectType, { path: string; fields: string }> = {
|
||||
VM: {
|
||||
path: 'vms',
|
||||
fields: 'id,name_label,name_description,power_state,$container,$pool,other',
|
||||
},
|
||||
host: {
|
||||
path: 'hosts',
|
||||
fields: 'id,name_label,name_description,power_state,residentVms,$pool',
|
||||
},
|
||||
pool: {
|
||||
path: 'pools',
|
||||
fields: 'id,name_label,master',
|
||||
},
|
||||
task: {
|
||||
path: 'tasks',
|
||||
fields: 'id,start,end,properties,status,progress,tasks',
|
||||
},
|
||||
}
|
||||
32
@xen-orchestra/web/src/utils/xo-api-definition.util.ts
Normal file
32
@xen-orchestra/web/src/utils/xo-api-definition.util.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { ApiDefinition } from '@/types/xo'
|
||||
import type { XoHost } from '@/types/xo/host.type'
|
||||
import type { XoPool } from '@/types/xo/pool.type'
|
||||
import type { XoTask } from '@/types/xo/task.type'
|
||||
import type { XoVm } from '@/types/xo/vm.type'
|
||||
|
||||
export const xoApiDefinition = {
|
||||
pool: {
|
||||
type: 'collection',
|
||||
path: 'pools',
|
||||
fields: 'id,name_label,master',
|
||||
handler: (record: XoPool) => record,
|
||||
},
|
||||
host: {
|
||||
type: 'collection',
|
||||
path: 'hosts',
|
||||
fields: 'id,name_label,name_description,power_state,residentVms,$pool',
|
||||
handler: (record: XoHost) => record,
|
||||
},
|
||||
vm: {
|
||||
type: 'collection',
|
||||
path: 'vms',
|
||||
fields: 'id,name_label,name_description,power_state,$container,$pool,other',
|
||||
handler: (record: XoVm) => record,
|
||||
},
|
||||
task: {
|
||||
type: 'collection',
|
||||
path: 'tasks',
|
||||
fields: 'id,start,end,properties,status,progress,tasks',
|
||||
handler: (record: XoTask) => record,
|
||||
},
|
||||
} satisfies ApiDefinition
|
||||
Reference in New Issue
Block a user