feat(web): introducing defineRemoteResource (Web Core) and useXoCollectionState (XO 6) to handle XO API resources (#8855)

This commit is contained in:
Thierry Goettelmann
2025-08-13 10:28:40 +02:00
committed by GitHub
parent c1c88c6797
commit e0b9bc2004
106 changed files with 1665 additions and 1327 deletions

View File

@@ -0,0 +1,115 @@
# `defineRemoteResource` documentation
This utility will create a function that can be used to fetch data from a remote resource (i.e., an API endpoint)
When called, a subscription to the resource is registered.
If multiple Components call that same function, a subscription will be registered for each of them.
They will share the same data and state, which means that the data will be fetched only once and shared across all Components.
The fetch will start automatically when at least one Component is using the resource, then, when the last Component releases the resource, the polling will stop.
The data is cached for a certain duration, so if a Component remounts in the frame time, the data will be retrieved from the cache to be displayed immediately, while the data is being fetched again in the background.
## Basic usage
```typescript
const useMyResource = defineRemoteResource({
url: '/api/path/to/resource',
})
```
By default, the initial data (`TData`) will be `undefined`, the returned state will be `{ data: Ref<TData> }` and when new data is received, it will just replace the previous one.
```typescript
const { data } = useMyResource()
```
## Initial data, state and received data handling
The initial data, the state and what happen when data is retrieved can be customized by passing the `initialData`, `state` and `onDataReceived` properties:
```typescript
const useMyResource = defineRemoteResource({
url: '/api/path/to/resource',
initialData: () => {} as Partial<MyResource>,
state: (data) => {
return {
myResource: data,
customProp: computed(() => data.foobar)
}
},
onDataReceived: (currentData, receivedData) => {
deepMerge(currentData.value, receivedData)
}
}
const { myResource, customProp } = useMyResource()
```
## Context
The context is available in the `$context` property of the returned state.
It contains the following properties:
| Property | Type | Description |
| ------------- | --------------------------------- | ---------------------------------------------------------------------------------- |
| `scope` | `EffectScope` | The Vue effect scope used to be shared across the resource and its dependencies |
| `args` | `any[]` | The arguments passed to the `useMyResource` function |
| `isReady` | `ComputedRef<boolean>` | Indicates if the resource is ready (i.e., the data has been fetched at least once) |
| `hasError` | `ComputedRef<boolean>` | Indicates if an error occurred during the last fetch |
| `lastError` | `ComputedRef<Error \| undefined>` | The last error that occurred during the fetch, if any |
| `isEnabled` | `Ref<boolean>` | Whether the resource is enabled (execute the request) or not |
| `enable` | `() => void` | Function to manually enable the resource |
| `disable` | `() => void` | Function to manually disable the resource |
| `forceReload` | `() => void` | Function to manually reload the resource |
```typescript
const { myResource, $context } = useMyResource()
```
It's also passed as the second argument of the `state` builder and can be used to extend the resource state.
```typescript
const useMyResource = defineRemoteResource({
url: '/api/path/to/resource',
state: (data, context) => ({
myResource: data,
isMyResourceReady: context.isReady,
}),
})
const { myResource, isMyResourceReady } = useMyResource()
```
## Dynamic URLs
You can define a dynamic URL by using a function that returns the URL string.
```typescript
const useMyResource = defineRemoteResource({
url: (id: string) => `/api/path/to/resource/${id}`,
})
const { id } = defineProps<{
id: string
}>()
const { data } = useMyResource(() => id)
```
## Polling and caching
By default, the resource will poll for updates every 30 seconds and cache the result for 10 seconds.
This can be customized by passing the `pollingIntervalMs` and `cacheDurationMs` properties:
```typescript
const useMyResource = defineRemoteResource({
url: '/api/path/to/resource',
pollingIntervalMs: 60_000, // Poll every 60 seconds
cacheDurationMs: 5 * 60_000, // Cache for 5 minutes
})
```

View File

@@ -0,0 +1,294 @@
import type { ResourceContext, UseRemoteResource } from '@core/packages/remote-resource/types.ts'
import type { VoidFunction } from '@core/types/utility.type.ts'
import { ifElse } from '@core/utils/if-else.utils.ts'
import { type MaybeRef, noop, useTimeoutPoll } from '@vueuse/core'
import { merge } from 'lodash-es'
import readNDJSONStream from 'ndjson-readablestream'
import {
computed,
type ComputedRef,
type EffectScope,
getCurrentScope,
type MaybeRefOrGetter,
onScopeDispose,
reactive,
type Ref,
ref,
toRef,
toValue,
watch,
} from 'vue'
const DEFAULT_CACHE_DURATION_MS = 10_000
const DEFAULT_POLLING_INTERVAL_MS = 30_000
export function defineRemoteResource<
TData,
TState extends object = { data: Ref<TData> },
TArgs extends any[] = [],
>(config: {
url: string | ((...args: TArgs) => string)
initialData: () => TData
state?: (data: Ref<NoInfer<TData>>, context: ResourceContext<TArgs>) => TState
onDataReceived?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
cacheDurationMs?: number
pollingIntervalMs?: number
stream?: boolean
}): UseRemoteResource<TState, TArgs>
export function defineRemoteResource<TData, TState extends object, TArgs extends any[] = []>(config: {
url: string | ((...args: TArgs) => string)
state?: (data: Ref<TData | undefined>, context: ResourceContext<TArgs>) => TState
onDataReceived?: (data: Ref<TData | undefined>, receivedData: any) => void
cacheDurationMs?: number
pollingIntervalMs?: number
stream?: boolean
}): UseRemoteResource<TState, TArgs>
export function defineRemoteResource<
TData,
TState extends object = { data: Ref<TData> },
TArgs extends any[] = [],
>(config: {
url: string | ((...args: TArgs) => string)
initialData?: () => TData
state?: (data: Ref<TData>, context: ResourceContext<TArgs>) => TState
onDataReceived?: (data: Ref<NoInfer<TData>>, receivedData: any) => void
cacheDurationMs?: number
pollingIntervalMs?: number
stream?: boolean
}) {
const cache = new Map<
string,
{
count: number
pause: VoidFunction
resume: VoidFunction
state: object
isReady: Ref<boolean>
isFetching: Ref<boolean>
lastError: Ref<Error | undefined>
hasError: ComputedRef<boolean>
}
>()
const buildUrl = typeof config.url === 'string' ? () => config.url as string : config.url
const buildData = config.initialData ?? (() => undefined as TData | undefined)
const buildState = config.state ?? ((data: Ref<TData>) => ({ data }))
const cacheDuration = config.cacheDurationMs ?? DEFAULT_CACHE_DURATION_MS
const pollingInterval = config.pollingIntervalMs ?? DEFAULT_POLLING_INTERVAL_MS
const onDataReceived =
config.onDataReceived ??
((data: Ref<TData>, receivedData: any) => {
if (!config.stream || data.value === undefined) {
data.value = receivedData
return
}
if (Array.isArray(data.value) && Array.isArray(receivedData)) {
data.value.push(...receivedData)
return
}
merge(data.value, receivedData)
})
function subscribeToUrl(url: string) {
const entry = cache.get(url)
if (!entry) {
return
}
entry.count += 1
if (entry.count === 1) {
entry.resume()
}
}
function unsubscribeFromUrl(url: string) {
const entry = cache.get(url)
if (!entry) {
return
}
entry.count -= 1
if (entry.count > 0) {
return
}
entry.pause()
setTimeout(() => {
cache.delete(url)
}, cacheDuration)
}
function registerUrl(url: string, context: ResourceContext<TArgs>) {
if (cache.has(url)) {
return
}
const isReady = ref(false)
const isFetching = ref(false)
const lastError = ref<Error>()
const hasError = computed(() => lastError.value !== undefined)
const data = ref(buildData()) as Ref<TData>
async function execute() {
try {
isFetching.value = true
const response = await fetch(url)
if (!response.ok) {
lastError.value = Error(`Failed to fetch: ${response.statusText}`)
return
}
if (!response.body) {
return
}
if (config.stream) {
for await (const event of readNDJSONStream(response.body)) {
onDataReceived(data, event)
}
} else {
onDataReceived(data, await response.json())
}
isReady.value = true
} catch (error) {
lastError.value = error instanceof Error ? error : new Error(String(error))
} finally {
isFetching.value = false
}
}
let pause: VoidFunction = noop
let resume: VoidFunction = execute
if (pollingInterval > 0) {
const timeoutPoll = useTimeoutPoll(execute, pollingInterval, {
immediateCallback: true,
immediate: false,
})
pause = timeoutPoll.pause
resume = timeoutPoll.resume
}
const state = buildState(data, context)
cache.set(url, {
count: 0,
pause,
resume,
state,
isReady,
isFetching,
lastError,
hasError,
})
}
function initializeUrl(url: ComputedRef<string>, context: ResourceContext<TArgs>) {
watch(
url,
(toUrl, fromUrl) => {
registerUrl(toUrl, context)
if (context.isEnabled.value) {
subscribeToUrl(toUrl)
}
if (fromUrl) {
unsubscribeFromUrl(fromUrl)
}
},
{ immediate: true }
)
}
return function useRemoteResource(
optionsOrParentContext?: { isEnabled?: MaybeRef<boolean>; scope?: EffectScope },
...args: { [K in keyof TArgs]: MaybeRefOrGetter<TArgs[K]> }
) {
const scope = optionsOrParentContext?.scope ?? getCurrentScope()
if (!scope) {
throw new Error('No effect scope found. Please provide a scope or use this function within a Vue component.')
}
const isEnabled = toRef(optionsOrParentContext?.isEnabled ?? true)
return scope.run(() => {
const url = computed(() => buildUrl(...(args.map(arg => toValue(arg)) as TArgs)))
onScopeDispose(() => {
unsubscribeFromUrl(url.value)
})
const context: ResourceContext<TArgs> = {
scope,
args,
isReady: computed(() => cache.get(url.value)?.isReady.value ?? false),
isFetching: computed(() => cache.get(url.value)?.isFetching?.value ?? false),
lastError: computed(() => cache.get(url.value)?.lastError.value),
hasError: computed(() => cache.get(url.value)?.hasError.value ?? false),
isEnabled,
enable: () => {
isEnabled.value = true
},
disable: () => {
isEnabled.value = false
},
forceReload: () => {
cache.get(url.value)?.pause()
cache.get(url.value)?.resume()
},
}
initializeUrl(url, context)
const state = reactive({} as TState)
ifElse(
isEnabled,
() => subscribeToUrl(url.value),
() => unsubscribeFromUrl(url.value)
)
watch(
url,
() => {
Object.assign(state, cache.get(url.value)!.state)
},
{ immediate: true }
)
return {
...Object.fromEntries(
Object.entries(state).map(([key, value]) =>
typeof value === 'function' ? [key, value] : [key, toRef(state, key as any)]
)
),
$context: context,
}
})!
}
}

View File

@@ -0,0 +1,28 @@
import type { MaybeRef } from '@vueuse/core'
import type { MaybeRefOrGetter } from '@vueuse/shared'
import type { ComputedRef, EffectScope, Ref, ToRef } from 'vue'
export type ResourceContext<TArgs extends any[]> = {
scope: EffectScope
args: { [K in keyof TArgs]: MaybeRefOrGetter<TArgs[K]> }
isReady: ComputedRef<boolean>
isFetching: ComputedRef<boolean>
hasError: ComputedRef<boolean>
lastError: ComputedRef<Error | undefined>
isEnabled: Ref<boolean>
enable: () => void
disable: () => void
forceReload: () => void
}
export type UseRemoteResource<TState, TArgs extends any[]> = (
optionsOrParentContext?: {
isEnabled?: MaybeRef<boolean>
scope?: EffectScope
},
...args: { [K in keyof TArgs]: MaybeRefOrGetter<TArgs[K]> }
) => {
[K in keyof TState]: TState[K] extends (...args: any[]) => any ? TState[K] : ToRef<TState[K]>
} & {
$context: ResourceContext<TArgs>
}

View File

@@ -26,7 +26,7 @@
"human-format": "^1.2.1",
"iterable-backoff": "^0.1.0",
"lodash-es": "^4.17.21",
"ndjson-readablestream": "^1.2.0",
"ndjson-readablestream": "^1.3.0",
"placement.js": "^1.0.0-beta.5",
"simple-icons": "^14.14.0",
"vue-echarts": "^6.6.8"

View File

@@ -36,7 +36,7 @@
"d3-time-format": "^4.1.0",
"human-format": "^1.2.1",
"lodash-es": "^4.17.21",
"ndjson-readablestream": "^1.2.0",
"ndjson-readablestream": "^1.3.0",
"npm-run-all2": "^7.0.2",
"pinia": "^3.0.1",
"postcss": "^8.5.3",

View File

@@ -42,7 +42,7 @@
</template>
<script lang="ts" setup>
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import type { XoHost } from '@/types/xo/host.type'
import type { HostState } from '@core/types/object-icon.type'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
@@ -61,6 +61,7 @@ const { host } = defineProps<{
const { t } = useI18n()
const { isMasterHost } = useHostStore().subscribe()
const { isMasterHost } = useXoHostCollection()
const isMaster = computed(() => isMasterHost(host.id))
</script>

View File

@@ -13,8 +13,8 @@
</template>
<script lang="ts" setup>
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import { useXoVmCollection } from '@/remote-resources/use-xo-vm-collection.ts'
import type { XoHost } from '@/types/xo/host.type'
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
@@ -32,10 +32,11 @@ const { host } = defineProps<{
const { t } = useI18n()
const { isReady: isHostReady } = useHostStore().subscribe()
const { vmsByHost, isReady: isVmReady } = useVmStore().subscribe()
const { areHostsReady } = useXoHostCollection()
const isReady = logicAnd(isHostReady, isVmReady)
const { vmsByHost, areVmsReady } = useXoVmCollection()
const isReady = logicAnd(areHostsReady, areVmsReady)
const hostVms = computed(() => vmsByHost.value.get(host.id) ?? [])

View File

@@ -1,5 +1,5 @@
<template>
<VtsQuickInfoCard class="host-dashboard-quick-info" :loading="!isReady">
<VtsQuickInfoCard class="host-dashboard-quick-info" :loading="!areHostsReady">
<VtsQuickInfoColumn>
<VtsQuickInfoRow :label="t('state')">
<template #value>
@@ -50,7 +50,7 @@
</template>
<script lang="ts" setup>
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import { HOST_POWER_STATE, type XoHost } from '@/types/xo/host.type'
import VtsIcon, { type IconAccent } from '@core/components/icon/VtsIcon.vue'
import VtsQuickInfoCard from '@core/components/quick-info-card/VtsQuickInfoCard.vue'
@@ -75,7 +75,7 @@ const { host } = defineProps<{
const { t } = useI18n()
const { getMasterHostByPoolId, isMasterHost, isReady } = useHostStore().subscribe()
const { getMasterHostByPoolId, isMasterHost, areHostsReady } = useXoHostCollection()
const powerStateConfig: Record<
string,

View File

@@ -1,7 +1,7 @@
<template>
<UiCard class="host-dashboard-ram-provisioning">
<UiCardTitle>{{ t('ram-provisioning') }}</UiCardTitle>
<VtsLoadingHero v-if="!isReady" type="card" />
<VtsLoadingHero v-if="!areHostsReady" type="card" />
<template v-else>
<UiProgressBar
display-mode="percent"
@@ -28,7 +28,7 @@
</template>
<script lang="ts" setup>
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import type { XoHost } from '@/types/xo/host.type'
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
@@ -45,7 +45,7 @@ const { host } = defineProps<{
const { t } = useI18n()
const { isReady } = useHostStore().subscribe()
const { areHostsReady } = useXoHostCollection()
const ramUsage = computed(() => {
const total = host.memory.size

View File

@@ -10,8 +10,8 @@
</template>
<script lang="ts" setup>
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import { useXoVmCollection } from '@/remote-resources/use-xo-vm-collection.ts'
import type { XoHost } from '@/types/xo/host.type'
import { VM_POWER_STATE } from '@/types/xo/vm.type'
import VtsDonutChartWithLegend, {
@@ -30,10 +30,10 @@ const { host } = defineProps<{
host: XoHost
}>()
const { isReady: isHostReady } = useHostStore().subscribe()
const { vmsByHost, isReady: isVmReady } = useVmStore().subscribe()
const { areHostsReady } = useXoHostCollection()
const { vmsByHost, areVmsReady } = useXoVmCollection()
const isReady = logicAnd(isHostReady, isVmReady)
const isReady = logicAnd(areHostsReady, areVmsReady)
const hostVms = computed(() => vmsByHost.value.get(host.id) ?? [])

View File

@@ -299,8 +299,8 @@
</template>
<script setup lang="ts">
import { useNetworkStore } from '@/stores/xo-rest-api/network.store.ts'
import { usePifStore } from '@/stores/xo-rest-api/pif.store.ts'
import { useXoNetworkCollection } from '@/remote-resources/use-xo-network-collection.ts'
import { useXoPifCollection } from '@/remote-resources/use-xo-pif-collection.ts'
import type { XoPif } from '@/types/xo/pif.type.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsConnectionStatus from '@core/components/connection-status/VtsConnectionStatus.vue'
@@ -328,15 +328,15 @@ const emit = defineEmits<{
close: []
}>()
const { get: getNetwork } = useNetworkStore().subscribe()
const { getBondsDevices } = usePifStore().subscribe()
const { useGetNetworkById } = useXoNetworkCollection()
const { getBondsDevices } = useXoPifCollection()
const uiStore = useUiStore()
const { t } = useI18n()
const ipAddresses = computed(() => [pif.ip, ...pif.ipv6].filter(ip => ip))
const network = computed(() => getNetwork(pif.$network))
const network = useGetNetworkById(() => pif.$network)
const networkNbd = computed(() => (network.value?.nbd ? t('on') : t('off')))

View File

@@ -41,10 +41,14 @@
</UiButton>
</UiTableActions>
<UiTopBottomTable :selected-items="0" :total-items="0">
<UiTablePagination v-if="isReady" v-bind="paginationBindings" />
<UiTablePagination v-if="arePifsReady" v-bind="paginationBindings" />
</UiTopBottomTable>
</div>
<VtsDataTable :is-ready :has-error :no-data-message="pifs.length === 0 ? t('no-pif-detected') : undefined">
<VtsDataTable
:is-ready="arePifsReady"
:has-error="hasPifFetchError"
:no-data-message="pifs.length === 0 ? t('no-pif-detected') : undefined"
>
<template #thead>
<tr>
<template v-for="column of visibleColumns" :key="column.id">
@@ -127,16 +131,17 @@
<div>{{ t('no-result') }}</div>
</VtsStateHero>
<UiTopBottomTable :selected-items="0" :total-items="0">
<UiTablePagination v-if="isReady" v-bind="paginationBindings" />
<UiTablePagination v-if="arePifsReady" v-bind="paginationBindings" />
</UiTopBottomTable>
</div>
</div>
</template>
<script setup lang="ts">
import { useNetworkStore } from '@/stores/xo-rest-api/network.store.ts'
import { usePifStore } from '@/stores/xo-rest-api/pif.store.ts'
import { useXoNetworkCollection } from '@/remote-resources/use-xo-network-collection.ts'
import { useXoPifCollection } from '@/remote-resources/use-xo-pif-collection.ts'
import type { XoPif } from '@/types/xo/pif.type.ts'
import { getPifStatus } from '@/utils/xo-records/pif.util.ts'
import VtsConnectionStatus from '@core/components/connection-status/VtsConnectionStatus.vue'
import VtsDataTable from '@core/components/data-table/VtsDataTable.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
@@ -175,8 +180,8 @@ const { pifs } = defineProps<{
pifs: XoPif[]
}>()
const { isReady, hasError, getPifStatus } = usePifStore().subscribe()
const { get } = useNetworkStore().subscribe()
const { arePifsReady, hasPifFetchError } = useXoPifCollection()
const { getNetworkById } = useXoNetworkCollection()
const { t } = useI18n()
const selectedPifId = useRouteQuery('id')
@@ -196,7 +201,7 @@ const pifsIds = computed(() => pifs.map(pif => pif.id))
const { selected, areAllSelected } = useMultiSelect(pifsIds)
const getNetworkName = (pif: XoPif) => get(pif.$network)?.name_label ?? ''
const getNetworkName = (pif: XoPif) => getNetworkById(pif.$network)?.name_label ?? ''
const getVlanData = (vlan: number) => (vlan !== -1 ? vlan : t('none'))

View File

@@ -52,8 +52,8 @@
</template>
<script setup lang="ts">
import { useHostStore } from '@/stores/xo-rest-api/host.store.ts'
import { usePoolStore } from '@/stores/xo-rest-api/pool.store.ts'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import { useXoPoolCollection } from '@/remote-resources/use-xo-pool-collection.ts'
import { HOST_POWER_STATE, type XoHost } from '@/types/xo/host.type.ts'
import VtsEnabledState from '@core/components/enabled-state/VtsEnabledState.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
@@ -75,10 +75,11 @@ const { host } = defineProps<{
const { t } = useI18n()
const { get: getPoolById } = usePoolStore().subscribe()
const { getMasterHostByPoolId, isMasterHost } = useHostStore().subscribe()
const { useGetPoolById } = useXoPoolCollection()
const pool = computed(() => getPoolById(host.$pool))
const { getMasterHostByPoolId, isMasterHost } = useXoHostCollection()
const pool = useGetPoolById(() => host.$pool)
const isMaster = computed(() => isMasterHost(host.id))

View File

@@ -27,8 +27,8 @@
</template>
<script setup lang="ts">
import { usePciStore } from '@/stores/xo-rest-api/pci.store'
import { usePgpuStore } from '@/stores/xo-rest-api/pgpu.store'
import { useXoPciCollection } from '@/remote-resources/use-xo-pci-collection.ts'
import { useXoPgpuCollection } from '@/remote-resources/use-xo-pgpu-collection.ts'
import type { XoHost } from '@/types/xo/host.type.ts'
import VtsQuickInfoRow from '@core/components/quick-info-row/VtsQuickInfoRow.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
@@ -43,21 +43,21 @@ const { host } = defineProps<{
const { t } = useI18n()
const { get: getPci, isReady: isPciReady } = usePciStore().subscribe()
const { get: getGpu, isReady: isPgpuReady } = usePgpuStore().subscribe()
const { getPciById, arePcisReady } = useXoPciCollection()
const { getPgpuById, arePgpusReady } = useXoPgpuCollection()
const isReady = logicAnd(isPgpuReady, isPciReady)
const isReady = logicAnd(arePgpusReady, arePcisReady)
const devicesNames = useArrayReduce(
() => host.PGPUs ?? [],
(acc, pGpuId) => {
const pciId = getGpu(pGpuId)?.pci
const pciId = getPgpuById(pGpuId)?.pci
if (!pciId) {
return acc
}
const deviceName = getPci(pciId)?.device_name
const deviceName = getPciById(pciId)?.device_name
if (!deviceName) {
return acc

View File

@@ -11,7 +11,7 @@
</template>
<script setup lang="ts">
import { useVmControllerStore } from '@/stores/xo-rest-api/vm-controller.store.ts'
import { useXoVmControllerCollection } from '@/remote-resources/use-xo-vm-controller-collection.ts'
import type { XoHost } from '@/types/xo/host.type.ts'
import VtsQuickInfoRow from '@core/components/quick-info-row/VtsQuickInfoRow.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
@@ -26,14 +26,14 @@ const { host } = defineProps<{
const { t } = useI18n()
const { get: getController } = useVmControllerStore().subscribe()
const { getVmControllerById } = useXoVmControllerCollection()
const controllerMemory = computed(() => {
if (host.controlDomain === undefined) {
return
}
const controllerDomain = getController(host.controlDomain)
const controllerDomain = getVmControllerById(host.controlDomain)
if (controllerDomain === undefined) {
return

View File

@@ -22,10 +22,10 @@
</template>
<script setup lang="ts">
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { usePifStore } from '@/stores/xo-rest-api/pif.store'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import { HOST_POWER_STATE } from '@/types/xo/host.type'
import type { XoPif } from '@/types/xo/pif.type'
import { getPifStatus } from '@/utils/xo-records/pif.util.ts'
import VtsConnectionStatus from '@core/components/connection-status/VtsConnectionStatus.vue'
import UiButtonIcon from '@core/components/ui/button-icon/UiButtonIcon.vue'
import UiObjectIcon from '@core/components/ui/object-icon/UiObjectIcon.vue'
@@ -41,15 +41,14 @@ const { pif } = defineProps<{
const { t } = useI18n()
const { get } = useHostStore().subscribe()
const { getPifStatus } = usePifStore().subscribe()
const { getHostById } = useXoHostCollection()
const router = useRouter()
const status = computed(() => getPifStatus(pif))
const pifHost = computed(() => {
const host = get(pif.$host)
const host = getHostById(pif.$host)
if (!host) {
return

View File

@@ -30,7 +30,7 @@
</template>
<script setup lang="ts">
import { useServerStore } from '@/stores/xo-rest-api/server.store.ts'
import { useXoServerCollection } from '@/remote-resources/use-xo-server-collection.ts'
import type { XoServer } from '@/types/xo/server.type.ts'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
import UiAlert from '@core/components/ui/alert/UiAlert.vue'
@@ -38,7 +38,6 @@ import UiButton from '@core/components/ui/button/UiButton.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import { useUiStore } from '@core/stores/ui.store.ts'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { serverId } = defineProps<{
@@ -50,10 +49,10 @@ const emit = defineEmits<{
}>()
const { t } = useI18n()
const { get: getServer } = useServerStore().subscribe()
const { useGetServerById } = useXoServerCollection()
const uiStore = useUiStore()
const server = computed(() => getServer(serverId))
const server = useGetServerById(() => serverId)
</script>
<style lang="postcss" scoped>

View File

@@ -6,63 +6,59 @@
</div>
</template>
<script setup lang="ts">
import { useHostStore } from '@/stores/xo-rest-api/host.store.ts'
import { useSrStore } from '@/stores/xo-rest-api/sr.store.ts'
import { useVmControllerStore } from '@/stores/xo-rest-api/vm-controller.store.ts'
import { useVmStore } from '@/stores/xo-rest-api/vm.store.ts'
<script setup lang="ts" generic="TRecord extends XapiXoRecord">
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import { useXoSrCollection } from '@/remote-resources/use-xo-sr-collection.ts'
import { useXoVmCollection } from '@/remote-resources/use-xo-vm-collection.ts'
import { useXoVmControllerCollection } from '@/remote-resources/use-xo-vm-controller-collection.ts'
import type { XoHost } from '@/types/xo/host.type.ts'
import type { XoSr } from '@/types/xo/sr.type.ts'
import type { XoVmController } from '@/types/xo/vm-controller.type.ts'
import type { XoVm } from '@/types/xo/vm.type.ts'
import UiLink from '@core/components/ui/link/UiLink.vue'
import { faDatabase, faDesktop, faServer } from '@fortawesome/free-solid-svg-icons'
import type { XapiXoRecord } from '@vates/types'
import { computed } from 'vue'
const { type, uuid } = defineProps<{
type: XapiXoRecord['type'] | 'unknown'
uuid: XapiXoRecord['uuid']
type: TRecord['type'] | 'unknown'
uuid: TRecord['uuid']
}>()
const { records: hostRecords } = useHostStore().subscribe()
const { records: vmRecords } = useVmStore().subscribe()
const { records: vmControllerRecords } = useVmControllerStore().subscribe()
const { records: srRecords } = useSrStore().subscribe()
const { getHostById } = useXoHostCollection()
const { getVmById } = useXoVmCollection()
const { getVmControllerById } = useXoVmControllerCollection()
const { getSrById } = useXoSrCollection()
const record = computed(() => {
if (type === 'VM') {
return vmRecords.value.find(vm => vm.id === uuid)
switch (type) {
case 'VM':
return getVmById(uuid as XoVm['id'])
case 'host':
return getHostById(uuid as XoHost['id'])
case 'VM-controller':
return getVmControllerById(uuid as XoVmController['id'])
case 'SR':
return getSrById(uuid as XoSr['id'])
default:
return undefined
}
if (type === 'host') {
return hostRecords.value.find(host => host.id === uuid)
}
if (type === 'VM-controller') {
return vmControllerRecords.value.find(vmController => vmController.id === uuid)
}
if (type === 'SR') {
return srRecords.value.find(sr => sr.id === uuid)
}
return undefined
})
const nameLabel = computed(() => record.value?.name_label ?? uuid)
const icon = computed(() => {
if (type === 'VM' || type === 'VM-controller') {
return faDesktop
switch (type) {
case 'VM':
case 'VM-controller':
return faDesktop
case 'host':
return faServer
case 'SR':
return faDatabase
default:
return undefined
}
if (type === 'host') {
return faServer
}
if (type === 'SR') {
return faDatabase
}
return undefined
})
const route = computed(() => {

View File

@@ -3,14 +3,14 @@
<UiCardTitle>
{{ t('alarms') }}
<UiCounter
v-if="alarms.length !== 0 && areAlarmsReady"
v-if="alarms.length !== 0 && isReady"
accent="danger"
size="small"
variant="primary"
:value="alarms.length"
/>
</UiCardTitle>
<VtsLoadingHero v-if="!areAlarmsReady" type="card" />
<VtsLoadingHero v-if="!isReady" type="card" />
<VtsAllGoodHero v-else-if="alarms.length === 0" type="card" />
<div v-else class="alarm-list-container">
<UiAlarmList>
@@ -33,12 +33,11 @@
<script setup lang="ts">
import AlarmLink from '@/components/pool/dashboard/alarms/AlarmLink.vue'
import { useAlarmStore } from '@/stores/xo-rest-api/alarm.store.ts'
import { useHostStore } from '@/stores/xo-rest-api/host.store.ts'
import { useSrStore } from '@/stores/xo-rest-api/sr.store.ts'
import { useVmControllerStore } from '@/stores/xo-rest-api/vm-controller.store.ts'
import { useVmStore } from '@/stores/xo-rest-api/vm.store.ts'
import type { XoAlarm } from '@/types/xo/alarm.type.ts'
import { useXoAlarmCollection } from '@/remote-resources/use-xo-alarm-collection.ts'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import { useXoSrCollection } from '@/remote-resources/use-xo-sr-collection.ts'
import { useXoVmCollection } from '@/remote-resources/use-xo-vm-collection.ts'
import { useXoVmControllerCollection } from '@/remote-resources/use-xo-vm-controller-collection.ts'
import type { XoPoolDashboard } from '@/types/xo/pool-dashboard.type.ts'
import VtsAllGoodHero from '@core/components/state-hero/VtsAllGoodHero.vue'
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
@@ -48,7 +47,7 @@ import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
import { useUiStore } from '@core/stores/ui.store.ts'
import { computed } from 'vue'
import { logicAnd } from '@vueuse/math'
import { useI18n } from 'vue-i18n'
const { poolDashboard } = defineProps<{
@@ -57,40 +56,17 @@ const { poolDashboard } = defineProps<{
const { t } = useI18n()
const { get } = useAlarmStore().subscribe()
const { isReady: areHostsReady } = useHostStore().subscribe()
const { isReady: areVmsReady } = useVmStore().subscribe()
const { isReady: areVmControllersReady } = useVmControllerStore().subscribe()
const { isReady: areSrsReady } = useSrStore().subscribe()
const { useGetAlarmsByIds, areAlarmsReady } = useXoAlarmCollection()
const { areHostsReady } = useXoHostCollection()
const { areVmsReady } = useXoVmCollection()
const { areVmControllersReady } = useXoVmControllerCollection()
const { areSrsReady } = useXoSrCollection()
const areAlarmsReady = computed(() => {
return (
poolDashboard?.alarms !== undefined &&
areHostsReady.value &&
areVmsReady.value &&
areVmControllersReady.value &&
areSrsReady.value
)
})
const isReady = logicAnd(areAlarmsReady, areHostsReady, areVmsReady, areVmControllersReady, areSrsReady)
const uiStore = useUiStore()
const alarms = computed(() => {
if (!poolDashboard?.alarms) {
return []
}
return poolDashboard.alarms
.reduce((acc, alarmId) => {
const alarm = get(alarmId)
if (alarm !== undefined) {
acc.push(alarm)
}
return acc
}, [] as XoAlarm[])
.sort((alarm1, alarm2) => new Date(alarm2.time).getTime() - new Date(alarm1.time).getTime())
})
const alarms = useGetAlarmsByIds(() => poolDashboard?.alarms ?? [])
</script>
<style lang="postcss" scoped>

View File

@@ -41,12 +41,12 @@
</UiButton>
</UiTableActions>
<UiTopBottomTable :selected-items="0" :total-items="0" @toggle-select-all="toggleSelect">
<UiTablePagination v-if="isReady" v-bind="paginationBindings" />
<UiTablePagination v-if="areNetworksReady" v-bind="paginationBindings" />
</UiTopBottomTable>
</div>
<VtsDataTable
:is-ready
:has-error
:is-ready="areNetworksReady"
:has-error="hasNetworkFetchError"
:no-data-message="networks.length === 0 ? t('no-network-detected') : undefined"
>
<template #thead>
@@ -104,14 +104,14 @@
<div>{{ t('no-result') }}</div>
</VtsStateHero>
<UiTopBottomTable :selected-items="0" :total-items="0" @toggle-select-all="toggleSelect">
<UiTablePagination v-if="isReady" v-bind="paginationBindings" />
<UiTablePagination v-if="areNetworksReady" v-bind="paginationBindings" />
</UiTopBottomTable>
</div>
</div>
</template>
<script setup lang="ts">
import { useNetworkStore } from '@/stores/xo-rest-api/network.store.ts'
import { useXoNetworkCollection } from '@/remote-resources/use-xo-network-collection.ts'
import type { XoNetwork } from '@/types/xo/network.type.ts'
import VtsDataTable from '@core/components/data-table/VtsDataTable.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
@@ -147,7 +147,7 @@ const { networks } = defineProps<{
networks: XoNetwork[]
}>()
const { isReady, hasError } = useNetworkStore().subscribe()
const { areNetworksReady, hasNetworkFetchError } = useXoNetworkCollection()
const { t } = useI18n()
const searchQuery = ref('')

View File

@@ -130,7 +130,7 @@
<script setup lang="ts">
import PifRow from '@/components/pif/PifRow.vue'
import { usePifStore } from '@/stores/xo-rest-api/pif.store.ts'
import { useXoPifCollection } from '@/remote-resources/use-xo-pif-collection.ts'
import type { XoNetwork } from '@/types/xo/network.type.ts'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
@@ -154,7 +154,7 @@ const emit = defineEmits<{
close: []
}>()
const { getPifsByNetworkId } = usePifStore().subscribe()
const { getPifsByNetworkId } = useXoPifCollection()
const uiStore = useUiStore()
const { t } = useI18n()

View File

@@ -44,12 +44,12 @@
</UiButton>
</UiTableActions>
<UiTopBottomTable :selected-items="0" :total-items="0" @toggle-select-all="toggleSelect">
<UiTablePagination v-if="isReady" v-bind="paginationBindings" />
<UiTablePagination v-if="areNetworksReady" v-bind="paginationBindings" />
</UiTopBottomTable>
</div>
<VtsDataTable
:is-ready
:has-error
:is-ready="areNetworksReady"
:has-error="hasNetworkFetchError"
:no-data-message="networks.length === 0 ? t('no-network-detected') : undefined"
>
<template #thead>
@@ -108,15 +108,15 @@
<div>{{ t('no-result') }}</div>
</VtsStateHero>
<UiTopBottomTable :selected-items="0" :total-items="0" @toggle-select-all="toggleSelect">
<UiTablePagination v-if="isReady" v-bind="paginationBindings" />
<UiTablePagination v-if="areNetworksReady" v-bind="paginationBindings" />
</UiTopBottomTable>
</div>
</div>
</template>
<script setup lang="ts">
import { useNetworkStore } from '@/stores/xo-rest-api/network.store.ts'
import { usePifStore } from '@/stores/xo-rest-api/pif.store.ts'
import { useXoNetworkCollection } from '@/remote-resources/use-xo-network-collection.ts'
import { useXoPifCollection } from '@/remote-resources/use-xo-pif-collection.ts'
import type { XoNetwork } from '@/types/xo/network.type.ts'
import VtsConnectionStatus from '@core/components/connection-status/VtsConnectionStatus.vue'
import VtsDataTable from '@core/components/data-table/VtsDataTable.vue'
@@ -157,9 +157,9 @@ const { networks } = defineProps<{
const { t } = useI18n()
const { isReady, hasError } = useNetworkStore().subscribe()
const { areNetworksReady, hasNetworkFetchError } = useXoNetworkCollection()
const { records: pifs } = usePifStore().subscribe()
const { pifs } = useXoPifCollection()
const selectedNetworkId = useRouteQuery('id')

View File

@@ -3,7 +3,7 @@
<UiTitle>
{{ t('connections') }}
</UiTitle>
<VtsLoadingHero v-if="!isReady" type="card" />
<VtsLoadingHero v-if="!areServersReady" type="card" />
<template v-else>
<VtsQuickInfoRow :label="t('ip-address')" :value="server?.host" />
<VtsQuickInfoRow :label="t('proxy-url')" :value="server?.httpProxy" />
@@ -23,7 +23,7 @@
</template>
<script setup lang="ts">
import { useServerStore } from '@/stores/xo-rest-api/server.store'
import { useXoServerCollection } from '@/remote-resources/use-xo-server-collection.ts'
import type { XoPool } from '@/types/xo/pool.type'
import VtsEnabledState from '@core/components/enabled-state/VtsEnabledState.vue'
import VtsQuickInfoRow from '@core/components/quick-info-row/VtsQuickInfoRow.vue'
@@ -37,7 +37,7 @@ const { pool } = defineProps<{ pool: XoPool }>()
const { t } = useI18n()
const { serverByPool, isReady } = useServerStore().subscribe()
const { serverByPool, areServersReady } = useXoServerCollection()
const server = computed(() => serverByPool.value.get(pool.id)?.[0])
</script>

View File

@@ -3,7 +3,7 @@
<UiTitle>
{{ t('pool-management') }}
</UiTitle>
<VtsLoadingHero v-if="!isReady" type="card" />
<VtsLoadingHero v-if="!areHostsReady" type="card" />
<template v-else>
<VtsQuickInfoRow :label="t('master')">
<template #value>
@@ -35,7 +35,7 @@
</template>
<script setup lang="ts">
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import type { XoPool } from '@/types/xo/pool.type'
import VtsEnabledState from '@core/components/enabled-state/VtsEnabledState.vue'
import VtsQuickInfoRow from '@core/components/quick-info-row/VtsQuickInfoRow.vue'
@@ -44,7 +44,6 @@ import UiCard from '@core/components/ui/card/UiCard.vue'
import UiLink from '@core/components/ui/link/UiLink.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { faServer } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { pool } = defineProps<{
@@ -53,7 +52,7 @@ const { pool } = defineProps<{
const { t } = useI18n()
const { get: getHostById, isReady } = useHostStore().subscribe()
const { useGetHostById, areHostsReady } = useXoHostCollection()
const primaryHost = computed(() => getHostById(pool.master))
const primaryHost = useGetHostById(() => pool.master)
</script>

View File

@@ -3,7 +3,7 @@
<UiTitle>
{{ t('networking') }}
</UiTitle>
<VtsLoadingHero v-if="!isReady" type="card" />
<VtsLoadingHero v-if="!areNetworksReady" type="card" />
<template v-else>
<VtsQuickInfoRow :label="t('backup-network')">
<template #value>
@@ -25,7 +25,7 @@
</template>
<script setup lang="ts">
import { useNetworkStore } from '@/stores/xo-rest-api/network.store'
import { useXoNetworkCollection } from '@/remote-resources/use-xo-network-collection.ts'
import type { XoNetwork } from '@/types/xo/network.type'
import type { XoPool } from '@/types/xo/pool.type'
import VtsQuickInfoRow from '@core/components/quick-info-row/VtsQuickInfoRow.vue'
@@ -43,7 +43,7 @@ const { pool } = defineProps<{
const { t } = useI18n()
const { get: getNetworkById, isReady } = useNetworkStore().subscribe()
const { getNetworkById, areNetworksReady } = useXoNetworkCollection()
const backupNetwork = computed(() =>
pool.otherConfig['xo:backupNetwork']

View File

@@ -3,7 +3,7 @@
<UiTitle>
{{ t('storage-configuration') }}
</UiTitle>
<VtsLoadingHero v-if="!isReady" type="card" />
<VtsLoadingHero v-if="!areSrsReady" type="card" />
<template v-else>
<VtsQuickInfoRow :label="t('default-storage-repository')">
<template #value>
@@ -40,7 +40,7 @@
</VtsQuickInfoRow>
<VtsQuickInfoRow :label="t('heartbeat-storage-repository')">
<template #value>
<ul v-if="haSrs !== undefined && haSrs.length > 0">
<ul v-if="haSrs.length > 0">
<li v-for="haSr in haSrs" :key="haSr.id">
<VtsIcon :icon="faDatabase" accent="current" />
{{ haSr.name_label }}
@@ -55,17 +55,15 @@
</UiCard>
</template>
<script setup lang="ts">
import { useSrStore } from '@/stores/xo-rest-api/sr.store'
<script lang="ts" setup>
import { useXoSrCollection } from '@/remote-resources/use-xo-sr-collection.ts'
import type { XoPool } from '@/types/xo/pool.type'
import type { XoSr } from '@/types/xo/sr.type'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsQuickInfoRow from '@core/components/quick-info-row/VtsQuickInfoRow.vue'
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiTitle from '@core/components/ui/title/UiTitle.vue'
import { faDatabase } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const { pool } = defineProps<{
@@ -74,20 +72,10 @@ const { pool } = defineProps<{
const { t } = useI18n()
const { get: getSrById, isReady } = useSrStore().subscribe()
const { useGetSrById, useGetSrsByIds, areSrsReady } = useXoSrCollection()
const defaultSr = computed(() => (pool.default_SR ? getSrById(pool.default_SR) : undefined))
const suspendSr = computed(() => (pool.suspendSr ? getSrById(pool.suspendSr) : undefined))
const crashDumpSr = computed(() => (pool.crashDumpSr ? getSrById(pool.crashDumpSr) : undefined))
const haSrs = computed(() => {
if (pool.haSrs === undefined || pool.haSrs?.length === 0) {
return
}
return pool.haSrs.reduce((acc, srId) => {
const sr = getSrById(srId)
return sr ? [...acc, sr] : acc
}, [] as XoSr[])
})
const defaultSr = useGetSrById(() => pool.default_SR)
const suspendSr = useGetSrById(() => pool.suspendSr)
const crashDumpSr = useGetSrById(() => pool.crashDumpSr)
const haSrs = useGetSrsByIds(() => pool.haSrs ?? [])
</script>

View File

@@ -7,10 +7,10 @@
</template>
<script setup lang="ts">
import { useHostStore } from '@/stores/xo-rest-api/host.store.ts'
import { useSrStore } from '@/stores/xo-rest-api/sr.store.ts'
import { useVmControllerStore } from '@/stores/xo-rest-api/vm-controller.store.ts'
import { useVmStore } from '@/stores/xo-rest-api/vm.store.ts'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import { useXoSrCollection } from '@/remote-resources/use-xo-sr-collection.ts'
import { useXoVmCollection } from '@/remote-resources/use-xo-vm-collection.ts'
import { useXoVmControllerCollection } from '@/remote-resources/use-xo-vm-controller-collection.ts'
import type { XoVmController } from '@/types/xo/vm-controller.type.ts'
import UiLink from '@core/components/ui/link/UiLink.vue'
import { faDatabase, faDesktop, faServer } from '@fortawesome/free-solid-svg-icons'
@@ -22,23 +22,23 @@ const { type, uuid } = defineProps<{
uuid: XapiXoRecord['uuid']
}>()
const { records: hostRecords } = useHostStore().subscribe()
const { records: vmRecords } = useVmStore().subscribe()
const { records: vmControllerRecords } = useVmControllerStore().subscribe()
const { records: srRecords } = useSrStore().subscribe()
const { hosts } = useXoHostCollection()
const { vms } = useXoVmCollection()
const { vmControllers } = useXoVmControllerCollection()
const { srs } = useXoSrCollection()
const record = computed(() => {
if (type === 'VM') {
return vmRecords.value.find(vm => vm.id === uuid)
return vms.value.find(vm => vm.id === uuid)
}
if (type === 'host') {
return hostRecords.value.find(host => host.id === uuid)
return hosts.value.find(host => host.id === uuid)
}
if (type === 'VM-controller') {
return vmControllerRecords.value.find(vmController => vmController.id === uuid)
return vmControllers.value.find(vmController => vmController.id === uuid)
}
if (type === 'SR') {
return srRecords.value.find(sr => sr.id === uuid)
return srs.value.find(sr => sr.id === uuid)
}
return undefined

View File

@@ -33,10 +33,10 @@
<script setup lang="ts">
import AlarmLink from '@/components/site/dashboard/AlarmLink.vue'
import { useHostStore } from '@/stores/xo-rest-api/host.store.ts'
import { useSrStore } from '@/stores/xo-rest-api/sr.store.ts'
import { useVmControllerStore } from '@/stores/xo-rest-api/vm-controller.store.ts'
import { useVmStore } from '@/stores/xo-rest-api/vm.store.ts'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import { useXoSrCollection } from '@/remote-resources/use-xo-sr-collection.ts'
import { useXoVmCollection } from '@/remote-resources/use-xo-vm-collection.ts'
import { useXoVmControllerCollection } from '@/remote-resources/use-xo-vm-controller-collection.ts'
import type { XoAlarm } from '@/types/xo/alarm.type.ts'
import VtsAllGoodHero from '@core/components/state-hero/VtsAllGoodHero.vue'
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
@@ -46,7 +46,7 @@ import UiCard from '@core/components/ui/card/UiCard.vue'
import UiCardTitle from '@core/components/ui/card-title/UiCardTitle.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
import { useUiStore } from '@core/stores/ui.store.ts'
import { computed } from 'vue'
import { logicAnd } from '@vueuse/math'
import { useI18n } from 'vue-i18n'
const { alarms } = defineProps<{
@@ -55,14 +55,12 @@ const { alarms } = defineProps<{
const { t } = useI18n()
const { isReady: areHostsReady } = useHostStore().subscribe()
const { isReady: areVmsReady } = useVmStore().subscribe()
const { isReady: areVmControllersReady } = useVmControllerStore().subscribe()
const { isReady: areSrsReady } = useSrStore().subscribe()
const { areHostsReady } = useXoHostCollection()
const { areVmsReady } = useXoVmCollection()
const { areVmControllersReady } = useXoVmControllerCollection()
const { areSrsReady } = useXoSrCollection()
const areAlarmsReady = computed(
() => areHostsReady.value && areVmsReady.value && areVmControllersReady.value && areSrsReady.value
)
const areAlarmsReady = logicAnd(areHostsReady, areVmsReady, areVmControllersReady, areSrsReady)
const uiStore = useUiStore()
</script>

View File

@@ -33,7 +33,7 @@
</template>
<script setup lang="ts">
import type { BackupRepositories } from '@/requests/use-site-dashboard.request.ts'
import type { BackupRepositories } from '@/remote-resources/use-xo-site-dashboard.ts'
import VtsStackedBarWithLegend, {
type StackedBarWithLegendProps,
} from '@core/components/stacked-bar-with-legend/VtsStackedBarWithLegend.vue'

View File

@@ -12,7 +12,7 @@
import BackupRepository from '@/components/site/dashboard/BackupRepository.vue'
import S3BackupRepository from '@/components/site/dashboard/S3BackupRepository.vue'
import StorageRepository from '@/components/site/dashboard/StorageRepository.vue'
import type { BackupRepositories, StorageRepositories } from '@/requests/use-site-dashboard.request.ts'
import type { BackupRepositories, StorageRepositories } from '@/remote-resources/use-xo-site-dashboard.ts'
import type { XoDashboard } from '@/types/xo/dashboard.type.ts'
import VtsDivider from '@core/components/divider/VtsDivider.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'

View File

@@ -32,7 +32,7 @@
</template>
<script setup lang="ts">
import type { StorageRepositories } from '@/requests/use-site-dashboard.request.ts'
import type { StorageRepositories } from '@/remote-resources/use-xo-site-dashboard.ts'
import VtsStackedBarWithLegend, {
type StackedBarWithLegendProps,
} from '@core/components/stacked-bar-with-legend/VtsStackedBarWithLegend.vue'

View File

@@ -1,5 +1,5 @@
<template>
<VtsLoadingHero v-if="!isPoolReady" type="panel" />
<VtsLoadingHero v-if="!arePoolsReady" type="panel" />
<UiPanel v-else :class="{ 'mobile-drawer': uiStore.isMobile }">
<template #header>
<div :class="{ 'action-buttons-container': uiStore.isMobile }">
@@ -190,8 +190,8 @@
</template>
<script setup lang="ts">
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { usePoolStore } from '@/stores/xo-rest-api/pool.store'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import { useXoPoolCollection } from '@/remote-resources/use-xo-pool-collection.ts'
import type { XoServer } from '@/types/xo/server.type'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsCopyButton from '@core/components/copy-button/VtsCopyButton.vue'
@@ -237,11 +237,11 @@ const emit = defineEmits<{
const { t } = useI18n()
const uiStore = useUiStore()
const { isReady: isPoolReady, get: getPoolById } = usePoolStore().subscribe()
const { get: getHostById, hostsByPool } = useHostStore().subscribe()
const { arePoolsReady, useGetPoolById } = useXoPoolCollection()
const { useGetHostById, hostsByPool } = useXoHostCollection()
const pool = computed(() => (server.poolId ? getPoolById(server.poolId) : undefined))
const primaryHost = computed(() => (server.master ? getHostById(server.master) : undefined))
const pool = useGetPoolById(() => server.poolId)
const primaryHost = useGetHostById(() => server.master)
const hosts = computed(() => (server.poolId ? hostsByPool.value.get(server.poolId) : undefined))
const connectionStatus = useMapper(

View File

@@ -39,12 +39,12 @@
</UiButton>
</UiTableActions>
<UiTopBottomTable :selected-items="0" :total-items="0" @toggle-select-all="toggleSelect">
<UiTablePagination v-if="isServerReady" v-bind="paginationBindings" />
<UiTablePagination v-if="areServersReady" v-bind="paginationBindings" />
</UiTopBottomTable>
</div>
<VtsDataTable
:is-ready="isServerReady"
:has-error
:is-ready="areServersReady"
:has-error="hasServerFetchError"
:no-data-message="servers.length === 0 ? t('no-server-detected') : undefined"
>
<template #thead>
@@ -117,15 +117,15 @@
</VtsStateHero>
<VtsStateHero v-if="!servers.length" image="no-data" type="page" />
<UiTopBottomTable :selected-items="0" :total-items="0" @toggle-select-all="toggleSelect">
<UiTablePagination v-if="isServerReady" v-bind="paginationBindings" />
<UiTablePagination v-if="areServersReady" v-bind="paginationBindings" />
</UiTopBottomTable>
</div>
</div>
</template>
<script setup lang="ts">
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { useServerStore } from '@/stores/xo-rest-api/server.store.ts'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import { useXoServerCollection } from '@/remote-resources/use-xo-server-collection.ts'
import type { XoServer } from '@/types/xo/server.type.ts'
import VtsDataTable from '@core/components/data-table/VtsDataTable.vue'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
@@ -166,8 +166,8 @@ const { servers } = defineProps<{
const { t } = useI18n()
const { isReady: isServerReady, hasError } = useServerStore().subscribe()
const { get: getHostById } = useHostStore().subscribe()
const { areServersReady, hasServerFetchError } = useXoServerCollection()
const { getHostById } = useXoHostCollection()
const selectedServerId = useRouteQuery('id')
const searchQuery = ref('')
@@ -222,7 +222,7 @@ const getPoolInfo = (server: XoServer) => {
}
const getPrimaryHost = (server: XoServer) => {
const masterHost = server.master ? getHostById(server.master) : undefined
const masterHost = getHostById(server.master)
return masterHost ? { label: masterHost.name_label, to: `/host/${masterHost.id}/`, icon: faServer } : undefined
}

View File

@@ -1,19 +1,10 @@
<template>
<VtsQuickTaskButton :loading="!isReady" :tasks />
<VtsQuickTaskButton :loading="!areTasksReady" :tasks="lastDayTasks" />
</template>
<script lang="ts" setup>
import { useTaskStore } from '@/stores/xo-rest-api/task.store'
import { convertTaskToCore } from '@/utils/convert-task-to-core.util'
import { useXoTaskCollection } from '@/remote-resources/use-xo-task-collection.ts'
import VtsQuickTaskButton from '@core/components/task/VtsQuickTaskButton.vue'
import { computed } from 'vue'
const { records, isReady } = useTaskStore().subscribe()
const ONE_DAY = 24 * 60 * 60 * 1000
const tasks = computed(() => {
const now = Date.now()
return records.value.filter(task => now - task.start < ONE_DAY).map(task => convertTaskToCore(task))
})
const { lastDayTasks, areTasksReady } = useXoTaskCollection()
</script>

View File

@@ -31,8 +31,8 @@
<script lang="ts" setup>
import VmTreeList from '@/components/tree/VmTreeList.vue'
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import { useXoVmCollection } from '@/remote-resources/use-xo-vm-collection.ts'
import type { HostBranch } from '@/types/tree.type'
import type { HostState } from '@core/types/object-icon.type'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
@@ -52,8 +52,8 @@ const props = defineProps<{
const { t } = useI18n()
const { isMasterHost } = useHostStore().subscribe()
const { runningVms } = useVmStore().subscribe()
const { isMasterHost } = useXoHostCollection()
const { runningVms } = useXoVmCollection()
const isMaster = computed(() => isMasterHost(props.branch.data.id))

View File

@@ -24,7 +24,7 @@
<script lang="ts" setup>
import HostTreeList from '@/components/tree/HostTreeList.vue'
import VmTreeList from '@/components/tree/VmTreeList.vue'
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
import { useXoVmCollection } from '@/remote-resources/use-xo-vm-collection.ts'
import type { HostBranch, PoolBranch, VmLeaf } from '@/types/tree.type'
import VtsTreeItem from '@core/components/tree/VtsTreeItem.vue'
import VtsTreeList from '@core/components/tree/VtsTreeList.vue'
@@ -41,7 +41,7 @@ const props = defineProps<{
const { t } = useI18n()
const { runningVms } = useVmStore().subscribe()
const { runningVms } = useXoVmCollection()
const treeBranches = computed(
() => props.branch.children.filter(child => child.discriminator === 'host') as HostBranch[]

View File

@@ -20,7 +20,7 @@
<script lang="ts" setup>
import PoolTreeList from '@/components/tree/PoolTreeList.vue'
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
import { useXoVmCollection } from '@/remote-resources/use-xo-vm-collection.ts'
import type { SiteBranch } from '@/types/tree.type'
import VtsTreeItem from '@core/components/tree/VtsTreeItem.vue'
import UiCounter from '@core/components/ui/counter/UiCounter.vue'
@@ -36,7 +36,7 @@ defineProps<{
const { t } = useI18n()
const { runningVms } = useVmStore().subscribe()
const { runningVms } = useXoVmCollection()
const runningVmsCount = computed(() => runningVms.value.length)
</script>

View File

@@ -1,5 +1,5 @@
<template>
<VtsQuickInfoCard class="vm-dashboard-quick-info" :loading="!isReady">
<VtsQuickInfoCard class="vm-dashboard-quick-info" :loading="!areVmsReady">
<VtsQuickInfoColumn>
<VtsQuickInfoRow :label="t('state')">
<template #value>
@@ -48,7 +48,7 @@
</template>
<script lang="ts" setup>
import { useVmStore } from '@/stores/xo-rest-api/vm.store.ts'
import { useXoVmCollection } from '@/remote-resources/use-xo-vm-collection.ts'
import { VM_POWER_STATE, type XoVm } from '@/types/xo/vm.type.ts'
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import VtsQuickInfoCard from '@core/components/quick-info-card/VtsQuickInfoCard.vue'
@@ -71,7 +71,7 @@ const { vm } = defineProps<{
const { t, locale } = useI18n()
const { isReady, getVmHost } = useVmStore().subscribe()
const { areVmsReady, getVmHost } = useXoVmCollection()
const host = computed(() => getVmHost(vm))

View File

@@ -178,8 +178,8 @@
</template>
<script setup lang="ts">
import { useNetworkStore } from '@/stores/xo-rest-api/network.store'
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
import { useXoNetworkCollection } from '@/remote-resources/use-xo-network-collection.ts'
import { useXoVmCollection } from '@/remote-resources/use-xo-vm-collection.ts'
import type { XoVif } from '@/types/xo/vif.type'
import VtsCardRowKeyValue from '@core/components/card/VtsCardRowKeyValue.vue'
import VtsConnectionStatus from '@core/components/connection-status/VtsConnectionStatus.vue'
@@ -203,17 +203,17 @@ const emit = defineEmits<{
const { t } = useI18n()
const { get: getNetwork } = useNetworkStore().subscribe()
const { get: getVm } = useVmStore().subscribe()
const { useGetNetworkById } = useXoNetworkCollection()
const { getVmById } = useXoVmCollection()
const uiStore = useUiStore()
const ipAddresses = computed(() => {
const addresses = getVm(vif.$VM)?.addresses
const addresses = getVmById(vif.$VM)?.addresses
return addresses ? [...new Set(Object.values(addresses).sort())] : []
})
const network = computed(() => getNetwork(vif.$network))
const network = useGetNetworkById(() => vif.$network)
const status = computed(() => (vif.attached ? 'connected' : 'disconnected'))
</script>

View File

@@ -52,10 +52,14 @@
</UiTableActions>
<UiTopBottomTable :selected-items="0" :total-items="0">
<UiTablePagination v-if="isReady" v-bind="paginationBindings" />
<UiTablePagination v-if="areVifsReady" v-bind="paginationBindings" />
</UiTopBottomTable>
</div>
<VtsDataTable :is-ready :has-error :no-data-message="vifs.length === 0 ? t('no-vif-detected') : undefined">
<VtsDataTable
:is-ready="areVifsReady"
:has-error="hasVifFetchError"
:no-data-message="vifs.length === 0 ? t('no-vif-detected') : undefined"
>
<template #thead>
<tr>
<template v-for="column of visibleColumns" :key="column.id">
@@ -126,16 +130,16 @@
</template>
</VtsDataTable>
<UiTopBottomTable :selected-items="0" :total-items="0">
<UiTablePagination v-if="isReady" v-bind="paginationBindings" />
<UiTablePagination v-if="areVifsReady" v-bind="paginationBindings" />
</UiTopBottomTable>
</div>
</div>
</template>
<script setup lang="ts">
import { useNetworkStore } from '@/stores/xo-rest-api/network.store'
import { useVifStore } from '@/stores/xo-rest-api/vif.store'
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
import { useXoNetworkCollection } from '@/remote-resources/use-xo-network-collection.ts'
import { useXoVifCollection } from '@/remote-resources/use-xo-vif-collection.ts'
import { useXoVmCollection } from '@/remote-resources/use-xo-vm-collection.ts'
import type { XoVif } from '@/types/xo/vif.type'
import VtsConnectionStatus from '@core/components/connection-status/VtsConnectionStatus.vue'
import VtsDataTable from '@core/components/data-table/VtsDataTable.vue'
@@ -173,14 +177,14 @@ const { vifs } = defineProps<{
vifs: XoVif[]
}>()
const { get: getNetwork } = useNetworkStore().subscribe()
const { get: getVm } = useVmStore().subscribe()
const { isReady, hasError } = useVifStore().subscribe()
const { getNetworkById } = useXoNetworkCollection()
const { getVmById } = useXoVmCollection()
const { areVifsReady, hasVifFetchError } = useXoVifCollection()
const { t } = useI18n()
const selectedVifId = useRouteQuery('id')
const getNetworkName = (vif: XoVif) => getNetwork(vif.$network)?.name_label ?? ''
const getNetworkName = (vif: XoVif) => getNetworkById(vif.$network)?.name_label ?? ''
const searchQuery = ref('')
@@ -197,7 +201,7 @@ const filteredVifs = computed(() => {
})
const getIpAddresses = (vif: XoVif) => {
const addresses = getVm(vif.$VM)?.addresses
const addresses = getVmById(vif.$VM)?.addresses
return addresses ? [...new Set(Object.values(addresses).sort())] : []
}

View File

@@ -38,7 +38,7 @@
</template>
<script setup lang="ts">
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import { VM_OPERATION, type XoVm } from '@/types/xo/vm.type'
import VtsEnabledState from '@core/components/enabled-state/VtsEnabledState.vue'
import VtsQuickInfoRow from '@core/components/quick-info-row/VtsQuickInfoRow.vue'
@@ -53,7 +53,7 @@ import { useI18n } from 'vue-i18n'
const { vm } = defineProps<{ vm: XoVm }>()
const { t } = useI18n()
const { get: getHostById } = useHostStore().subscribe()
const { getHostById } = useXoHostCollection()
const affinityHostName = computed(() => (vm.affinityHost ? getHostById(vm.affinityHost)?.name_label : ''))
const protectedOperations = [

View File

@@ -1,6 +1,6 @@
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { usePoolStore } from '@/stores/xo-rest-api/pool.store'
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import { useXoPoolCollection } from '@/remote-resources/use-xo-pool-collection.ts'
import { useXoVmCollection } from '@/remote-resources/use-xo-vm-collection.ts'
import type { XoSite } from '@/types/xo/site.type.ts'
import { defineTree } from '@core/composables/tree/define-tree'
import { useTreeFilter } from '@core/composables/tree-filter.composable'
@@ -9,9 +9,9 @@ import { logicAnd } from '@vueuse/math'
import { computed, useId } from 'vue'
export function useSiteTree() {
const { records: pools, isReady: isPoolReady } = usePoolStore().subscribe()
const { hostsByPool, isReady: isHostReady } = useHostStore().subscribe()
const { vmsByHost, hostLessVmsByPool, isReady: isVmReady } = useVmStore().subscribe()
const { pools, arePoolsReady } = useXoPoolCollection()
const { hostsByPool, areHostsReady } = useXoHostCollection()
const { vmsByHost, hostLessVmsByPool, areVmsReady } = useXoVmCollection()
const { filter, predicate } = useTreeFilter()
const site: XoSite = {
@@ -22,7 +22,7 @@ export function useSiteTree() {
const sites = computed(() => [site])
const isReady = logicAnd(isPoolReady, isHostReady, isVmReady)
const isReady = logicAnd(arePoolsReady, areHostsReady, areVmsReady)
const definitions = computed(() =>
defineTree(

View File

@@ -0,0 +1,84 @@
# `useXoCollectionState` documentation
This composable is used to manage the state of a collection of resources in Xen Orchestra, such as VMs, hosts, etc.
It is meant to be used in conjunction with the `defineRemoteResource` utility from the Web Core library.
```typescript
const useVmCollection = defineRemoteResource({
url: /path/to/vms',
initialData: () => [] as XoVm[],
state: (data, context) => useXoCollectionState(data, { context, baseName: 'vm' }),
})
```
## Arguments
| Name | Type | Description |
| ----------------- | ---------------------- | --------------------------------------------------------------------- |
| `collection` | `Ref<XoRecord[]>` | The XO record collection |
| `config.context` | `ResourceContext` | The context for the resource |
| `config.baseName` | `string \| NameConfig` | The base name for the resource, used to generate the state properties |
# Returns
When using a `string` as `baseName`, the following properties will be generated:
| Name | Type | Description |
| ---------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `<name>s` | `Ref<XoRecord[]>` | The collection of records |
| `get<Name>ById` | `(id: XoRecord['id'] \| undefined) => XoRecord \| undefined` | Function to get a record by its ID |
| `useGet<Name>ById` | `(id: MaybeRefOrGetter<XoRecord['id']> \| undefined) => ComputedRef<XoRecord \| undefined>` | Reactive version of `get<Name>ById` |
| `get<Name>sByIds` | `(ids: XoRecord['id'][]) => XoRecord[]` | Function to get multiple records by their IDs |
| `useGet<Name>sByIds` | `(ids: MaybeRefOrGetter<XoRecord['id'][]>) => ComputedRef<XoRecord[]>` | Reactive version of `get<Name>sByIds` |
| `has<Name>ById` | `(id: XoRecord['id'] \| undefined) => boolean` | Function to check if a record exists by its ID |
| `useHas<Name>ById` | `(id: MaybeRefOrGetter<XoRecord['id']> \| undefined) => ComputedRef<boolean>` | Reactive version of `has<Name>ById` |
| `are<Name>sReady` | `ComputedRef<boolean>` | Whether the collection is ready |
| `are<Name>sFetching` | `ComputedRef<boolean>` | Whether the collection is fetching |
| `has<Name>FetchError` | `ComputedRef<boolean>` | Whether there was an error fetching the collection |
| `last<Name>FetchError` | `ComputedRef<Error \| undefined>` | The last error that occurred while fetching the collection |
> [!IMPORTANT] > `are<Name>sReady` becomes `true` when a fetch completes successfully, even if the collection is empty. It will stay `true` even if a later fetch fails.
> `are<Name>sFetching` is `true` while the fetch is in progress, even if `are<Name>sReady` is `true`.
## Example
```typescript
const {
vms,
getVmById,
useGetVmById,
getVmsByIds,
useGetVmsByIds,
hasVmById,
useHasVmById,
areVmsReady,
areVmsFetching,
hasVmFetchError,
lastVmFetchError,
} = useXoCollectionState(vms, {
context,
baseName: 'vm',
})
```
### Using string tuple as `baseName` to handle irregular plural
```typescript
const {
proxies,
getProxyById,
useGetProxyById,
getProxiesByIds,
useGetProxiesByIds,
hasProxyById,
useHasProxyById,
areProxysReady,
areProxysFetching,
hasProxyFetchError,
lastProxyFetchError,
} = useXoCollectionState(proxies, {
context,
baseName: ['proxy', 'proxies'],
})
```

View File

@@ -0,0 +1,26 @@
import type { NameConfig } from '@/composables/xo-collection-state/types.ts'
export function toNameConfig(baseName: string | [string, string] | NameConfig): NameConfig {
if (typeof baseName === 'object' && !Array.isArray(baseName)) {
return baseName
}
const [singularName, pluralName] = Array.isArray(baseName) ? baseName : [baseName, `${baseName}s`]
const capitalizedSingularName = singularName.charAt(0).toUpperCase() + singularName.slice(1)
const capitalizedPluralName = pluralName.charAt(0).toUpperCase() + pluralName.slice(1)
return {
records: `${pluralName}`,
getById: `get${capitalizedSingularName}ById`,
getByIds: `get${capitalizedPluralName}ByIds`,
useGetById: `useGet${capitalizedSingularName}ById`,
useGetByIds: `useGet${capitalizedPluralName}ByIds`,
hasById: `has${capitalizedSingularName}ById`,
useHasById: `useHas${capitalizedSingularName}ById`,
isReady: `are${capitalizedPluralName}Ready`,
isFetching: `are${capitalizedPluralName}Fetching`,
hasError: `has${capitalizedSingularName}FetchError`,
lastError: `last${capitalizedSingularName}FetchError`,
}
}

View File

@@ -0,0 +1,37 @@
import type { XoRecord } from '@/types/xo'
import type { MaybeRefOrGetter } from '@vueuse/shared'
import type { ComputedRef, Ref } from 'vue'
export type NameConfig = {
records: string
getById: string
getByIds: string
useGetById: string
useGetByIds: string
hasById: string
useHasById: string
isReady: string
isFetching: string
hasError: string
lastError: string
}
export type BaseName = string | [string, string] | NameConfig
export type DefaultState<TRecord extends XoRecord> = {
records: Ref<TRecord[]>
getById: (id: TRecord['id'] | undefined) => TRecord | undefined
getByIds: (ids: TRecord['id'][]) => TRecord[]
useGetById: (id: MaybeRefOrGetter<TRecord['id'] | undefined>) => ComputedRef<TRecord | undefined>
useGetByIds: (id: MaybeRefOrGetter<TRecord['id'][]>) => ComputedRef<TRecord[]>
hasById: (id: TRecord['id'] | undefined) => boolean
useHasById: (id: MaybeRefOrGetter<TRecord['id'] | undefined>) => ComputedRef<boolean>
isReady: ComputedRef<boolean>
isFetching: ComputedRef<boolean>
hasError: ComputedRef<boolean>
lastError: ComputedRef<Error | undefined>
}
export type CollectionState<TRecord extends XoRecord, TNameConfig extends NameConfig> = {
[K in keyof NameConfig as TNameConfig[K]]: DefaultState<TRecord>[K]
}

View File

@@ -0,0 +1,104 @@
import { toNameConfig } from '@/composables/xo-collection-state/to-name-config.ts'
import type { BaseName, CollectionState, NameConfig } from '@/composables/xo-collection-state/types.ts'
import type { XoRecord } from '@/types/xo'
import type { ResourceContext } from '@core/packages/remote-resource/types.ts'
import { reactify } from '@vueuse/shared'
import { type Ref } from 'vue'
export function useXoCollectionState<TRecord extends XoRecord, const TBaseName extends string>(
collection: Ref<TRecord[]>,
config: {
context: ResourceContext<any[]>
baseName: TBaseName
}
): CollectionState<
TRecord,
{
records: `${TBaseName}s`
getById: `get${Capitalize<TBaseName>}ById`
getByIds: `get${Capitalize<TBaseName>}sByIds`
useGetById: `useGet${Capitalize<TBaseName>}ById`
useGetByIds: `useGet${Capitalize<TBaseName>}sByIds`
hasById: `has${Capitalize<TBaseName>}ById`
useHasById: `useHas${Capitalize<TBaseName>}ById`
isReady: `are${Capitalize<TBaseName>}sReady`
isFetching: `are${Capitalize<TBaseName>}sFetching`
hasError: `has${Capitalize<TBaseName>}FetchError`
lastError: `last${Capitalize<TBaseName>}FetchError`
}
>
export function useXoCollectionState<TRecord extends XoRecord, const TBaseName extends [string, string]>(
collection: Ref<TRecord[]>,
config: {
context: ResourceContext<any[]>
baseName: TBaseName
}
): CollectionState<
TRecord,
{
records: `${TBaseName[1]}`
getById: `get${Capitalize<TBaseName[0]>}ById`
getByIds: `get${Capitalize<TBaseName[1]>}ByIds`
useGetById: `useGet${Capitalize<TBaseName[0]>}ById`
useGetByIds: `useGet${Capitalize<TBaseName[1]>}ByIds`
hasById: `has${Capitalize<TBaseName[0]>}ById`
useHasById: `useHas${Capitalize<TBaseName[0]>}ById`
isReady: `are${Capitalize<TBaseName[1]>}Ready`
isFetching: `are${Capitalize<TBaseName[1]>}Fetching`
hasError: `has${Capitalize<TBaseName[0]>}FetchError`
lastError: `last${Capitalize<TBaseName[0]>}FetchError`
}
>
export function useXoCollectionState<TRecord extends XoRecord, TNameConfig extends NameConfig>(
collection: Ref<TRecord[]>,
config: {
context: ResourceContext<any[]>
baseName: TNameConfig
}
): CollectionState<TRecord, TNameConfig>
export function useXoCollectionState<TRecord extends XoRecord, TNameConfig extends BaseName>(
collection: Ref<TRecord[]>,
config: {
context: ResourceContext<any[]>
baseName: TNameConfig
}
) {
function getById(id: TRecord['id'] | undefined) {
if (id === undefined) {
return undefined
}
return collection.value.find(record => record.id === id)
}
function getByIds(ids: TRecord['id'][]) {
const idSet = new Set(ids)
return collection.value.filter(record => idSet.has(record.id))
}
function hasById(id: TRecord['id'] | undefined) {
if (id === undefined) {
return false
}
return collection.value.some(record => record.id === id)
}
const names = toNameConfig(config.baseName)
return {
[names.records]: collection,
[names.getById]: getById,
[names.useGetById]: reactify(getById),
[names.getByIds]: getByIds,
[names.useGetByIds]: reactify(getByIds),
[names.hasById]: hasById,
[names.useHasById]: reactify(hasById),
[names.isReady]: config.context.isReady,
[names.hasError]: config.context.hasError,
[names.lastError]: config.context.lastError,
} satisfies CollectionState<TRecord, NameConfig>
}

View File

@@ -1,5 +1,5 @@
<template>
<VtsLoadingHero v-if="!isReady" type="page" />
<VtsLoadingHero v-if="!arePoolsReady" type="page" />
<RouterView v-slot="{ Component }">
<SiteHeader />
<component :is="Component" />
@@ -8,11 +8,11 @@
<script lang="ts" setup>
import SiteHeader from '@/components/site/SiteHeader.vue'
import { usePoolStore } from '@/stores/xo-rest-api/pool.store.ts'
import { useXoPoolCollection } from '@/remote-resources/use-xo-pool-collection.ts'
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
import { useDefaultTab } from '@core/composables/default-tab.composable.ts'
useDefaultTab('/(site)', 'dashboard')
const { isReady } = usePoolStore().subscribe()
const { arePoolsReady } = useXoPoolCollection()
</script>

View File

@@ -33,14 +33,15 @@ import PoolsStatus from '@/components/site/dashboard/PoolsStatus.vue'
import Repositories from '@/components/site/dashboard/Repositories.vue'
import ResourcesOverview from '@/components/site/dashboard/ResourcesOverview.vue'
import VmsStatus from '@/components/site/dashboard/VmsStatus.vue'
import { useSiteDashboard } from '@/requests/use-site-dashboard.request.ts'
import { useAlarmStore } from '@/stores/xo-rest-api/alarm.store.ts'
import { useXoAlarmCollection } from '@/remote-resources/use-xo-alarm-collection.ts'
import { useXoSiteDashboard } from '@/remote-resources/use-xo-site-dashboard.ts'
import { useUiStore } from '@core/stores/ui.store.ts'
const uiStore = useUiStore()
const { dashboard, backupRepositories, storageRepositories } = useSiteDashboard()
const { records: alarms } = useAlarmStore().subscribe()
const { dashboard, backupRepositories, storageRepositories } = useXoSiteDashboard()
const { alarms } = useXoAlarmCollection()
</script>
<style lang="postcss" scoped>

View File

@@ -13,7 +13,7 @@
<script setup lang="ts">
import PoolsSidePanel from '@/components/site/pools/PoolsSidePanel.vue'
import PoolsTable from '@/components/site/pools/PoolsTable.vue'
import { useServerStore } from '@/stores/xo-rest-api/server.store'
import { useXoServerCollection } from '@/remote-resources/use-xo-server-collection.ts'
import type { XoServer } from '@/types/xo/server.type'
import VtsNoSelectionHero from '@core/components/state-hero/VtsNoSelectionHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
@@ -21,11 +21,11 @@ import UiPanel from '@core/components/ui/panel/UiPanel.vue'
import { useRouteQuery } from '@core/composables/route-query.composable'
import { useUiStore } from '@core/stores/ui.store'
const { records: servers, get } = useServerStore().subscribe()
const { servers, getServerById } = useXoServerCollection()
const uiStore = useUiStore()
const selectedServer = useRouteQuery<XoServer | undefined>('id', {
toData: id => get(id as XoServer['id']),
toData: id => getServerById(id as XoServer['id']),
toQuery: server => server?.id ?? '',
})
</script>

View File

@@ -1,5 +1,5 @@
<template>
<VtsLoadingHero v-if="!isReady" type="page" />
<VtsLoadingHero v-if="!areHostsReady" type="page" />
<VtsObjectNotFoundHero v-else-if="!host" :id="route.params.id" type="page" />
<RouterView v-else v-slot="{ Component }">
<HostHeader v-if="uiStore.hasUi" :host />
@@ -9,21 +9,20 @@
<script lang="ts" setup>
import HostHeader from '@/components/host/HostHeader.vue'
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import type { XoHost } from '@/types/xo/host.type'
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
import VtsObjectNotFoundHero from '@core/components/state-hero/VtsObjectNotFoundHero.vue'
import { useDefaultTab } from '@core/composables/default-tab.composable.ts'
import { useUiStore } from '@core/stores/ui.store'
import { computed } from 'vue'
import { useRoute } from 'vue-router/auto'
useDefaultTab('/host/[id]', 'dashboard')
const route = useRoute<'/host/[id]'>()
const { isReady, get } = useHostStore().subscribe()
const { areHostsReady, useGetHostById } = useXoHostCollection()
const uiStore = useUiStore()
const host = computed(() => get(route.params.id as XoHost['id']))
const host = useGetHostById(() => route.params.id as XoHost['id'])
</script>

View File

@@ -11,8 +11,8 @@
</template>
<script lang="ts" setup>
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { HOST_OPERATION, type XoHost } from '@/types/xo/host.type'
import { isHostOperationPending } from '@/utils/xo-records/host.util.ts'
import VtsActionsConsole from '@core/components/console/VtsActionsConsole.vue'
import VtsClipboardConsole from '@core/components/console/VtsClipboardConsole.vue'
import VtsLayoutConsole from '@core/components/console/VtsLayoutConsole.vue'
@@ -27,8 +27,6 @@ const props = defineProps<{
const { t } = useI18n()
const { isHostOperationPending } = useHostStore().subscribe()
const STOP_OPERATIONS = [HOST_OPERATION.SHUTDOWN, HOST_OPERATION.REBOOT]
const url = computed(

View File

@@ -13,21 +13,22 @@
<script setup lang="ts">
import HostPifSidePanel from '@/components/host/network/HostPifSidePanel.vue'
import HostPifTable from '@/components/host/network/HostPifTable.vue'
import { usePifStore } from '@/stores/xo-rest-api/pif.store'
import { useXoPifCollection } from '@/remote-resources/use-xo-pif-collection.ts'
import type { XoHost } from '@/types/xo/host.type.ts'
import type { XoPif } from '@/types/xo/pif.type'
import VtsNoSelectionHero from '@core/components/state-hero/VtsNoSelectionHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
import UiPanel from '@core/components/ui/panel/UiPanel.vue'
import { useRouteQuery } from '@core/composables/route-query.composable'
import { useUiStore } from '@core/stores/ui.store'
import { useArrayFilter } from '@vueuse/shared'
import { computed } from 'vue'
import { useRoute } from 'vue-router/auto'
const { records } = usePifStore().subscribe()
const { pifsByHost } = useXoPifCollection()
const uiStore = useUiStore()
const route = useRoute<'/host/[id]'>()
const pifs = useArrayFilter(records, pif => pif.$host === route.params.id)
const pifs = computed(() => pifsByHost.value.get(route.params.id as XoHost['id']) ?? [])
const selectedPif = useRouteQuery<XoPif | undefined>('id', {
toData: id => pifs.value.find(pif => pif.id === id),

View File

@@ -1,10 +1,10 @@
<template>
<VtsLoadingHero v-if="!isReady" type="page" />
<VtsLoadingHero v-if="!areVmsReady" type="page" />
<UiCard v-else class="vms">
<div class="pagination-container">
<!-- TODO: update with item selection button when available -->
<p class="typo-body-regular-small count">{{ t('n-vms', { n: vms.length }) }}</p>
<UiTablePagination v-if="isReady" v-bind="paginationBindings" />
<UiTablePagination v-if="areVmsReady" v-bind="paginationBindings" />
</div>
<VtsTable vertical-border>
<thead>
@@ -30,13 +30,13 @@
<div class="pagination-container">
<!-- TODO: update with item selection button when available -->
<p class="typo-body-regular-small count">{{ t('n-vms', { n: vms.length }) }}</p>
<UiTablePagination v-if="isReady" v-bind="paginationBindings" />
<UiTablePagination v-if="areVmsReady" v-bind="paginationBindings" />
</div>
</UiCard>
</template>
<script lang="ts" setup>
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
import { useXoVmCollection } from '@/remote-resources/use-xo-vm-collection.ts'
import type { XoHost } from '@/types/xo/host.type'
import type { VmState } from '@core/types/object-icon.type'
import VtsCellObject from '@core/components/cell-object/VtsCellObject.vue'
@@ -61,7 +61,7 @@ const props = defineProps<{
const { t } = useI18n()
const { isReady, vmsByHost } = useVmStore().subscribe()
const { areVmsReady, vmsByHost } = useXoVmCollection()
const definitions = computed(() =>
defineTree(vmsByHost.value.get(props.host.id) ?? [], {

View File

@@ -1,5 +1,5 @@
<template>
<VtsLoadingHero v-if="!isReady" type="page" />
<VtsLoadingHero v-if="!arePoolsReady" type="page" />
<VtsObjectNotFoundHero v-else-if="!pool" :id="route.params.id" type="page" />
<RouterView v-else v-slot="{ Component }">
<PoolHeader :pool />
@@ -9,19 +9,18 @@
<script lang="ts" setup>
import PoolHeader from '@/components/pool/PoolHeader.vue'
import { usePoolStore } from '@/stores/xo-rest-api/pool.store'
import { useXoPoolCollection } from '@/remote-resources/use-xo-pool-collection.ts'
import type { XoPool } from '@/types/xo/pool.type'
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
import VtsObjectNotFoundHero from '@core/components/state-hero/VtsObjectNotFoundHero.vue'
import { useDefaultTab } from '@core/composables/default-tab.composable.ts'
import { computed } from 'vue'
import { useRoute } from 'vue-router'
useDefaultTab('/pool/[id]', 'dashboard')
const route = useRoute<'/pool/[id]'>()
const { isReady, get } = usePoolStore().subscribe()
const { arePoolsReady, useGetPoolById } = useXoPoolCollection()
const pool = computed(() => get(route.params.id as XoPool['id']))
const pool = useGetPoolById(() => route.params.id as XoPool['id'])
</script>

View File

@@ -36,7 +36,7 @@ import PoolDashboardRamUsage from '@/components/pool/dashboard/PoolDashboardRamU
import PoolDashboardStatus from '@/components/pool/dashboard/PoolDashboardStatus.vue'
import PoolDashboardStoragesUsage from '@/components/pool/dashboard/PoolDashboardStoragesUsage.vue'
import { useFetchStats } from '@/composables/fetch-stats.composable.ts'
import { usePoolDashboard } from '@/requests/use-pool-dashboard.request.ts'
import { useXoPoolDashboard } from '@/remote-resources/use-xo-pool-dashboard.ts'
import type { XoPool } from '@/types/xo/pool.type'
import { GRANULARITY } from '@/utils/rest-api-stats.ts'
import { useUiStore } from '@core/stores/ui.store.ts'
@@ -45,7 +45,7 @@ const { pool } = defineProps<{ pool: XoPool }>()
const { data, isFetching, error } = useFetchStats('pool', () => pool.id, GRANULARITY.Hours)
const { dashboard: poolDashboard } = usePoolDashboard(() => pool.id)
const { poolDashboard } = useXoPoolDashboard({}, () => pool.id)
const uiStore = useUiStore()
</script>

View File

@@ -1,10 +1,10 @@
<template>
<VtsLoadingHero v-if="!isReady" type="page" />
<VtsLoadingHero v-if="!areHostsReady" type="page" />
<UiCard v-else class="hosts">
<div class="pagination-container">
<!-- TODO: update with item selection button when available -->
<p class="typo-body-regular-small count">{{ t('n-hosts', { n: hosts.length }) }}</p>
<UiTablePagination v-if="isReady" v-bind="paginationBindings" />
<UiTablePagination v-if="areHostsReady" v-bind="paginationBindings" />
</div>
<VtsTable vertical-border>
<thead>
@@ -34,13 +34,13 @@
<div class="pagination-container">
<!-- TODO: update with item selection button when available -->
<p class="typo-body-regular-small count">{{ t('n-hosts', { n: hosts.length }) }}</p>
<UiTablePagination v-if="isReady" v-bind="paginationBindings" />
<UiTablePagination v-if="areHostsReady" v-bind="paginationBindings" />
</div>
</UiCard>
</template>
<script lang="ts" setup>
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import type { XoPool } from '@/types/xo/pool.type'
import type { HostState } from '@core/types/object-icon.type'
import VtsCellObject from '@core/components/cell-object/VtsCellObject.vue'
@@ -65,7 +65,7 @@ const props = defineProps<{
const { t } = useI18n()
const { isReady, hostsByPool } = useHostStore().subscribe()
const { areHostsReady, hostsByPool } = useXoHostCollection()
const definitions = computed(() =>
defineTree(hostsByPool.value.get(props.pool.id) ?? [], {

View File

@@ -15,7 +15,7 @@
import PoolHostInternalNetworksTable from '@/components/pool/network/PoolHostInternalNetworksTable.vue'
import PoolNetworkSidePanel from '@/components/pool/network/PoolNetworkSidePanel.vue'
import PoolNetworksTable from '@/components/pool/network/PoolNetworksTable.vue'
import { useNetworkStore } from '@/stores/xo-rest-api/network.store'
import { useXoNetworkCollection } from '@/remote-resources/use-xo-network-collection.ts'
import type { XoNetwork } from '@/types/xo/network.type'
import type { XoPool } from '@/types/xo/pool.type'
import VtsNoSelectionHero from '@core/components/state-hero/VtsNoSelectionHero.vue'
@@ -29,7 +29,7 @@ const { pool } = defineProps<{
pool: XoPool
}>()
const { networksWithoutPifs, networksWithPifs, get } = useNetworkStore().subscribe()
const { networksWithoutPifs, networksWithPifs, getNetworkById } = useXoNetworkCollection()
const uiStore = useUiStore()
const internalNetworks = computed(() => networksWithoutPifs.value.filter(network => network.$pool === pool.id))
@@ -37,7 +37,7 @@ const internalNetworks = computed(() => networksWithoutPifs.value.filter(network
const networks = computed(() => networksWithPifs.value.filter(network => network.$pool === pool.id))
const selectedNetwork = useRouteQuery<XoNetwork | undefined>('id', {
toData: id => get(id as XoNetwork['id']),
toData: id => getNetworkById(id as XoNetwork['id']),
toQuery: network => network?.id ?? '',
})
</script>

View File

@@ -19,10 +19,10 @@ import PoolGeneralInfo from '@/components/pool/system/PoolGeneralInfo.vue'
import PoolManagement from '@/components/pool/system/PoolManagement.vue'
import PoolNetworking from '@/components/pool/system/PoolNetworking.vue'
import PoolStorageConfiguration from '@/components/pool/system/PoolStorageConfiguration.vue'
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { useNetworkStore } from '@/stores/xo-rest-api/network.store'
import { useServerStore } from '@/stores/xo-rest-api/server.store'
import { useSrStore } from '@/stores/xo-rest-api/sr.store'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import { useXoNetworkCollection } from '@/remote-resources/use-xo-network-collection.ts'
import { useXoServerCollection } from '@/remote-resources/use-xo-server-collection.ts'
import { useXoSrCollection } from '@/remote-resources/use-xo-sr-collection.ts'
import type { XoPool } from '@/types/xo/pool.type'
import VtsColumn from '@core/components/column/VtsColumn.vue'
import VtsColumns from '@core/components/columns/VtsColumns.vue'
@@ -31,10 +31,10 @@ import { logicAnd } from '@vueuse/math'
defineProps<{ pool: XoPool }>()
const { isReady: isServersReady } = useServerStore().subscribe()
const { isReady: isHostsReady } = useHostStore().subscribe()
const { isReady: isNetworksReady } = useNetworkStore().subscribe()
const { isReady: isSrsReady } = useSrStore().subscribe()
const { areServersReady } = useXoServerCollection()
const { areHostsReady } = useXoHostCollection()
const { areNetworksReady } = useXoNetworkCollection()
const { areSrsReady } = useXoSrCollection()
const isReady = logicAnd(isServersReady, isHostsReady, isNetworksReady, isSrsReady)
const isReady = logicAnd(areServersReady, areHostsReady, areNetworksReady, areSrsReady)
</script>

View File

@@ -1,10 +1,10 @@
<template>
<VtsLoadingHero v-if="!isReady" type="page" />
<VtsLoadingHero v-if="!areVmsReady" type="page" />
<UiCard v-else class="vms">
<div class="pagination-container">
<!-- TODO: update with item selection button when available -->
<p class="typo-body-regular-small count">{{ t('n-vms', { n: vms.length }) }}</p>
<UiTablePagination v-if="isReady" v-bind="paginationBindings" />
<UiTablePagination v-if="areVmsReady" v-bind="paginationBindings" />
</div>
<VtsTable vertical-border>
<thead>
@@ -32,13 +32,13 @@
<div class="pagination-container">
<!-- TODO: update with item selection button when available -->
<p class="typo-body-regular-small count">{{ t('n-vms', { n: vms.length }) }}</p>
<UiTablePagination v-if="isReady" v-bind="paginationBindings" />
<UiTablePagination v-if="areVmsReady" v-bind="paginationBindings" />
</div>
</UiCard>
</template>
<script lang="ts" setup>
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
import { useXoVmCollection } from '@/remote-resources/use-xo-vm-collection.ts'
import type { XoPool } from '@/types/xo/pool.type'
import type { VmState } from '@core/types/object-icon.type'
import VtsCellObject from '@core/components/cell-object/VtsCellObject.vue'
@@ -57,18 +57,18 @@ import { faAlignLeft, faDesktop } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const props = defineProps<{
const { pool } = defineProps<{
pool: XoPool
}>()
const { t } = useI18n()
const { isReady, records } = useVmStore().subscribe()
const { areVmsReady, vmsByPool } = useXoVmCollection()
const vmsByPool = computed(() => records.value.filter(vm => vm.$pool === props.pool.id))
const poolVms = computed(() => vmsByPool.value.get(pool.id) ?? [])
const definitions = computed(() =>
defineTree(vmsByPool.value ?? [], {
defineTree(poolVms.value, {
getLabel: 'name_label',
})
)

View File

@@ -1,5 +1,5 @@
<template>
<VtsLoadingHero v-if="!isReady" type="page" />
<VtsLoadingHero v-if="!areVmsReady" type="page" />
<VtsObjectNotFoundHero v-else-if="!vm" :id="route.params.id" type="page" />
<RouterView v-else v-slot="{ Component }">
<VmHeader v-if="uiStore.hasUi" :vm />
@@ -9,21 +9,20 @@
<script lang="ts" setup>
import VmHeader from '@/components/vm/VmHeader.vue'
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
import { useXoVmCollection } from '@/remote-resources/use-xo-vm-collection.ts'
import type { XoVm } from '@/types/xo/vm.type'
import VtsLoadingHero from '@core/components/state-hero/VtsLoadingHero.vue'
import VtsObjectNotFoundHero from '@core/components/state-hero/VtsObjectNotFoundHero.vue'
import { useDefaultTab } from '@core/composables/default-tab.composable.ts'
import { useUiStore } from '@core/stores/ui.store'
import { computed } from 'vue'
import { useRoute } from 'vue-router'
useDefaultTab('/vm/[id]', 'dashboard')
const route = useRoute<'/vm/[id]'>()
const { isReady, get: getVm } = useVmStore().subscribe()
const { areVmsReady, useGetVmById } = useXoVmCollection()
const uiStore = useUiStore()
const vm = computed(() => getVm(route.params.id as XoVm['id']))
const vm = useGetVmById(() => route.params.id as XoVm['id'])
</script>

View File

@@ -11,9 +11,9 @@
</template>
<script lang="ts" setup>
import { useVmStore } from '@/stores/xo-rest-api/vm.store'
import type { XoVm } from '@/types/xo/vm.type'
import { VM_OPERATION } from '@/types/xo/vm.type'
import { isVmOperatingPending } from '@/utils/xo-records/vm.util.ts'
import VtsActionsConsole from '@core/components/console/VtsActionsConsole.vue'
import VtsClipboardConsole from '@core/components/console/VtsClipboardConsole.vue'
import VtsLayoutConsole from '@core/components/console/VtsLayoutConsole.vue'
@@ -28,8 +28,6 @@ const props = defineProps<{
const { t } = useI18n()
const { isVmOperatingPending } = useVmStore().subscribe()
const STOP_OPERATIONS = [
VM_OPERATION.SHUTDOWN,
VM_OPERATION.CLEAN_SHUTDOWN,

View File

@@ -13,7 +13,7 @@
<script setup lang="ts">
import VmVifSidePanel from '@/components/vm/network/VmVifSidePanel.vue'
import VmVifsTable from '@/components/vm/network/VmVifsTable.vue'
import { useVifStore } from '@/stores/xo-rest-api/vif.store.ts'
import { useXoVifCollection } from '@/remote-resources/use-xo-vif-collection.ts'
import type { XoVif } from '@/types/xo/vif.type.ts'
import VtsNoSelectionHero from '@core/components/state-hero/VtsNoSelectionHero.vue'
import UiCard from '@core/components/ui/card/UiCard.vue'
@@ -23,15 +23,15 @@ import { useUiStore } from '@core/stores/ui.store.ts'
import { useArrayFilter } from '@vueuse/shared'
import { useRoute } from 'vue-router/auto'
const { records } = useVifStore().subscribe()
const { vifs: rawVifs, getVifById } = useXoVifCollection()
const uiStore = useUiStore()
const route = useRoute<'/vm/[id]'>()
const vifs = useArrayFilter(records, vif => vif.$VM === route.params.id)
const vifs = useArrayFilter(rawVifs, vif => vif.$VM === route.params.id)
const selectedVif = useRouteQuery<XoVif | undefined>('id', {
toData: id => vifs.value.find(vif => vif.id === id),
toData: id => getVifById(id as XoVif['id']),
toQuery: vif => vif?.id ?? '',
})
</script>

View File

@@ -327,15 +327,15 @@
import NetworkSelect from '@/components/select/NetworkSelect.vue'
import SrSelect from '@/components/select/SrSelect.vue'
import { createVM } from '@/jobs/vm-create.job.ts'
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import { useNetworkStore } from '@/stores/xo-rest-api/network.store'
import { usePifStore } from '@/stores/xo-rest-api/pif.store'
import { usePoolStore } from '@/stores/xo-rest-api/pool.store'
import { useSrStore } from '@/stores/xo-rest-api/sr.store'
import { useVbdStore } from '@/stores/xo-rest-api/vbd.store'
import { useVdiStore } from '@/stores/xo-rest-api/vdi.store'
import { useVifStore } from '@/stores/xo-rest-api/vif.store'
import { useVmTemplateStore } from '@/stores/xo-rest-api/vm-template.store'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import { useXoNetworkCollection } from '@/remote-resources/use-xo-network-collection.ts'
import { useXoPifCollection } from '@/remote-resources/use-xo-pif-collection.ts'
import { useXoPoolCollection } from '@/remote-resources/use-xo-pool-collection.ts'
import { useXoSrCollection } from '@/remote-resources/use-xo-sr-collection.ts'
import { useXoVbdCollection } from '@/remote-resources/use-xo-vbd-collection.ts'
import { useXoVdiCollection } from '@/remote-resources/use-xo-vdi-collection.ts'
import { useXoVifCollection } from '@/remote-resources/use-xo-vif-collection.ts'
import { useXoVmTemplateCollection } from '@/remote-resources/use-xo-vm-template-collection.ts'
import type { XoNetwork } from '@/types/xo/network.type.ts'
import type { Vdi, Vif, VifToSend, VmState } from '@/types/xo/new-vm.type'
import type { XoPool } from '@/types/xo/pool.type.ts'
@@ -387,15 +387,15 @@ const isOpen = ref(false)
const isBusy = ref(false)
const { records: networks, get: getNetwork } = useNetworkStore().subscribe()
const { getPifsByNetworkId } = usePifStore().subscribe()
const { records: pools } = usePoolStore().subscribe()
const { vmsTemplatesByPool } = useVmTemplateStore().subscribe()
const { records: srs, vdiIsosBySrName } = useSrStore().subscribe()
const { get: getVbd } = useVbdStore().subscribe()
const { get: getVdi } = useVdiStore().subscribe()
const { get: getVif } = useVifStore().subscribe()
const { hostsByPool } = useHostStore().subscribe()
const { networks, getNetworkById } = useXoNetworkCollection()
const { getPifsByNetworkId } = useXoPifCollection()
const { pools } = useXoPoolCollection()
const { srs, vdiIsosBySrName } = useXoSrCollection()
const { getVbdById } = useXoVbdCollection()
const { getVdiById } = useXoVdiCollection()
const { getVifById } = useXoVifCollection()
const { hostsByPool } = useXoHostCollection()
const { vmsTemplatesByPool } = useXoVmTemplateCollection()
const vmState = reactive<VmState>({
name: '',
@@ -529,13 +529,13 @@ const getVmTemplateVdis = (template: XoVmTemplate) =>
const getExistingVdis = (template: XoVmTemplate) => {
return template.$VBDs.reduce<Vdi[]>((acc, vbdId) => {
const vbd = getVbd(vbdId)
const vbd = getVbdById(vbdId)
if (vbd === undefined || vbd.is_cd_drive) {
return acc
}
const vdi = getVdi(vbd.VDI)
const vdi = getVdiById(vbd.VDI)
if (vdi === undefined) {
console.error('VDI not found')
@@ -584,14 +584,14 @@ const getExistingVifs = (template: XoVmTemplate): Vif[] => {
return []
}
return template.VIFs.reduce<Vif[]>((acc, vifId) => {
const vif = getVif(vifId)
const vif = getVifById(vifId)
if (vif === undefined) {
console.error('VIF not found')
return acc
}
const network = getNetwork(vif.$network)
const network = getNetworkById(vif.$network)
if (network === undefined) {
console.error('Network not found')

View File

@@ -0,0 +1,18 @@
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
import type { XoAlarm } from '@/types/xo/alarm.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
import { createDateSorter } from '@core/utils/date-sorter.utils.ts'
import { useSorted } from '@vueuse/core'
export const useXoAlarmCollection = defineRemoteResource({
url: '/rest/v0/alarms?fields=id,time,body,object',
initialData: () => [] as XoAlarm[],
state: (rawAlarms, context) => {
const alarms = useSorted(rawAlarms, createDateSorter('time'))
return useXoCollectionState(alarms, {
context,
baseName: 'alarm',
})
},
})

View File

@@ -0,0 +1,60 @@
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
import { useXoPoolCollection } from '@/remote-resources/use-xo-pool-collection.ts'
import { type XoHost } from '@/types/xo/host.type.ts'
import type { XoPool } from '@/types/xo/pool.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
import { sortByNameLabel } from '@core/utils/sort-by-name-label.util.ts'
import { useSorted } from '@vueuse/core'
import { computed } from 'vue'
export const useXoHostCollection = defineRemoteResource({
url: '/rest/v0/hosts?fields=id,name_label,name_description,power_state,controlDomain,residentVms,$pool,current_operations,address,startTime,version,bios_strings,cpus,CPUs,memory,tags,iscsiIqn,powerOnMode,build,otherConfig,multipathing,logging,enabled,agentStartTime,PGPUs',
initialData: () => [] as XoHost[],
state: (rawHosts, context) => {
const hosts = useSorted(rawHosts, sortByNameLabel)
const state = useXoCollectionState(hosts, {
context,
baseName: 'host',
})
const { pools, getPoolById } = useXoPoolCollection(context)
const hostsByPool = computed(() => {
const hostsByPoolMap = new Map<XoPool['id'], XoHost[]>()
hosts.value.forEach(host => {
const poolId = host.$pool
if (!hostsByPoolMap.has(poolId)) {
hostsByPoolMap.set(poolId, [])
}
hostsByPoolMap.get(poolId)!.push(host)
})
return hostsByPoolMap
})
function isMasterHost(hostId: XoHost['id']) {
return !!pools.value.find(pool => pool.master === hostId)
}
function getMasterHostByPoolId(poolId: XoPool['id']) {
const masterHostId = getPoolById(poolId)?.master
if (masterHostId === undefined) {
return
}
return state.getHostById(masterHostId)
}
return {
...state,
hostsByPool,
isMasterHost,
getMasterHostByPoolId,
}
},
})

View File

@@ -0,0 +1,34 @@
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
import { useXoPifCollection } from '@/remote-resources/use-xo-pif-collection.ts'
import type { XoNetwork } from '@/types/xo/network.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
import { sortByNameLabel } from '@core/utils/sort-by-name-label.util.ts'
import { useSorted } from '@vueuse/core'
import { computed } from 'vue'
export const useXoNetworkCollection = defineRemoteResource({
url: '/rest/v0/networks?fields=id,defaultIsLocked,name_label,nbd,tags,$pool,name_description,MTU,PIFs,other_config',
initialData: () => [] as XoNetwork[],
state: (rawNetworks, context) => {
const { hostMasterPifsByNetwork } = useXoPifCollection(context)
const networks = useSorted(rawNetworks, sortByNameLabel)
const networksWithPifs = computed(() => {
const networkIds = Array.from(hostMasterPifsByNetwork.value.keys())
return networks.value.filter(network => networkIds.includes(network.id) && network.PIFs.length > 0)
})
const networksWithoutPifs = computed(() => networks.value.filter(network => network.PIFs.length === 0))
return {
...useXoCollectionState(networks, {
context,
baseName: 'network',
}),
networksWithPifs,
networksWithoutPifs,
}
},
})

View File

@@ -0,0 +1,13 @@
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
import type { XoPci } from '@/types/xo/pci.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
export const useXoPciCollection = defineRemoteResource({
url: '/rest/v0/pcis?fields=id,device_name',
initialData: () => [] as XoPci[],
state: (pcis, context) =>
useXoCollectionState(pcis, {
context,
baseName: 'pci',
}),
})

View File

@@ -0,0 +1,13 @@
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
import type { XoPgpu } from '@/types/xo/pgpu.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
export const useXoPgpuCollection = defineRemoteResource({
url: '/rest/v0/pgpus?fields=id,pci',
initialData: () => [] as XoPgpu[],
state: (pgpus, context) =>
useXoCollectionState(pgpus, {
context,
baseName: 'pgpu',
}),
})

View File

@@ -0,0 +1,67 @@
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import type { XoHost } from '@/types/xo/host.type.ts'
import type { XoNetwork } from '@/types/xo/network.type.ts'
import type { XoPif } from '@/types/xo/pif.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
import { computed } from 'vue'
export const useXoPifCollection = defineRemoteResource({
url: '/rest/v0/pifs?fields=$host,$network,attached,carrier,device,dns,gateway,id,ip,ipv6,mac,management,mode,mtu,netmask,speed,vlan,isBondMaster,bondSlaves',
initialData: () => [] as XoPif[],
state: (pifs, context) => {
const state = useXoCollectionState(pifs, {
context,
baseName: 'pif',
})
const { isMasterHost } = useXoHostCollection(context)
const hostMasterPifsByNetwork = computed(() => {
const hostMasterPifsByNetworkMap = new Map<XoNetwork['id'], XoPif[]>()
pifs.value
.filter(pif => isMasterHost(pif.$host))
.forEach(pif => {
const networkId = pif.$network
if (!hostMasterPifsByNetworkMap.has(networkId)) {
hostMasterPifsByNetworkMap.set(networkId, [])
}
hostMasterPifsByNetworkMap.get(networkId)?.push(pif)
})
return hostMasterPifsByNetworkMap
})
function getPifsByNetworkId(networkId: XoNetwork['id']) {
return pifs.value.filter(pif => pif.$network === networkId)
}
const pifsByHost = computed(() => {
const pifsByHostMap = new Map<XoHost['id'], XoPif[]>()
pifs.value.forEach(pif => {
const hostId = pif.$host
if (!pifsByHostMap.has(hostId)) {
pifsByHostMap.set(hostId, [])
}
pifsByHostMap.get(hostId)!.push(pif)
})
return pifsByHostMap
})
function getBondsDevices(pif: XoPif) {
return pif.isBondMaster ? pif.bondSlaves.flatMap(slaveId => state.getPifById(slaveId)?.device ?? []) : []
}
return {
...state,
hostMasterPifsByNetwork,
pifsByHost,
getPifsByNetworkId,
getBondsDevices,
}
},
})

View File

@@ -0,0 +1,18 @@
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
import type { XoPool } from '@/types/xo/pool.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
import { sortByNameLabel } from '@core/utils/sort-by-name-label.util.ts'
import { useSorted } from '@vueuse/core'
export const useXoPoolCollection = defineRemoteResource({
url: '/rest/v0/pools?fields=id,name_label,master,default_SR,tags,otherConfig,auto_poweron,HA_enabled,migrationCompression,suspendSr,crashDumpSr,haSrs',
initialData: () => [] as XoPool[],
state: (rawPools, context) => {
const pools = useSorted(rawPools, sortByNameLabel)
return useXoCollectionState(pools, {
context,
baseName: 'pool',
})
},
})

View File

@@ -0,0 +1,9 @@
import type { XoPoolDashboard } from '@/types/xo/pool-dashboard.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
import type { Ref } from 'vue'
export const useXoPoolDashboard = defineRemoteResource({
url: (poolId: string) => `/rest/v0/pools/${poolId}/dashboard?ndjson=true`,
stream: true,
state: (poolDashboard: Ref<XoPoolDashboard | undefined>) => ({ poolDashboard }),
})

View File

@@ -0,0 +1,38 @@
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
import type { XoPool } from '@/types/xo/pool.type.ts'
import type { XoServer } from '@/types/xo/server.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
import { computed } from 'vue'
export const useXoServerCollection = defineRemoteResource({
url: '/rest/v0/servers?fields=host,httpProxy,username,readOnly,allowUnauthorized,label,poolId,poolNameLabel,id,status,master,error,poolNameDescription',
initialData: () => [] as XoServer[],
state: (servers, context) => {
const serverByPool = computed(() => {
const serverByPoolMap = new Map<XoPool['id'], XoServer[]>()
servers.value.forEach(server => {
const poolId = server.poolId
if (!poolId) {
return
}
if (!serverByPoolMap.has(poolId)) {
serverByPoolMap.set(poolId, [])
}
serverByPoolMap.get(poolId)!.push(server)
})
return serverByPoolMap
})
return {
...useXoCollectionState(servers, {
context,
baseName: 'server',
}),
serverByPool,
}
},
})

View File

@@ -0,0 +1,63 @@
import type { XoDashboard } from '@/types/xo/dashboard.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
import { formatSizeRaw } from '@core/utils/size.util.ts'
import type { Info, Scale } from 'human-format'
import { computed } from 'vue'
export const useXoSiteDashboard = defineRemoteResource({
url: '/rest/v0/dashboard?ndjson=true',
stream: true,
initialData: () => ({}) as XoDashboard,
state: (dashboard, context) => {
const backupRepositories = computed<BackupRepositories | undefined>(() => {
if (dashboard.value?.backupRepositories === undefined) {
return
}
return {
available: formatSizeRaw(dashboard.value.backupRepositories.other.size.available, 1),
backups: formatSizeRaw(dashboard.value.backupRepositories.other.size.backups, 1),
other: formatSizeRaw(dashboard.value.backupRepositories.other.size.other ?? 0, 1),
total: formatSizeRaw(dashboard.value.backupRepositories.other.size.total, 1),
used: formatSizeRaw(dashboard.value.backupRepositories.other.size.used ?? 0, 1),
}
})
const storageRepositories = computed<StorageRepositories | undefined>(() => {
if (dashboard.value?.storageRepositories === undefined) {
return
}
return {
total: formatSizeRaw(dashboard.value.storageRepositories.size.total, 1),
used: formatSizeRaw(dashboard.value.storageRepositories.size.used, 1),
available: formatSizeRaw(dashboard.value.storageRepositories.size.available, 1),
replicated: formatSizeRaw(dashboard.value.storageRepositories.size.replicated, 1),
other: formatSizeRaw(dashboard.value.storageRepositories.size.other, 1),
}
})
return {
dashboard,
isDashboardReady: context.isReady,
backupRepositories,
storageRepositories,
}
},
})
export type BackupRepositories = {
available: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
backups: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
other: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
total: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
used: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
}
export type StorageRepositories = {
total: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
used: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
available: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
replicated: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
other: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
}

View File

@@ -0,0 +1,54 @@
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
import { useXoVdiCollection } from '@/remote-resources/use-xo-vdi-collection.ts'
import type { XoSr } from '@/types/xo/sr.type.ts'
import type { XoVdi } from '@/types/xo/vdi.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
import { computed } from 'vue'
export const useXoSrCollection = defineRemoteResource({
url: '/rest/v0/srs?fields=id,name_label,name_description,$pool,content_type,physical_usage,size,SR_type,VDIs',
initialData: () => [] as XoSr[],
state: (srs, context) => {
const { getVdiById } = useXoVdiCollection(context)
const state = useXoCollectionState(srs, {
context,
baseName: 'sr',
})
const isoVdiIds = computed(() =>
srs.value.reduce((acc, sr) => {
if (sr.VDIs) {
sr.VDIs.forEach(vdiId => acc.add(vdiId))
}
return acc
}, new Set<XoVdi['id']>())
)
const vdiIsosBySrName = computed(() => {
const groupedVDIs: Record<string, XoVdi[]> = {}
isoVdiIds.value.forEach(vdiId => {
const vdi = getVdiById(vdiId)
if (vdi) {
const srName = state.getSrById(vdi.$SR)?.name_label ?? 'Unknown SR'
if (!groupedVDIs[srName]) {
groupedVDIs[srName] = []
}
groupedVDIs[srName].push(vdi)
}
})
return groupedVDIs
})
return {
...state,
vdiIsosBySrName,
}
},
})

View File

@@ -0,0 +1,26 @@
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
import type { XoTask } from '@/types/xo/task.type.ts'
import { convertTaskToCore } from '@/utils/convert-task-to-core.util.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
import { computed } from 'vue'
const ONE_DAY = 24 * 60 * 60 * 1000
export const useXoTaskCollection = defineRemoteResource({
url: '/rest/v0/tasks?fields=id,start,end,properties,status,progress,tasks',
initialData: () => [] as XoTask[],
state: (tasks, context) => {
const lastDayTasks = computed(() => {
const now = Date.now()
return tasks.value.filter(task => now - task.start < ONE_DAY).map(task => convertTaskToCore(task))
})
return {
...useXoCollectionState(tasks, {
context,
baseName: 'task',
}),
lastDayTasks,
}
},
})

View File

@@ -0,0 +1,13 @@
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
import type { XoVbd } from '@/types/xo/vbd.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
export const useXoVbdCollection = defineRemoteResource({
url: '/rest/v0/vbds?fields=id,name_label,name_description,VDI,is_cd_drive,position',
initialData: () => [] as XoVbd[],
state: (vbds, context) =>
useXoCollectionState(vbds, {
context,
baseName: 'vbd',
}),
})

View File

@@ -0,0 +1,13 @@
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
import type { XoVdi } from '@/types/xo/vdi.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
export const useXoVdiCollection = defineRemoteResource({
url: '/rest/v0/vdis?fields=id,name_label,name_description,$VBDs,$SR,size,$pool',
initialData: () => [] as XoVdi[],
state: (vdis, context) =>
useXoCollectionState(vdis, {
context,
baseName: 'vdi',
}),
})

View File

@@ -0,0 +1,13 @@
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
import type { XoVif } from '@/types/xo/vif.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
export const useXoVifCollection = defineRemoteResource({
url: '/rest/v0/vifs?fields=$VM,$network,attached,device,txChecksumming,id,lockingMode,MAC,MTU',
initialData: () => [] as XoVif[],
state: (vifs, context) =>
useXoCollectionState(vifs, {
context,
baseName: 'vif',
}),
})

View File

@@ -0,0 +1,85 @@
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
import { useXoHostCollection } from '@/remote-resources/use-xo-host-collection.ts'
import type { XoHost } from '@/types/xo/host.type.ts'
import type { XoPool } from '@/types/xo/pool.type.ts'
import { VM_POWER_STATE, type XoVm } from '@/types/xo/vm.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
import { sortByNameLabel } from '@core/utils/sort-by-name-label.util.ts'
import { useSorted } from '@vueuse/core'
import { computed } from 'vue'
export const useXoVmCollection = defineRemoteResource({
url: '/rest/v0/vms?fields=id,name_label,name_description,power_state,$container,$pool,other,current_operations,CPUs,addresses,tags,os_version,virtualizationMode,secureBoot,VTPMs,viridian,isNestedVirtEnabled,memory,VGPUs,high_availability,auto_poweron,startDelay,vga,videoram,pvDriversVersion,cpuWeight,cpuCap,cpuMask,coresPerSocket,mainIpAddress,nicType,affinityHost,suspendSr,blockedOperations,hasVendorDevice,startTime,installTime,pvDriversDetected',
initialData: () => [] as XoVm[],
state: (rawVms, context) => {
const { getHostById } = useXoHostCollection(context)
const vms = useSorted(rawVms, sortByNameLabel)
const runningVms = computed(() => vms.value.filter(vm => vm.power_state === VM_POWER_STATE.RUNNING))
const vmsByHost = computed(() => createVmsByHostMap(vms.value, false))
const hostLessVmsByPool = computed(() => createVmsByHostMap(vms.value, true))
const vmsByPool = computed(() => {
return vms.value.reduce((acc, vm) => {
if (!acc.has(vm.$pool)) {
acc.set(vm.$pool, [])
}
acc.get(vm.$pool)!.push(vm)
return acc
}, new Map<XoPool['id'], XoVm[]>())
})
function getVmHost(vm: XoVm): XoHost | undefined {
const vmHostId = extractVmHostId(vm)
if (vmHostId === undefined) {
return undefined
}
return getHostById(vmHostId)
}
return {
...useXoCollectionState(vms, {
context,
baseName: 'vm',
}),
runningVms,
vmsByHost,
vmsByPool,
getVmHost,
hostLessVmsByPool,
}
},
})
function createVmsByHostMap<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
}
function extractVmHostId(vm: XoVm) {
return vm.$container === vm.$pool ? undefined : (vm.$container as XoHost['id'])
}

View File

@@ -0,0 +1,13 @@
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
import type { XoVmController } from '@/types/xo/vm-controller.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
export const useXoVmControllerCollection = defineRemoteResource({
url: '/rest/v0/vm-controllers?fields=id,name_label,power_state,memory,$container',
initialData: () => [] as XoVmController[],
state: (vmControllers, context) =>
useXoCollectionState(vmControllers, {
context,
baseName: 'vmController',
}),
})

View File

@@ -0,0 +1,39 @@
import { useXoCollectionState } from '@/composables/xo-collection-state/use-xo-collection-state.ts'
import type { XoPool } from '@/types/xo/pool.type.ts'
import type { XoVmTemplate } from '@/types/xo/vm-template.type.ts'
import { defineRemoteResource } from '@core/packages/remote-resource/define-remote-resource.ts'
import { sortByNameLabel } from '@core/utils/sort-by-name-label.util.ts'
import { useSorted } from '@vueuse/core'
import { computed } from 'vue'
export const useXoVmTemplateCollection = defineRemoteResource({
url: '/rest/v0/vm-templates?fields=id,uuid,name_label,name_description,$pool,template_info,VIFs,$VBDs,boot,CPUs,memory,tags,isDefaultTemplate',
initialData: () => [] as XoVmTemplate[],
state: (rawTemplates, context) => {
const templates = useSorted(rawTemplates, sortByNameLabel)
const vmsTemplatesByPool = computed(() => {
const vmTemplatesByPoolMap = new Map<XoPool['id'], XoVmTemplate[]>()
templates.value.forEach(template => {
const poolId = template.$pool
if (!vmTemplatesByPoolMap.has(poolId)) {
vmTemplatesByPoolMap.set(poolId, [])
}
vmTemplatesByPoolMap.get(poolId)!.push(template)
})
return vmTemplatesByPoolMap
})
return {
...useXoCollectionState(templates, {
context,
baseName: 'template',
}),
vmsTemplatesByPool,
}
},
})

View File

@@ -1,17 +0,0 @@
import type { XoPoolDashboard } from '@/types/xo/pool-dashboard.type.ts'
import { defineRequest } from '@core/packages/request/define-request.ts'
import { merge } from 'lodash-es'
import { ref } from 'vue'
export const usePoolDashboard = defineRequest({
url: (poolId: string) => `/rest/v0/pools/${poolId}/dashboard?fields=*&ndjson=true`,
state: () => ({
dashboard: ref({} as XoPoolDashboard),
}),
onUrlChange: ({ dashboard }) => {
dashboard.value = {} as XoPoolDashboard
},
onDataReceived: ({ dashboard }, data) => {
merge(dashboard.value, data)
},
})

View File

@@ -1,68 +0,0 @@
import type { XoDashboard } from '@/types/xo/dashboard.type.ts'
import { defineRequest } from '@core/packages/request/define-request.ts'
import { formatSizeRaw } from '@core/utils/size.util.ts'
import type { Info, Scale } from 'human-format'
import { merge } from 'lodash-es'
import { computed, ref } from 'vue'
export const useSiteDashboard = defineRequest({
url: '/rest/v0/dashboard?fields=*&ndjson=true',
state: buildState,
onDataReceived: ({ dashboard }, data) => {
merge(dashboard.value, data)
},
})
export type BackupRepositories = {
available: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
backups: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
other: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
total: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
used: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
}
export type StorageRepositories = {
total: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
used: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
available: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
replicated: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
other: Info<Scale<'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB'>>
}
function buildState() {
const dashboard = ref({} as XoDashboard)
const backupRepositories = computed<BackupRepositories | undefined>(() => {
if (dashboard.value.backupRepositories === undefined) {
return
}
return {
available: formatSizeRaw(dashboard.value.backupRepositories.other.size.available, 1),
backups: formatSizeRaw(dashboard.value.backupRepositories.other.size.backups, 1),
other: formatSizeRaw(dashboard.value.backupRepositories.other.size.other ?? 0, 1),
total: formatSizeRaw(dashboard.value.backupRepositories.other.size.total, 1),
used: formatSizeRaw(dashboard.value.backupRepositories.other.size.used ?? 0, 1),
}
})
const storageRepositories = computed<StorageRepositories | undefined>(() => {
if (dashboard.value.storageRepositories === undefined) {
return
}
return {
total: formatSizeRaw(dashboard.value.storageRepositories.size.total, 1),
used: formatSizeRaw(dashboard.value.storageRepositories.size.used, 1),
available: formatSizeRaw(dashboard.value.storageRepositories.size.available, 1),
replicated: formatSizeRaw(dashboard.value.storageRepositories.size.replicated, 1),
other: formatSizeRaw(dashboard.value.storageRepositories.size.other, 1),
}
})
return {
dashboard,
backupRepositories,
storageRepositories,
}
}

View File

@@ -1,13 +0,0 @@
import type { XoAlarm } from '@/types/xo/alarm.type.ts'
import { createXoStoreConfig } from '@/utils/create-xo-store-config.util'
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
import { createDateSorter } from '@core/utils/date-sorter.utils.ts'
import { defineStore } from 'pinia'
export const useAlarmStore = defineStore('alarm', () => {
const config = createXoStoreConfig('alarm', {
sortBy: createDateSorter<XoAlarm>('time'),
})
return createSubscribableStoreContext(config, {})
})

View File

@@ -1,65 +0,0 @@
import { usePoolStore } from '@/stores/xo-rest-api/pool.store'
import { HOST_OPERATION, 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'
import { castArray } from 'lodash-es'
import { defineStore } from 'pinia'
import { computed } from 'vue'
export const useHostStore = defineStore('host', () => {
const deps = {
poolStore: usePoolStore(),
}
const poolContext = deps.poolStore.getContext()
const { context: baseContext, ...configRest } = createXoStoreConfig('host', {
sortBy: sortByNameLabel,
})
const isMasterHost = (hostId: XoHost['id']) => !!poolContext.records.value.find(pool => pool.master === hostId)
const hostsByPool = computed(() => {
const hostsByPoolMap = new Map<XoPool['id'], XoHost[]>()
baseContext.records.value.forEach(host => {
const poolId = host.$pool
if (!hostsByPoolMap.has(poolId)) {
hostsByPoolMap.set(poolId, [])
}
hostsByPoolMap.get(poolId)!.push(host)
})
return hostsByPoolMap
})
const getMasterHostByPoolId = (poolId: XoPool['id']) => {
const masterHostId = poolContext.get(poolId)?.master
if (masterHostId === undefined) {
return
}
return baseContext.get(masterHostId)
}
const isHostOperationPending = (host: XoHost, operations: HOST_OPERATION[] | HOST_OPERATION) => {
const currentOperations = Object.values(host.current_operations)
return castArray(operations).some(operation => currentOperations.includes(operation))
}
const context = {
...baseContext,
isMasterHost,
hostsByPool,
getMasterHostByPoolId,
isHostOperationPending,
}
return createSubscribableStoreContext({ context, ...configRest }, deps)
})

View File

@@ -1,35 +0,0 @@
import { usePifStore } from '@/stores/xo-rest-api/pif.store'
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'
export const useNetworkStore = defineStore('network', () => {
const deps = {
pifStore: usePifStore(),
}
const { context: baseContext, ...configRest } = createXoStoreConfig('network', {
sortBy: sortByNameLabel,
})
const pifContext = deps.pifStore.getContext()
const networksWithPifs = computed(() => {
const networkIds = Array.from(pifContext.hostMasterPifsByNetwork.value.keys())
return baseContext.records.value.filter(network => {
return networkIds.includes(network.id) && network.PIFs.length > 0
})
})
const networksWithoutPifs = computed(() => baseContext.records.value.filter(network => network.PIFs.length === 0))
const context = {
...baseContext,
networksWithPifs,
networksWithoutPifs,
}
return createSubscribableStoreContext({ context, ...configRest }, {})
})

View File

@@ -1,9 +0,0 @@
import { createXoStoreConfig } from '@/utils/create-xo-store-config.util'
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
import { defineStore } from 'pinia'
export const usePciStore = defineStore('pci', () => {
const config = createXoStoreConfig('pci')
return createSubscribableStoreContext(config, {})
})

View File

@@ -1,9 +0,0 @@
import { createXoStoreConfig } from '@/utils/create-xo-store-config.util'
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
import { defineStore } from 'pinia'
export const usePgpuStore = defineStore('pgpu', () => {
const config = createXoStoreConfig('pgpu')
return createSubscribableStoreContext(config, {})
})

View File

@@ -1,80 +0,0 @@
import { useHostStore } from '@/stores/xo-rest-api/host.store'
import type { XoHost } from '@/types/xo/host.type'
import type { XoNetwork } from '@/types/xo/network.type'
import type { XoPif } from '@/types/xo/pif.type'
import { createXoStoreConfig } from '@/utils/create-xo-store-config.util'
import type { ConnectionStatus } from '@core/components/connection-status/VtsConnectionStatus.vue'
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
import { defineStore } from 'pinia'
import { computed } from 'vue'
export const usePifStore = defineStore('pif', () => {
const deps = {
hostStore: useHostStore(),
}
const { context: baseContext, ...configRest } = createXoStoreConfig('pif')
const hostContext = deps.hostStore.getContext()
const hostMasterPifsByNetwork = computed(() => {
const hostMasterPifsByNetworkMap = new Map<XoNetwork['id'], XoPif[]>()
baseContext.records.value
.filter(pif => hostContext.isMasterHost(pif.$host))
.forEach(pif => {
const networkId = pif.$network
if (!hostMasterPifsByNetworkMap.has(networkId)) {
hostMasterPifsByNetworkMap.set(networkId, [])
}
hostMasterPifsByNetworkMap.get(networkId)?.push(pif)
})
return hostMasterPifsByNetworkMap
})
const getPifsByNetworkId = (networkId: XoNetwork['id']) => {
return baseContext.records.value.filter(pif => pif.$network === networkId)
}
const getPifStatus = (pif: XoPif): ConnectionStatus => {
if (!pif.attached) {
return 'disconnected'
}
if (!pif.carrier) {
return 'disconnected-from-physical-device'
}
return 'connected'
}
const pifsByHost = computed(() => {
const pifsByHostMap = new Map<XoHost['id'], XoPif[]>()
baseContext.records.value.forEach(pif => {
const hostId = pif.$host
if (!pifsByHostMap.has(hostId)) {
pifsByHostMap.set(hostId, [])
}
pifsByHostMap.get(hostId)!.push(pif)
})
return pifsByHostMap
})
const getBondsDevices = (pif: XoPif) => {
return pif.isBondMaster ? pif.bondSlaves.flatMap(slaveId => baseContext.get(slaveId)?.device ?? []) : []
}
const context = {
...baseContext,
hostMasterPifsByNetwork,
pifsByHost,
getPifsByNetworkId,
getPifStatus,
getBondsDevices,
}
return createSubscribableStoreContext({ context, ...configRest }, deps)
})

View File

@@ -1,12 +0,0 @@
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'
export const usePoolStore = defineStore('pool', () => {
const config = createXoStoreConfig('pool', {
sortBy: sortByNameLabel,
})
return createSubscribableStoreContext(config, {})
})

View File

@@ -1,36 +0,0 @@
import type { XoPool } from '@/types/xo/pool.type'
import type { XoServer } from '@/types/xo/server.type'
import { createXoStoreConfig } from '@/utils/create-xo-store-config.util'
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
import { defineStore } from 'pinia'
import { computed } from 'vue'
export const useServerStore = defineStore('server', () => {
const { context: baseContext, ...config } = createXoStoreConfig('server')
const serverByPool = computed(() => {
const serverByPoolMap = new Map<XoPool['id'], XoServer[]>()
baseContext.records.value.forEach(server => {
const poolId = server.poolId
if (!poolId) {
return
}
if (!serverByPoolMap.has(poolId)) {
serverByPoolMap.set(poolId, [])
}
serverByPoolMap.get(poolId)!.push(server)
})
return serverByPoolMap
})
const context = {
...baseContext,
serverByPool,
}
return createSubscribableStoreContext({ context, ...config }, {})
})

View File

@@ -1,56 +0,0 @@
import { useVdiStore } from '@/stores/xo-rest-api/vdi.store'
import type { XoSr } from '@/types/xo/sr.type'
import type { XoVdi } from '@/types/xo/vdi.type'
import { createXoStoreConfig } from '@/utils/create-xo-store-config.util'
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
import { defineStore } from 'pinia'
import { computed } from 'vue'
export const useSrStore = defineStore('sr', () => {
const { context: baseContext, ...configRest } = createXoStoreConfig('sr')
const deps = {
vdiStore: useVdiStore(),
}
const vdiContext = deps.vdiStore.getContext()
const srs = computed(() => baseContext.records.value)
const getSrName = (id: XoSr['id']) => baseContext.get(id)?.name_label
const isoSrs = computed(() => srs.value.filter(sr => sr.SR_type === 'iso'))
const isoVdiIds = computed(() =>
isoSrs.value.reduce((acc, sr) => {
if (sr.VDIs) {
sr.VDIs.forEach(vdiId => acc.add(vdiId))
}
return acc
}, new Set<XoVdi['id']>())
)
const vdiIsosBySrName = computed(() => {
const groupedVDIs: Record<string, XoVdi[]> = {}
isoVdiIds.value.forEach(vdiId => {
const vdi = vdiContext.get(vdiId)
if (vdi) {
const srName = getSrName(vdi.$SR) || 'Unknown SR'
if (!groupedVDIs[srName]) {
groupedVDIs[srName] = []
}
groupedVDIs[srName].push(vdi)
}
})
return groupedVDIs
})
const context = {
...baseContext,
vdiIsosBySrName,
}
return createSubscribableStoreContext({ context, ...configRest }, deps)
})

View File

@@ -1,9 +0,0 @@
import { createXoStoreConfig } from '@/utils/create-xo-store-config.util'
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
import { defineStore } from 'pinia'
export const useTaskStore = defineStore('task', () => {
const config = createXoStoreConfig('task', { pollInterval: 5000 })
return createSubscribableStoreContext(config, {})
})

View File

@@ -1,9 +0,0 @@
import { createXoStoreConfig } from '@/utils/create-xo-store-config.util'
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
import { defineStore } from 'pinia'
export const useVbdStore = defineStore('vbd', () => {
const config = createXoStoreConfig('vbd')
return createSubscribableStoreContext(config, {})
})

View File

@@ -1,9 +0,0 @@
import { createXoStoreConfig } from '@/utils/create-xo-store-config.util'
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
import { defineStore } from 'pinia'
export const useVdiStore = defineStore('vdi', () => {
const config = createXoStoreConfig('vdi')
return createSubscribableStoreContext(config, {})
})

View File

@@ -1,9 +0,0 @@
import { createXoStoreConfig } from '@/utils/create-xo-store-config.util'
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util'
import { defineStore } from 'pinia'
export const useVifStore = defineStore('vif', () => {
const config = createXoStoreConfig('vif')
return createSubscribableStoreContext(config, {})
})

View File

@@ -1,9 +0,0 @@
import { createXoStoreConfig } from '@/utils/create-xo-store-config.util.ts'
import { createSubscribableStoreContext } from '@core/utils/create-subscribable-store-context.util.ts'
import { defineStore } from 'pinia'
export const useVmControllerStore = defineStore('vm-controller', () => {
const config = createXoStoreConfig('vm-controller')
return createSubscribableStoreContext(config, {})
})

View File

@@ -1,33 +0,0 @@
import type { XoPool } from '@/types/xo/pool.type'
import type { XoVmTemplate } from '@/types/xo/vm-template.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'
export const useVmTemplateStore = defineStore('vm-template', () => {
const { context: baseContext, ...configRest } = createXoStoreConfig('vm-template', {
sortBy: sortByNameLabel,
})
const vmsTemplatesByPool = computed(() => {
const vmTemplatesByPoolMap = new Map<XoPool['id'], XoVmTemplate[]>()
baseContext.records.value.forEach(template => {
const poolId = template.$pool
if (!vmTemplatesByPoolMap.has(poolId)) {
vmTemplatesByPoolMap.set(poolId, [])
}
vmTemplatesByPoolMap.get(poolId)?.push(template)
})
return vmTemplatesByPoolMap
})
const context = {
...baseContext,
vmsTemplatesByPool,
}
return createSubscribableStoreContext({ context, ...configRest }, {})
})

View File

@@ -1,82 +0,0 @@
import { useHostStore } from '@/stores/xo-rest-api/host.store.ts'
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_OPERATION, 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 { castArray } from 'lodash-es'
import { defineStore } from 'pinia'
import { computed } from 'vue'
export const useVmStore = defineStore('vm', () => {
const deps = {
hostStore: useHostStore(),
}
const hostContext = deps.hostStore.getContext()
const { context: baseContext, ...configRest } = createXoStoreConfig('vm', {
sortBy: sortByNameLabel,
})
const runningVms = computed(() => baseContext.records.value.filter(vm => vm.power_state === VM_POWER_STATE.RUNNING))
const vmsByHost = computed(() => createVmsMap(baseContext.records.value, false))
const hostLessVmsByPool = computed(() => createVmsMap(baseContext.records.value, true))
const isVmOperatingPending = (vm: XoVm, operations: VM_OPERATION[] | VM_OPERATION) => {
const currentOperations = Object.values(vm.current_operations)
return castArray(operations).some(operation => currentOperations.includes(operation))
}
const getVmHost = (vm: XoVm): XoHost | undefined => {
const vmHostId = extractVmHostId(vm)
if (vmHostId === undefined) {
return undefined
}
return hostContext.get(vmHostId)
}
const context = {
...baseContext,
runningVms,
vmsByHost,
getVmHost,
hostLessVmsByPool,
isVmOperatingPending,
}
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
}
function extractVmHostId(vm: XoVm) {
return vm.$container === vm.$pool ? undefined : (vm.$container as XoHost['id'])
}

View File

@@ -1,68 +1,34 @@
import { xoApiDefinition } from '@/utils/xo-api-definition.util'
import type { ComputedRef, Ref } from 'vue'
import type { XoAlarm } from '@/types/xo/alarm.type.ts'
import type { XoHost } from '@/types/xo/host.type.ts'
import type { XoNetwork } from '@/types/xo/network.type.ts'
import type { XoPci } from '@/types/xo/pci.type.ts'
import type { XoPgpu } from '@/types/xo/pgpu.type.ts'
import type { XoPif } from '@/types/xo/pif.type.ts'
import type { XoPool } from '@/types/xo/pool.type.ts'
import type { XoServer } from '@/types/xo/server.type.ts'
import type { XoSr } from '@/types/xo/sr.type.ts'
import type { XoTask } from '@/types/xo/task.type.ts'
import type { XoVbd } from '@/types/xo/vbd.type.ts'
import type { XoVdi } from '@/types/xo/vdi.type.ts'
import type { XoVif } from '@/types/xo/vif.type.ts'
import type { XoVmController } from '@/types/xo/vm-controller.type.ts'
import type { XoVmTemplate } from '@/types/xo/vm-template.type.ts'
import type { XoVm } from '@/types/xo/vm.type.ts'
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
stream: boolean
}
>
// 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 | undefined>
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]
export type XoRecord =
| XoAlarm
| XoPool
| XoHost
| XoVm
| XoSr
| XoTask
| XoPif
| XoVbd
| XoVdi
| XoVif
| XoNetwork
| XoVmTemplate
| XoVmController
| XoServer
| XoPci
| XoPgpu

Some files were not shown because too many files have changed in this diff Show More