Compare commits

..

6 Commits

Author SHA1 Message Date
Olivier Floch
3d46fa9e3e feedback 2024-02-23 09:48:13 +01:00
Olivier Floch
e8eb2fe6a7 feat(xo6/core): improve color context 2024-02-23 09:25:04 +01:00
mathieuRA
aefcce45ff feat(xo-server/pusb): implement methods for USB passthrough 2024-02-22 14:58:55 +01:00
mathieuRA
367fb4d8a6 feat(xo-server): implement PUSB in xapi-object-to-xo 2024-02-22 14:58:55 +01:00
Julien Fontanet
e54a0bfc80 fix(xo-web/iso-device): fix SR predicate
Introduced by 1718649e0
2024-02-22 10:58:11 +01:00
Julien Fontanet
9e5541703b fix(xo-web/host): only count memory of running VMs
Introduced by 1718649e0

FIxes https://xcp-ng.org/forum/post/71886
2024-02-22 10:31:06 +01:00
11 changed files with 449 additions and 33 deletions

View File

@@ -2,7 +2,6 @@ import assert from 'node:assert'
import groupBy from 'lodash/groupBy.js'
import ignoreErrors from 'promise-toolbox/ignoreErrors'
import { asyncMap } from '@xen-orchestra/async-map'
import { createLogger } from '@xen-orchestra/log'
import { decorateMethodsWith } from '@vates/decorate-with'
import { defer } from 'golike-defer'
import { formatDateTime } from '@xen-orchestra/xapi'
@@ -11,8 +10,6 @@ import { getOldEntries } from '../../_getOldEntries.mjs'
import { Task } from '../../Task.mjs'
import { Abstract } from './_Abstract.mjs'
const { info, warn } = createLogger('xo:backups:AbstractXapi')
export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
constructor({
config,
@@ -174,20 +171,6 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
}
}
// this will delete current snapshot in case of failure
// to ensure any retry will start with a clean state, especially in the case of rolling snapshots
#removeCurrentSnapshotOnFailure() {
if (this._mustDoSnapshot() && this._exportedVm !== undefined) {
info('will delete snapshot on failure', { vm: this._vm, snapshot: this._exportedVm })
assert.notStrictEqual(
this._vm.$ref,
this._exportedVm.$ref,
'there should have a snapshot, but vm and snapshot have the same ref'
)
return this._xapi.VM_destroy(this._exportedVm.$ref)
}
}
async _fetchJobSnapshots() {
const jobId = this._jobId
const vmRef = this._vm.$ref
@@ -288,17 +271,11 @@ export const AbstractXapi = class AbstractXapiVmBackupRunner extends Abstract {
await this._exportedVm.update_blocked_operations({ pool_migrate, migrate_send })
}
}
} catch (error) {
try {
await this.#removeCurrentSnapshotOnFailure()
} catch (removeSnapshotError) {
warn('fail removing current snapshot', { error: removeSnapshotError })
}
throw error
} finally {
if (startAfter) {
ignoreErrors.call(vm.$callAsync('start', false, false))
}
await this._fetchJobSnapshots()
await this._removeUnusedSnapshots()
}

View File

@@ -0,0 +1,158 @@
<!-- TOC -->
- [Overview](#overview)
- [Simple Context](#simple-context)
- [1. Create the context](#1-create-the-context)
- [2. Use the context](#2-use-the-context)
- [2.1. Read](#21-read)
- [2.2. Update](#22-update)
- [Advanced Context](#advanced-context)
- [1. Create the context](#1-create-the-context-1)
- [2. Use the context](#2-use-the-context-1)
- [2.1. Read](#21-read-1)
- [2.2. Update](#22-update-1)
- [Caveats (boolean props)](#caveats-boolean-props)
<!-- TOC -->
# Overview
`createContext` lets you create a context that is both readable and writable, and is accessible by a component and all
its descendants at any depth.
Each descendant has the ability to change the context value, affecting itself and all of its descendants at any level.
## Simple Context
### 1. Create the context
`createContext` takes the initial context value as first argument.
```ts
// context.ts
const CounterContext = createContext(0)
```
### 2. Use the context
#### 2.1. Read
You can get the current Context value by using `useContext(CounterContext)`.
```ts
const counter = useContext(CounterContext)
console.log(counter.value) // 0
```
#### 2.2. Update
You can pass a `MaybeRefOrGetter` as second argument to update the context value.
```ts
// MyComponent.vue
const props = defineProps<{
counter?: number
}>()
const counter = useContext(CounterContext, () => props.counter)
// When calling <MyComponent />
console.log(counter.value) // 0
// When calling <MyComponent :counter="20" />
console.log(counter.value) // 20
```
## Advanced Context
To customize the context output, you can pass a custom context builder as the second argument of `createContext`.
### 1. Create the context
```ts
// context.ts
// Example 1. Return a object
const CounterContext = createContext(10, counter => ({
counter,
isEven: computed(() => counter.value % 2 === 0),
}))
// Example 2. Return a computed value
const DoubleContext = createContext(10, num => computed(() => num.value * 2))
// Example 3. Use a previous value
const ColorContext = createContext('info' as Color, (color, previousColor) => ({
name: color,
colorContextClass: computed(() => (previousColor.value === color.value ? undefined : `color-context-${color.value}`)),
}))
```
### 2. Use the context
#### 2.1. Read
When using the context, it will return your custom value.
```ts
const { counter, isEven } = useContext(CounterContext)
const double = useContext(DoubleContext)
console.log(counter.value) // 10
console.log(isEven.value) // true
console.log(double.value) // 20
```
#### 2.2. Update
Same as with a simple context, you can pass a `MaybeRefOrGetter` as second argument.
```ts
// Parent.vue
useContext(CounterContext, 99)
useContext(DoubleContext, 99)
// Child.vue
const { isEven } = useContext(CounterContext)
const double = useContext(DoubleContext)
console.log(isEven.value) // false
console.log(double.value) // 198
```
## Caveats (boolean props)
When working with `boolean` props, there's an important caveat to be aware of.
If the `MaybeRefOrGetter` returns any other value than `undefined`, the context will be updated according to this value.
This could be problematic if the value comes from a `boolean` prop.
```ts
const props = defineProps<{
disabled?: boolean
}>()
useContext(MyBooleanContext, () => props.disabled) // Update to `false` if `undefined`
```
In that case, Vue will automatically set the default value for `disabled` prop to `false`.
Even if the `disabled` prop in not provided at all, the current context will not be used and will be replaced
by `false`.
To circumvent this issue, you need to use `withDefaults` and specifically set the default value for `boolean` props
to `undefined`:
```ts
const props = withDefaults(
defineProps<{
disabled?: boolean
}>(),
{ disabled: undefined }
)
useContext(MyBoolean, () => props.disabled) // Keep parent value if `undefined`
```

View File

@@ -0,0 +1,142 @@
<!-- TOC -->
- [Overview](#overview)
- [CSS variables](#css-variables)
- [Available color contexts](#available-color-contexts)
- [Usage](#usage)
<!-- TOC -->
# Overview
The color context provides a way to apply a set of colors variants to a component and all its descendants at any depth.
Each descendant has the ability to change the context value, affecting itself and all of its descendants at any level.
The purpose is to colorize a component and its descendants by applying a single CSS class on the parent component (e.g., applying the class on a modal component container will style all children components using the context).
## CSS variables
The color context relies on the usage of the following variables:
```css
--color-context-primary;
--color-context-primary-hover;
--color-context-primary-active;
--color-context-primary-disabled;
--color-context-secondary;
--color-context-secondary-hover;
--color-context-secondary-active;
--color-context-tertiary;
```
Any component can use these variables for `color`, `background-color` or any other CSS property, to be usable with the color context.
When you set a color context, the variables are updated with the help of CSS classes defined in `_context.pcss`:
```css
.color-context-info {
--color-context-primary: var(--color-purple-base);
--color-context-primary-hover: var(--color-purple-d20);
--color-context-primary-active: var(--color-purple-d40);
--color-context-primary-disabled: var(--color-grey-400);
--color-context-secondary: var(--background-color-purple-10);
--color-context-secondary-hover: var(--background-color-purple-20);
--color-context-secondary-active: var(--background-color-purple-30);
}
.color-context-success {
--color-context-primary: var(--color-green-base);
--color-context-primary-hover: var(--color-green-d20);
/*...*/
}
```
You can add any other context by adding a `color-context-<my-context>` class and setting the desired values for the variables.
**Important note: remember to set a value for all variables to avoid any missing styles.**
## Available color contexts
Color contexts rely on the type `Color` defined in `/lib/types/color.type.ts`:
- `info` (_purple_)
- `success` (_green_)
- `warning` (_orange_)
- `error` (_red_)
## Usage
To get and set the color context in a component, you can pass the `ColorContext` to `useContext` and apply the `colorContextClass` to the root component:
```ts
// ParentComponent.vue
import { useContext } from '@core/composables/context.composable'
import { ColorContext } from '@core/context'
import type { Color } from '@core/types/color.type'
import { defineProps } from 'vue'
const props = defineProps<{
color?: Color
}>()
const { colorContextClass } = useContext(ColorContext, () => props.color)
```
All the components using the CSS variables will inherit the color context applied by the `colorContextClass`.
It's possible to change the color of a component on demand, if the component has a `color` prop, and passing it as the second parameter of the composable.
Then, the only thing to do is to apply the class in the component's `template`:
```vue
<!-- ParentComponent.vue -->
<template>
<div :class="colorContextClass">
<!-- Will use the color context defined by the class above-->
<MyComponent />
<!-- Will use the color "info" instead of the context-->
<MyComponent color="info" />
</div>
</template>
```
`MyComponent` using the context:
```vue
<!-- MyComponent.vue -->
<template>
<div :class="colorContextClass" class="my-component">
<p>Lorem ipsum dolor sit amet.</p>
</div>
</template>
<script lang="ts" setup>
import { useContext } from '@core/composables/context.composable'
import { ColorContext } from '@core/context'
import type { Color } from '@core/types/color.type'
import { defineProps } from 'vue'
const props = defineProps<{
color?: Color
}>()
const { colorContextClass } = useContext(ColorContext, () => props.color)
</script>
<style lang="postcss" scoped>
.my-component {
background-color: var(--color-context-secondary);
color: var(--color-context-primary);
}
</style>
```
In the example above, if the `color` prop is not set, the component will use the color context (i.e., if its parent uses a `success` color context, `MyComponent` will be styled with the `success` colors defined in `_context.pcss`).
If the `color` prop is set, the component will use the prop value to update the context for itself and its descendants.

View File

@@ -45,3 +45,55 @@
.context-border-color-info {
border-color: var(--color-purple-base);
}
.color-context-info {
--color-context-primary: var(--color-purple-base);
--color-context-primary-hover: var(--color-purple-d20);
--color-context-primary-active: var(--color-purple-d40);
--color-context-primary-disabled: var(--color-grey-400);
--color-context-secondary: var(--background-color-purple-10);
--color-context-secondary-hover: var(--background-color-purple-20);
--color-context-secondary-active: var(--background-color-purple-30);
--color-context-tertiary: var(--background-color-primary);
}
.color-context-success {
--color-context-primary: var(--color-green-base);
--color-context-primary-hover: var(--color-green-d20);
--color-context-primary-active: var(--color-green-d40);
--color-context-primary-disabled: var(--color-green-l60);
--color-context-secondary: var(--background-color-green-10);
--color-context-secondary-hover: var(--background-color-green-20);
--color-context-secondary-active: var(--background-color-green-30);
--color-context-tertiary: var(--background-color-primary);
}
.color-context-warning {
--color-context-primary: var(--color-orange-base);
--color-context-primary-hover: var(--color-orange-d20);
--color-context-primary-active: var(--color-orange-d40);
--color-context-primary-disabled: var(--color-orange-l60);
--color-context-secondary: var(--background-color-orange-10);
--color-context-secondary-hover: var(--background-color-orange-20);
--color-context-secondary-active: var(--background-color-orange-30);
--color-context-tertiary: var(--background-color-primary);
}
.color-context-error {
--color-context-primary: var(--color-red-base);
--color-context-primary-hover: var(--color-red-d20);
--color-context-primary-active: var(--color-red-d40);
--color-context-primary-disabled: var(--color-red-l60);
--color-context-secondary: var(--background-color-red-10);
--color-context-secondary-hover: var(--background-color-red-20);
--color-context-secondary-active: var(--background-color-red-30);
--color-context-tertiary: var(--background-color-primary);
}

View File

@@ -0,0 +1,34 @@
import type { ComputedRef, InjectionKey, MaybeRefOrGetter } from 'vue'
import { computed, inject, provide, toValue } from 'vue'
export const createContext = <T, Output = ComputedRef<T>>(
initialValue: MaybeRefOrGetter<T>,
customBuilder?: (value: ComputedRef<T>, previousValue: ComputedRef<T>) => Output
) => {
return {
id: Symbol('CONTEXT_ID') as InjectionKey<MaybeRefOrGetter<T>>,
initialValue,
builder: customBuilder ?? (value => value as Output),
}
}
type Context<T = any, Output = any> = ReturnType<typeof createContext<T, Output>>
type ContextOutput<Ctx extends Context> = Ctx extends Context<any, infer Output> ? Output : never
type ContextValue<Ctx extends Context> = Ctx extends Context<infer T> ? T : never
export const useContext = <Ctx extends Context, T extends ContextValue<Ctx>>(
context: Ctx,
newValue?: MaybeRefOrGetter<T | undefined>
): ContextOutput<Ctx> => {
const currentValue = inject(context.id, undefined)
const updatedValue = () => toValue(newValue) ?? toValue(currentValue) ?? context.initialValue
provide(context.id, updatedValue)
return context.builder(
computed(() => toValue(updatedValue)),
computed(() => toValue(currentValue))
)
}

View File

@@ -0,0 +1,8 @@
import { createContext } from '@core/composables/context.composable'
import type { Color } from '@core/types/color.type'
import { computed } from 'vue'
export const ColorContext = createContext('info' as Color, (color, previousColor) => ({
name: color,
colorContextClass: computed(() => (previousColor.value === color.value ? undefined : `color-context-${color.value}`)),
}))

View File

@@ -0,0 +1 @@
export type Color = 'info' | 'error' | 'warning' | 'success'

View File

@@ -0,0 +1,27 @@
export async function scan({ host }) {
await this.getXapi(host).call('PUSB.scan', host._xapiRef)
}
scan.params = {
host: { type: 'string' },
}
scan.resolve = {
host: ['host', 'host', 'operate'],
}
export async function set({ pusb, enabled }) {
const xapi = this.getXapi(pusb)
if (enabled !== undefined && enabled !== pusb.passthroughEnabled) {
await xapi.call('PUSB.set_passthrough_enabled', pusb._xapiRef, enabled)
}
}
set.params = {
id: { type: 'string' },
enabled: { type: 'boolean', optional: true },
}
set.resolve = {
pusb: ['id', 'PUSB', 'administrate'],
}

View File

@@ -889,6 +889,17 @@ const TRANSFORMS = {
vm: link(obj, 'VM'),
}
},
pusb(obj) {
return {
type: 'PUSB',
description: obj.description,
host: link(obj, 'host'),
passthroughEnabled: obj.passthrough_enabled,
usbGroup: link(obj, 'USB_group'),
}
},
}
// ===================================================================

View File

@@ -54,13 +54,9 @@ export default class IsoDevice extends Component {
() => this.props.vm.$pool,
() => this.props.vm.$container,
(vmPool, vmContainer) => sr => {
const vmRunning = vmContainer !== vmPool
const sameHost = vmContainer === sr.$container
const samePool = vmPool === sr.$pool
return (
samePool &&
(vmRunning ? sr.shared || sameHost : true) &&
vmPool === sr.$pool &&
(sr.shared || vmContainer === sr.$container) &&
(sr.SR_type === 'iso' || (sr.SR_type === 'udev' && sr.size))
)
}

View File

@@ -3,7 +3,6 @@ import _ from 'intl'
import Copiable from 'copiable'
import decorate from 'apply-decorators'
import Icon from 'icon'
import map from 'lodash/map'
import React from 'react'
import store from 'store'
import HomeTags from 'home-tags'
@@ -24,10 +23,21 @@ export default decorate([
provideState({
computed: {
areHostsVersionsEqual: ({ areHostsVersionsEqualByPool }, { host }) => areHostsVersionsEqualByPool[host.$pool],
inMemoryVms: (_, { vms }) => {
const result = []
for (const key of Object.keys(vms)) {
const vm = vms[key]
const { power_state } = vm
if (power_state === 'Running' || power_state === 'Paused') {
result.push(vm)
}
}
return result
},
},
}),
injectState,
({ statsOverview, host, nVms, vmController, vms, state: { areHostsVersionsEqual } }) => {
({ statsOverview, host, nVms, vmController, state: { areHostsVersionsEqual, inMemoryVms } }) => {
const pool = getObject(store.getState(), host.$pool)
const vmsFilter = encodeURIComponent(new CM.Property('$container', new CM.String(host.id)).toString())
return (
@@ -120,7 +130,7 @@ export default decorate([
tooltip={`${host.productBrand} (${formatSize(vmController.memory.size)})`}
value={vmController.memory.size}
/>
{map(vms, vm => (
{inMemoryVms.map(vm => (
<UsageElement
tooltip={`${vm.name_label} (${formatSize(vm.memory.size)})`}
key={vm.id}