Compare commits

..

2 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
11 changed files with 400 additions and 33 deletions

View File

@@ -437,8 +437,7 @@ export async function cleanVm(
}
}
// no warning because a VHD can be unused for perfectly good reasons,
// e.g. the corresponding backup (metadata file) has been deleted
logWarn('unused VHD', { path: vhd })
if (remove) {
logInfo('deleting unused VHD', { path: vhd })
unusedVhdsDeletion.push(VhdAbstract.unlink(handler, vhd))

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

@@ -13,6 +13,7 @@
- [Home & REST API] `$container` field of an halted VM now points to a host if a VDI is on a local storage [Forum#71769](https://xcp-ng.org/forum/post/71769)
- [Size Input] Ability to select two new units in the dropdown (`TiB`, `PiB`) (PR [#7382](https://github.com/vatesfr/xen-orchestra/pull/7382))
### Bug fixes
> Users must be able to say: “I had this issue, happy to know it's fixed”
@@ -22,8 +23,6 @@
- [Remotes] Correctly clear error when the remote is tested with success
- [Import/VMWare] Fix importing last snapshot (PR [#7370](https://github.com/vatesfr/xen-orchestra/pull/7370))
- [Host/Reboot] Fix false positive warning when restarting an host after updates (PR [#7366](https://github.com/vatesfr/xen-orchestra/pull/7366))
- [New/VM] Respect _Fast clone_ setting broken since 5.91.0 (PR [#7388](https://github.com/vatesfr/xen-orchestra/issues/7388))
- [Backup] Remove incorrect _unused VHD_ warning because the situation is normal (PR [#7406](https://github.com/vatesfr/xen-orchestra/issues/7406))
### Packages to release

View File

@@ -425,32 +425,6 @@ It works even if the VM is running, because we'll automatically export a snapsho
In the VM "Snapshots" tab, you can also export a snapshot like you export a VM.
## VM migration
### Simple VM Migration (VM.pool_migrate)
In simple migration, the VM's active state is transferred from host A to host B while its disks remains in its original location. This feature is only possible when the VM's disks are on a shared SR by both hosts and if the VM is running.
#### Use Case
- Migrate a VM within the same pool from host A to host B without moving the VM's VDIs.
### VM Migration with Storage Motion (VM.migrate_send)
VM migration with storage motion allows you to migrate a VM from one host to another when the VM's disks are not on a shared SR between the two hosts or if a specific network is chosen for the migration. VDIs will be migrated to the destination SR if one is provided.
#### Use Cases
- Migrate a VM to another pool.
- Migrate a VM within the same pool from host A to host B by selecting a network for the migration.
- Migrate a VM within the same pool from host A to host B by moving the VM's VDIs to another storage.
### Expected Behavior
- Migrating a VM that has VDIs on a shared SR from host A to host B must trigger a "Simple VM Migration".
- Migrating a VM that has VDIs on a shared SR from host A to host B using a particular network must trigger a "VM Migration with Storage Motion" without moving its VDIs.
- Migrating a VM from host A to host B with a destination SR must trigger a "VM Migration with Storage Motion" and move VDIs to the destination SR, regardless of where the VDIs were stored.
## Hosts management
Outside updates (see next section), you can also do host management via Xen Orchestra. Basic operations are supported, like reboot, shutdown and so on.

View File

@@ -460,7 +460,7 @@ export const SelectHostVm = makeStoreSelect(
export const SelectVmTemplate = makeStoreSelect(
() => {
const getVmTemplatesByPool = createGetObjectsOfType('VM-template').filter(getPredicate).sort().groupBy('$pool')
const getVmTemplatesByPool = createGetObjectsOfType('VM-template').filter(getPredicate).sort().groupBy('$container')
const getPools = createGetObjectsOfType('pool')
.pick(createSelector(getVmTemplatesByPool, vmTemplatesByPool => keys(vmTemplatesByPool)))
.sort()

View File

@@ -300,7 +300,7 @@ export default class NewVm extends BaseComponent {
get _isDiskTemplate() {
const { template } = this.props
return template && template.$VBDs.length !== 0 && template.name_label !== 'Other install media'
return template && template.template_info.disks.length === 0 && template.name_label !== 'Other install media'
}
_setState = (newValues, callback) => {
this.setState(
@@ -470,7 +470,7 @@ export default class NewVm extends BaseComponent {
const data = {
affinityHost: state.affinityHost && state.affinityHost.id,
clone: this._isDiskTemplate && state.fastClone,
clone: !this._isDiskTemplate && state.fastClone,
existingDisks: state.existingDisks,
installation,
name_label: state.name_label,