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:
Thierry Goettelmann
2024-08-16 15:03:49 +02:00
committed by GitHub
parent 79ce97b3fd
commit f78a140fbb
33 changed files with 331 additions and 237 deletions

View 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, {})
```

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'

View File

@@ -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, {

View File

@@ -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'

View File

@@ -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[]

View File

@@ -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'

View File

@@ -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<{

View File

@@ -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'

View File

@@ -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[]

View File

@@ -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>

View File

@@ -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()

View File

@@ -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>

View File

@@ -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()

View File

@@ -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()

View File

@@ -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>

View File

@@ -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')))

View File

@@ -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

View File

@@ -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
}

View File

@@ -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'>

View File

@@ -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'>

View 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'>

View File

@@ -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>
}

View 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'][]
}

View 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]

View 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
}

View File

@@ -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[]
}

View File

@@ -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'>

View File

@@ -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,

View File

@@ -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)),
}
}

View File

@@ -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',
},
}

View 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