Compare commits

..

1 Commits

Author SHA1 Message Date
Thierry
de14528dd4 feat(xo): POC XO Core, XO 6, XO Lite 2023-12-22 15:57:52 +01:00
69 changed files with 2668 additions and 176 deletions

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
@xen-orchestra/web
@xen-orchestra/web-core
@xen-orchestra/web-lite

View File

@@ -123,7 +123,7 @@ const onProgress = makeOnProgress({
onTaskUpdate(taskLog) {},
})
Task.run({ properties: { name: 'my task' }, onProgress }, asyncFn)
Task.run({ data: { name: 'my task' }, onProgress }, asyncFn)
```
It can also be fed event logs directly:

View File

@@ -139,7 +139,7 @@ const onProgress = makeOnProgress({
onTaskUpdate(taskLog) {},
})
Task.run({ properties: { name: 'my task' }, onProgress }, asyncFn)
Task.run({ data: { name: 'my task' }, onProgress }, asyncFn)
```
It can also be fed event logs directly:

View File

@@ -0,0 +1,14 @@
import antfu from '@antfu/eslint-config'
export default antfu({
rules: {
'import/order': ['error', { alphabetize: { order: 'asc', orderImportKind: 'asc' } }],
},
overrides: {
vue: {
'vue/component-api-style': 'error',
'vue/no-empty-component-block': 'error',
'vue/block-order': ['error', { order: ['template', 'script', 'style'] }],
},
},
})

View File

@@ -0,0 +1,121 @@
:root {
--color-grey-000: #000000;
--color-grey-100: #1a1b38;
--color-grey-200: #595a6f;
--color-grey-300: #9899a5;
--color-grey-400: #bfbfc6;
--color-grey-500: #e5e5e7;
--color-grey-600: #ffffff;
--color-background-primary: #ffffff;
--color-background-secondary: #f6f6f7;
--color-purple-base: #8f84ff;
--color-purple-d20: color(#8f84ff blend(black 20%));
--color-purple-d40: color(#8f84ff blend(black 40%));
--color-purple-d60: color(#8f84ff blend(black 60%));
--color-purple-l20: color(#8f84ff blend(white 20%));
--color-purple-l40: color(#8f84ff blend(white 40%));
--color-purple-l60: color(#8f84ff blend(white 60%));
--color-background-purple-10: color(white blend(#8f84ff 10%));
--color-background-purple-20: color(white blend(#8f84ff 20%));
--color-background-purple-30: color(white blend(#8f84ff 30%));
--color-background-purple-60: color(white blend(#8f84ff 60%));
--color-green-base: #2ca878;
--color-green-d20: color(#2ca878 blend(black 20%));
--color-green-d40: color(#2ca878 blend(black 40%));
--color-green-d60: color(#2ca878 blend(black 60%));
--color-green-l20: color(#2ca878 blend(white 20%));
--color-green-l40: color(#2ca878 blend(white 40%));
--color-green-l60: color(#2ca878 blend(white 60%));
--color-background-green-10: color(white blend(#2ca878 10%));
--color-background-green-20: color(white blend(#2ca878 20%));
--color-background-green-30: color(white blend(#2ca878 30%));
--color-background-green-60: color(white blend(#2ca878 60%));
--color-orange-base: #ef7f18;
--color-orange-d20: color(#ef7f18 blend(black 20%));
--color-orange-d40: color(#ef7f18 blend(black 40%));
--color-orange-d60: color(#ef7f18 blend(black 60%));
--color-orange-l20: color(#ef7f18 blend(white 20%));
--color-orange-l40: color(#ef7f18 blend(white 40%));
--color-orange-l60: color(#ef7f18 blend(white 60%));
--color-background-orange-10: color(white blend(#ef7f18 10%));
--color-background-orange-20: color(white blend(#ef7f18 20%));
--color-background-orange-30: color(white blend(#ef7f18 30%));
--color-background-orange-60: color(white blend(#ef7f18 60%));
--color-red-base: #be1621;
--color-red-d20: color(#be1621 blend(black 20%));
--color-red-d40: color(#be1621 blend(black 40%));
--color-red-d60: color(#be1621 blend(black 60%));
--color-red-l20: color(#be1621 blend(white 20%));
--color-red-l40: color(#be1621 blend(white 40%));
--color-red-l60: color(#be1621 blend(white 60%));
--color-background-red-10: color(white blend(#be1621 10%));
--color-background-red-20: color(white blend(#be1621 20%));
--color-background-red-30: color(white blend(#be1621 30%));
--color-background-red-60: color(white blend(#be1621 60%));
}
:root.dark {
--color-grey-000: #ffffff;
--color-grey-100: #e5e5e7;
--color-grey-400: #595a6f;
--color-grey-200: #bfbfc6;
--color-grey-300: #9899a5;
--color-grey-500: #1a1b38;
--color-grey-600: #000000;
--color-background-primary: #14141e;
--color-background-secondary: #17182b;
--color-purple-base: #8f84ff;
--color-purple-d20: color(#8f84ff blend(white 20%));
--color-purple-d40: color(#8f84ff blend(white 40%));
--color-purple-d60: color(#8f84ff blend(white 60%));
--color-purple-l20: color(#8f84ff blend(black 20%));
--color-purple-l40: color(#8f84ff blend(black 40%));
--color-purple-l60: color(#8f84ff blend(black 60%));
--color-background-purple-10: color(#17182b blend(#8f84ff 25%));
--color-background-purple-20: color(#17182b blend(#8f84ff 35%));
--color-background-purple-30: color(#17182b blend(#8f84ff 45%));
--color-background-purple-60: color(#17182b blend(#8f84ff 85%));
--color-green-base: #2ca878;
--color-green-d20: color(#2ca878 blend(white 20%));
--color-green-d40: color(#2ca878 blend(white 40%));
--color-green-d60: color(#2ca878 blend(white 60%));
--color-green-l20: color(#2ca878 blend(black 20%));
--color-green-l40: color(#2ca878 blend(black 40%));
--color-green-l60: color(#2ca878 blend(black 60%));
--color-background-green-10: color(#17182b blend(#2ca878 25%));
--color-background-green-20: color(#17182b blend(#2ca878 35%));
--color-background-green-30: color(#17182b blend(#2ca878 45%));
--color-background-green-60: color(#17182b blend(#2ca878 85%));
--color-orange-base: #ef7f18;
--color-orange-d20: color(#ef7f18 blend(white 20%));
--color-orange-d40: color(#ef7f18 blend(white 40%));
--color-orange-d60: color(#ef7f18 blend(white 60%));
--color-orange-l20: color(#ef7f18 blend(black 20%));
--color-orange-l40: color(#ef7f18 blend(black 40%));
--color-orange-l60: color(#ef7f18 blend(black 60%));
--color-background-orange-10: color(#17182b blend(#ef7f18 25%));
--color-background-orange-20: color(#17182b blend(#ef7f18 35%));
--color-background-orange-30: color(#17182b blend(#ef7f18 45%));
--color-background-orange-60: color(#17182b blend(#ef7f18 85%));
--color-red-base: #be1621;
--color-red-d20: color(#be1621 blend(white 20%));
--color-red-d40: color(#be1621 blend(white 40%));
--color-red-d60: color(#be1621 blend(white 60%));
--color-red-l20: color(#be1621 blend(black 20%));
--color-red-l40: color(#be1621 blend(black 40%));
--color-red-l60: color(#be1621 blend(black 60%));
--color-background-red-10: color(#17182b blend(#be1621 25%));
--color-background-red-20: color(#17182b blend(#be1621 35%));
--color-background-red-30: color(#17182b blend(#be1621 45%));
--color-background-red-60: color(#17182b blend(#be1621 85%));
}

View File

@@ -0,0 +1,47 @@
.context-color-success {
color: var(--color-green-base);
}
.context-color-error {
color: var(--color-red-base);
}
.context-color-warning {
color: var(--color-orange-base);
}
.context-color-info {
color: var(--color-purple-base);
}
.context-background-color-success {
background-color: var(--color-background-green-10);
}
.context-background-color-error {
background-color: var(--color-background-red-10);
}
.context-background-color-warning {
background-color: var(--color-background-orange-10);
}
.context-background-color-info {
background-color: var(--color-background-purple-10);
}
.context-border-color-success {
border-color: var(--color-green-base);
}
.context-border-color-error {
border-color: var(--color-red-base);
}
.context-border-color-warning {
border-color: var(--color-orange-base);
}
.context-border-color-info {
border-color: var(--color-purple-base);
}

View File

@@ -0,0 +1,6 @@
@import '@fontsource/poppins/400.css';
@import '@fontsource/poppins/500.css';
@import '@fontsource/poppins/600.css';
@import '@fontsource/poppins/700.css';
@import '@fontsource/poppins/900.css';
@import '@fontsource/poppins/400-italic.css';

View File

@@ -0,0 +1,42 @@
html {
box-sizing: border-box;
font-size: 10px;
}
body {
font-size: 1.6rem;
}
*,
*::before,
*::after {
box-sizing: inherit;
margin: 0;
position: relative;
font-family: Poppins, sans-serif;
}
body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
ol,
ul {
margin: 0;
padding: 0;
font-weight: normal;
}
ol,
ul {
list-style: none;
}
img {
max-width: 100%;
height: auto;
}

View File

@@ -0,0 +1,25 @@
:root {
--shadow-100: 0 0.1rem 0.1rem 0 rgba(26, 27, 56, 0.06);
--shadow-200: 0 0.1rem 0.1rem 0 rgba(26, 27, 56, 0.08), 0 0.2rem 0.1rem 0 rgba(26, 27, 56, 0.06),
0 0.1rem 0.3rem 0 rgba(26, 27, 56, 0.1);
--shadow-300: 0 0.6rem 1rem 0 rgba(26, 27, 56, 0.08), 0 0.1rem 1.8rem 0 rgba(26, 27, 56, 0.06),
0 0.3rem 0.5rem 0 rgba(26, 27, 56, 0.1);
--shadow-400: 0 2.4rem 3.8rem 0 rgba(26, 27, 56, 0.04), 0 0.9rem 4.6rem 0 rgba(26, 27, 56, 0.06),
0 1.1rem 1.5rem 0 rgba(26, 27, 56, 0.1);
}
:root.dark {
--shadow-100: 0 0.1rem 0.1rem 0 rgba(0, 0, 0, 0.12);
--shadow-200: 0 0.1rem 0.1rem 0 rgba(0, 0, 0, 0.16), 0 0.2rem 0.1rem 0 rgba(0, 0, 0, 0.12),
0 0.1rem 0.3rem 0 rgba(0, 0, 0, 0.2);
--shadow-300: 0 0.6rem 1rem 0 rgba(0, 0, 0, 0.16), 0 0.1rem 1.8rem 0 rgba(0, 0, 0, 0.12),
0 0.3rem 0.5rem 0 rgba(0, 0, 0, 0.2);
--shadow-400: 0 2.4rem 3.8rem 0 rgba(0, 0, 0, 0.08), 0 0.9rem 4.6rem 0 rgba(0, 0, 0, 0.12),
0 1.1rem 1.5rem 0 rgba(0, 0, 0, 0.2);
}

View File

@@ -0,0 +1,112 @@
.h1,
.h2,
.h3,
.h4,
.h5,
.h6,
.h7,
.p1,
.p2,
.p3,
.p4,
.c1,
.c2,
.c3,
.c4,
.c5 {
font-weight: 400;
&.black {
font-weight: 900;
}
&.semi-bold {
font-weight: 600;
}
&.medium {
font-weight: 500;
}
&.underline {
text-decoration: underline;
}
&.italic {
font-style: italic;
}
}
.h4,
.h5,
.h6,
.h7,
.p1,
.p2,
.p3,
.p4,
.c1,
.c2,
.c3,
.c4,
.c5 {
line-height: 1.5em;
letter-spacing: 0.04em;
}
.h1 {
font-size: 4.8rem;
line-height: 6rem;
}
.h2 {
font-size: 3.6rem;
line-height: 6rem;
}
.h3 {
font-size: 2.4rem;
line-height: 3.2rem;
}
.h4 {
font-size: 2rem;
}
.h5 {
font-size: 1.8rem;
}
.h6,
.p1,
.c1 {
font-size: 1.6rem;
}
.h7,
.p2,
.c2 {
font-size: 1.4rem;
}
.p3,
.c3 {
font-size: 1.2rem;
}
.p4,
.c4 {
font-size: 1rem;
}
.c5 {
font-size: 0.8rem;
}
.c1,
.c2,
.c3,
.c4,
.c5 {
text-transform: uppercase;
}

View File

@@ -0,0 +1,15 @@
@import '_colors.pcss';
@import '_reset.pcss';
@import '_fonts.pcss';
@import '_context.pcss';
@import '_shadows.pcss';
@import '_typography.pcss';
:root {
color: var(--color-grey-100);
background-color: var(--color-background-primary);
&.dark {
color-scheme: dark;
}
}

View File

@@ -0,0 +1,38 @@
<template>
<div :class="classProp" class="ui-card">
<slot />
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useContext } from '../../composables/context.composable'
import type { Color } from '../../types/color'
import { ColorContext } from '../../utils/context'
const props = defineProps<{
color?: Color
}>()
const { name: contextColor, backgroundClass } = useContext(ColorContext, () => props.color)
// We don't want to inherit "info" color
const classProp = computed(() => {
if (props.color === undefined && contextColor.value === 'info')
return 'bg-primary'
return backgroundClass.value
})
</script>
<style lang="postcss" scoped>
.ui-card {
padding: 2.1rem;
border-radius: 0.8rem;
box-shadow: var(--shadow-200);
}
.bg-primary {
background-color: var(--background-color-primary);
}
</style>

View File

@@ -0,0 +1,19 @@
<template>
<UiSpinner v-if="busy" class="ui-icon" />
<FontAwesomeIcon v-else-if="icon !== undefined" :icon="icon" class="ui-icon" :fixed-width="fixedWidth" />
</template>
<script lang="ts" setup>
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import UiSpinner from './UiSpinner.vue'
withDefaults(
defineProps<{
busy?: boolean
icon?: IconDefinition
fixedWidth?: boolean
}>(),
{ fixedWidth: true },
)
</script>

View File

@@ -0,0 +1,47 @@
<!-- Adapted from https://www.benmvp.com/blog/how-to-create-circle-svg-gradient-loading-spinner/ -->
<template>
<svg class="ui-spinner" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" fill="none">
<defs>
<linearGradient :id="secondHalfId">
<stop offset="0%" stop-opacity="0" stop-color="currentColor" />
<stop offset="100%" stop-opacity="0.5" stop-color="currentColor" />
</linearGradient>
<linearGradient :id="firstHalfId">
<stop offset="0%" stop-opacity="1" stop-color="currentColor" />
<stop offset="100%" stop-opacity="0.5" stop-color="currentColor" />
</linearGradient>
</defs>
<g stroke-width="40">
<path d="M 30 200 A 170 170 180 0 1 370 200" :stroke="`url(#${secondHalfId})`" />
<path d="M 370 200 A 170 170 0 0 1 30 200" :stroke="`url(#${firstHalfId})`" />
<path stroke="currentColor" stroke-linecap="round" d="M 30 200 A 170 170 180 0 1 30 200" />
</g>
</svg>
</template>
<script lang="ts" setup>
import { uniqueId } from 'lodash-es'
const firstHalfId = uniqueId('spinner-first-half-')
const secondHalfId = uniqueId('spinner-second-half-')
</script>
<style lang="postcss" scoped>
.ui-spinner {
width: 1.2em;
height: 1.2em;
animation: rotate 1s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -0,0 +1,36 @@
import type { ComputedRef, InjectionKey, MaybeRefOrGetter } from 'vue'
import { computed, inject, provide, toValue } from 'vue'
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 function createContext<T, Output = ComputedRef<T>>(
initialValue: MaybeRefOrGetter<T>,
customBuilder?: (value: ComputedRef<T>) => Output,
) {
return {
id: Symbol('context') as InjectionKey<MaybeRefOrGetter<T>>,
initialValue,
builder: customBuilder ?? (value => value as Output),
}
}
export function useContext<Ctx extends Context, T extends ContextValue<Ctx>>(
context: Ctx,
newValue?: MaybeRefOrGetter<T | undefined>,
): ContextOutput<Ctx> {
const currentValue = inject(context.id, context.initialValue)
const build = (value: MaybeRefOrGetter<T>) => context.builder(computed(() => toValue(value)))
if (newValue !== undefined) {
const updatedValue = () => toValue(newValue) ?? toValue(currentValue)
provide(context.id, updatedValue)
return build(updatedValue)
}
return build(currentValue)
}

View File

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

View File

@@ -0,0 +1,12 @@
import { computed } from 'vue'
import { createContext } from '../composables/context.composable'
import type { Color } from '../types/color'
export const DisabledContext = createContext(false)
export const ColorContext = createContext('info' as Color, color => ({
name: color,
textClass: computed(() => `context-color-${color.value}`),
backgroundClass: computed(() => `context-background-color-${color.value}`),
borderClass: computed(() => `context-border-color-${color.value}`),
}))

View File

@@ -0,0 +1,51 @@
{
"name": "@xen-orchestra/web-core",
"type": "module",
"version": "0.0.1",
"private": true,
"exports": {
"./*": {
"types": "./lib/*",
"import": "./lib/*"
},
"./eslint": {
"import": "./eslint.config.js"
}
},
"files": [
"lib"
],
"scripts": {
"lint": "eslint",
"lint:fix": "eslint --fix"
},
"devDependencies": {
"@antfu/eslint-config": "^2.4.6",
"@fontsource/poppins": "^5.0.8",
"@fortawesome/fontawesome-common-types": "^6.5.1",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.5",
"@tsconfig/node18": "^18.2.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^18.19.3",
"@vitejs/plugin-vue": "^5.0.0-beta.1",
"@vue/tsconfig": "^0.5.1",
"@vueuse/core": "^10.7.0",
"eslint": "^8.56.0",
"glob": "^10.3.10",
"lodash-es": "^4.17.21",
"npm-run-all2": "^6.1.1",
"pinia": "^2.1.7",
"typescript": "~5.3.3",
"vite": "^5.0.10",
"vite-plugin-dts": "^3.6.4",
"vite-plugin-lib-inject-css": "^1.3.0",
"vue": "^3.4.0-beta.4",
"vue-router": "^4.2.5",
"vue-tsc": "^1.8.25"
},
"lint-staged": {
"*": "eslint --fix"
}
}

View File

@@ -0,0 +1,10 @@
{
"extends": ["@vue/tsconfig/tsconfig.dom.json"],
"compilerOptions": {
"baseUrl": ".",
"module": "ESNext",
"noEmit": true
},
"include": ["env.d.ts", "eslint.config.js", "lib/**/*", "lib/**/*.vue"],
"exclude": ["lib/**/__tests__/*"]
}

31
@xen-orchestra/web-lite/.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
typed-router.d.ts

View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

View File

@@ -0,0 +1,40 @@
# web-lite
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```

1
@xen-orchestra/web-lite/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1 @@
export { default } from '@xen-orchestra/web-core/eslint'

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,40 @@
{
"name": "@xen-orchestra/web-lite",
"type": "module",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"devDependencies": {
"@antfu/eslint-config": "^2.4.6",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@tsconfig/node18": "^18.2.2",
"@types/node": "^18.19.3",
"@vitejs/plugin-vue": "^5.0.0-beta.1",
"@vue/tsconfig": "^0.5.1",
"@vueuse/core": "^10.7.0",
"@xen-orchestra/web-core": "^0.0.1",
"eslint": "^8.56.0",
"npm-run-all2": "^6.1.1",
"pinia": "^2.1.7",
"postcss-apply": "^0.12.0",
"postcss-color-function": "^4.1.0",
"postcss-nested": "^6.0.1",
"typescript": "~5.3.3",
"unplugin-vue-router": "^0.7.0",
"vite": "^5.0.10",
"vue": "^3.4.0-beta.4",
"vue-router": "^4.2.5",
"vue-tsc": "^1.8.25"
},
"lint-staged": {
"*": "eslint --fix"
}
}

View File

@@ -0,0 +1,9 @@
'use strict'
module.exports = {
plugins: {
'postcss-apply': {},
'postcss-nested': {},
'postcss-color-function': {},
},
}

View File

@@ -0,0 +1,3 @@
<template>
<RouterView />
</template>

View File

@@ -0,0 +1,19 @@
<template>
<div class="main-layout">
<div>XO Lite Main Layout</div>
<div class="content">
<slot />
</div>
</div>
</template>
<style lang="postcss" scoped>
.main-layout {
font-size: 2rem;
padding: 4rem;
}
.content {
padding: 1rem 2rem;
}
</style>

View File

@@ -0,0 +1,18 @@
import '@xen-orchestra/web-core/assets/css/base.pcss'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router/auto'
import App from './App.vue'
const app = createApp(App)
const router = createRouter({
history: createWebHashHistory(),
})
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@@ -0,0 +1,30 @@
<template>
<MainLayout>
<UiCard color="info">
<div>Welcome on XO Lite main page</div>
<div>
Demo icon from web-core:
<UiIcon :icon="faShip" />
</div>
<div>
<button
type="button"
@click="toggleDark()"
>
Toggle Dark Mode {{ isDark ? 'OFF' : 'ON' }}
</button>
</div>
</UiCard>
</MainLayout>
</template>
<script lang="ts" setup>
import { faShip } from '@fortawesome/free-solid-svg-icons/faShip'
import { useDark, useToggle } from '@vueuse/core'
import UiCard from '@xen-orchestra/web-core/components/ui/UiCard.vue'
import UiIcon from '@xen-orchestra/web-core/components/ui/UiIcon.vue'
import MainLayout from '@/layouts/MainLayout.vue'
const isDark = useDark()
const toggleDark = useToggle(isDark)
</script>

View File

@@ -0,0 +1,22 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"rootDir": "..",
"paths": {
"@/*": ["./src/*"],
"@xen-orchestra/web-core/*": ["../web-core/lib/*"]
},
"noEmit": true
},
"include": [
"env.d.ts",
"typed-router.d.ts",
"src/**/*",
"src/**/*.vue",
"../web-core/lib/**/*",
"../web-core/lib/**/*.vue"
],
"exclude": ["src/**/__tests__/*"]
}

View File

@@ -0,0 +1,11 @@
{
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
],
"files": []
}

View File

@@ -0,0 +1,11 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"],
"noEmit": true
},
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*"]
}

View File

@@ -0,0 +1,15 @@
import { URL, fileURLToPath } from 'node:url'
import vue from '@vitejs/plugin-vue'
import vueRouter from 'unplugin-vue-router/vite'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vueRouter(), vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})

31
@xen-orchestra/web/.gitignore vendored Normal file
View File

@@ -0,0 +1,31 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
typed-router.d.ts

View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

View File

@@ -0,0 +1,40 @@
# web
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```

1
@xen-orchestra/web/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1 @@
export { default } from '@xen-orchestra/web-core/eslint'

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,38 @@
{
"name": "@xen-orchestra/web",
"type": "module",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"devDependencies": {
"@antfu/eslint-config": "^2.4.6",
"@tsconfig/node18": "^18.2.2",
"@types/node": "^18.19.3",
"@vitejs/plugin-vue": "^5.0.0-beta.1",
"@vue/tsconfig": "^0.5.1",
"@xen-orchestra/web-core": "^0.0.1",
"eslint": "^8.56.0",
"npm-run-all2": "^6.1.1",
"pinia": "^2.1.7",
"postcss-apply": "^0.12.0",
"postcss-color-function": "^4.1.0",
"postcss-nested": "^6.0.1",
"typescript": "~5.3.3",
"unplugin-vue-router": "^0.7.0",
"vite": "^5.0.10",
"vue": "^3.4.0-beta.4",
"vue-router": "^4.2.5",
"vue-tsc": "^1.8.25"
},
"lint-staged": {
"*": "eslint --fix"
}
}

View File

@@ -0,0 +1,9 @@
'use strict'
module.exports = {
plugins: {
'postcss-apply': {},
'postcss-nested': {},
'postcss-color-function': {},
},
}

View File

View File

@@ -0,0 +1,3 @@
<template>
<RouterView />
</template>

View File

@@ -0,0 +1,20 @@
<template>
<div class="main-layout">
<div>XO 6 Main Layout</div>
<div class="content">
<slot />
</div>
</div>
</template>
<style lang="postcss" scoped>
.main-layout {
padding: 8rem;
font-size: 3rem;
}
.content {
padding: 2rem 4rem;
border-bottom: 2px solid black;
}
</style>

View File

@@ -0,0 +1,18 @@
import '@xen-orchestra/web-core/assets/css/base.pcss'
import { createPinia } from 'pinia'
import { createApp } from 'vue'
import { createRouter, createWebHashHistory } from 'vue-router/auto'
import App from './App.vue'
const app = createApp(App)
const router = createRouter({
history: createWebHashHistory(),
})
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@@ -0,0 +1,27 @@
<template>
<MainLayout>
<UiCard color="info">
<div>Welcome on XO 6 main page</div>
<div>
Demo icon from web-core:
<UiIcon :icon="faRocket" />
</div>
<div>
<button type="button" @click="toggleDark()">
Toggle Dark Mode {{ isDark ? 'OFF' : 'ON' }}
</button>
</div>
</UiCard>
</MainLayout>
</template>
<script lang="ts" setup>
import { faRocket } from '@fortawesome/free-solid-svg-icons/faRocket'
import { useDark, useToggle } from '@vueuse/core'
import UiCard from '@xen-orchestra/web-core/components/ui/UiCard.vue'
import UiIcon from '@xen-orchestra/web-core/components/ui/UiIcon.vue'
import MainLayout from '@/layouts/MainLayout.vue'
const isDark = useDark()
const toggleDark = useToggle(isDark)
</script>

View File

@@ -0,0 +1,22 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"rootDir": "..",
"paths": {
"@/*": ["./src/*"],
"@xen-orchestra/web-core/*": ["../web-core/lib/*"]
},
"noEmit": true
},
"include": [
"env.d.ts",
"typed-router.d.ts",
"src/**/*",
"src/**/*.vue",
"../web-core/lib/**/*",
"../web-core/lib/**/*.vue"
],
"exclude": ["src/**/__tests__/*"]
}

View File

@@ -0,0 +1,11 @@
{
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
],
"files": []
}

View File

@@ -0,0 +1,11 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"],
"noEmit": true
},
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*"]
}

View File

@@ -0,0 +1,15 @@
import { URL, fileURLToPath } from 'node:url'
import vue from '@vitejs/plugin-vue'
import vueRouter from 'unplugin-vue-router/vite'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vueRouter(), vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})

View File

@@ -6,16 +6,12 @@
### Enhancements
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [SR] show an icon on SR during VDI coalescing (with XCP-ng 8.3+) (PR [#7241](https://github.com/vatesfr/xen-orchestra/pull/7241))
- [VDI/Export] Expose NBD settings in the XO and REST APIs api (PR [#7251](https://github.com/vatesfr/xen-orchestra/pull/7251))
- [Menu/Proxies] Added a warning icon if unable to check proxies upgrade (PR [#7237](https://github.com/vatesfr/xen-orchestra/pull/7237))
### Bug fixes
- [Backup/Report] Missing report for Mirror Backup (PR [#7254](https://github.com/vatesfr/xen-orchestra/pull/7254))
> Users must be able to say: “I had this issue, happy to know it's fixed”
### Packages to release

View File

@@ -157,32 +157,33 @@ function extractFlags(args) {
const noop = Function.prototype
function parseValue(value) {
if (value.startsWith('json:')) {
return JSON.parse(value.slice(5))
}
if (value === 'true') {
return true
}
if (value === 'false') {
return false
}
return value
}
const PARAM_RE = /^([^=]+)=([^]*)$/
function parseParameters(args) {
if (args[0] === '--') {
return args.slice(1).map(parseValue)
}
const params = {}
forEach(args, function (arg) {
let matches
if (!(matches = arg.match(PARAM_RE))) {
throw new Error('invalid arg: ' + arg)
}
params[matches[1]] = parseValue(matches[2])
const name = matches[1]
let value = matches[2]
if (value.startsWith('json:')) {
value = JSON.parse(value.slice(5))
}
if (name === '@') {
params['@'] = value
return
}
if (value === 'true') {
value = true
} else if (value === 'false') {
value = false
}
params[name] = value
})
return params

View File

@@ -11,7 +11,7 @@ import { defer } from 'golike-defer'
import { format } from 'json-rpc-peer'
import { FAIL_ON_QUEUE } from 'limit-concurrency-decorator'
import { getStreamAsBuffer } from 'get-stream'
import { ignoreErrors, timeout } from 'promise-toolbox'
import { ignoreErrors } from 'promise-toolbox'
import { invalidParameters, noSuchObject, unauthorized } from 'xo-common/api-errors.js'
import { Ref } from 'xen-api'
@@ -1027,7 +1027,11 @@ start.resolve = {
// -------------------------------------------------------------------
export const stop = defer(async function ($defer, { vm, force, forceShutdownDelay, bypassBlockedOperation = force }) {
// TODO: implements timeout.
// - if !force → clean shutdown
// - if force is true → hard shutdown
// - if force is integer → clean shutdown and after force seconds, hard shutdown.
export const stop = defer(async function ($defer, { vm, force, bypassBlockedOperation = force }) {
const xapi = this.getXapi(vm)
if (bypassBlockedOperation) {
@@ -1049,14 +1053,13 @@ export const stop = defer(async function ($defer, { vm, force, forceShutdownDela
// Clean shutdown
try {
await timeout.call(xapi.shutdownVm(vm._xapiRef), forceShutdownDelay, () =>
xapi.shutdownVm(vm._xapiRef, { hard: true })
)
await xapi.shutdownVm(vm._xapiRef)
} catch (error) {
const { code } = error
if (code === 'VM_MISSING_PV_DRIVERS' || code === 'VM_LACKS_FEATURE_SHUTDOWN') {
throw invalidParameters('clean shutdown requires PV drivers')
}
throw error
}
})
@@ -1064,7 +1067,6 @@ export const stop = defer(async function ($defer, { vm, force, forceShutdownDela
stop.params = {
id: { type: 'string' },
force: { type: 'boolean', optional: true },
forceShutdownDelay: { type: 'number', default: 0 },
bypassBlockedOperation: { type: 'boolean', optional: true },
}
@@ -1398,7 +1400,7 @@ export async function importMultipleFromEsxi({
await asyncEach(
vms,
async vm => {
await Task.run({ properties: { name: `importing vm ${vm}` } }, async () => {
await Task.run({ data: { name: `importing vm ${vm}` } }, async () => {
try {
const vmUuid = await this.migrationfromEsxi({
host,

View File

@@ -190,8 +190,6 @@ export default class Redis extends Collection {
await Promise.all(promises)
model = this._unserialize(model) ?? model
model.id = id
return model
})
)

View File

@@ -29,7 +29,7 @@ export class PluginsMetadata extends Collection {
throw new Error('no such plugin metadata')
}
await this.update({
return /* await */ this.update({
...pluginMetadata,
...data,
})

View File

@@ -251,19 +251,9 @@ export default class Api {
constructor(app) {
this._logger = null
this._methods = { __proto__: null }
this._app = app
const defer =
const seq = async methods => {
for (const method of methods) {
await this.#callApiMethod(method[0], method[1])
}
}
seq.validate = ajv.compile({ type: 'array', minLength: 1, items: { type: ['array', 'string'] } })
const if =
this._methods = { __proto__: null, seq }
this.addApiMethods(methods)
app.hooks.on('start', async () => {
this._logger = await app.getLogger('api')
@@ -377,7 +367,8 @@ export default class Api {
}
async callApiMethod(connection, name, params = {}) {
if (!Object.hasOwn(this._methods, name)) {
const method = this._methods[name]
if (!method) {
throw new MethodNotFound(name)
}
@@ -392,12 +383,11 @@ export default class Api {
apiContext.permission = 'none'
}
return this.#apiContext.run(apiContext, () => this.#callApiMethod(name, params))
return this.#apiContext.run(apiContext, () => this.#callApiMethod(name, method, params))
}
async #callApiMethod(name, params) {
async #callApiMethod(name, method, params) {
const app = this._app
const method = this._methods[name]
const startTime = Date.now()
const { connection, user } = this.apiContext

View File

@@ -3,7 +3,11 @@ import { noSuchObject } from 'xo-common/api-errors.js'
import Collection from '../collection/redis.mjs'
import patch from '../patch.mjs'
class CloudConfigs extends Collection {}
class CloudConfigs extends Collection {
get(properties) {
return super.get(properties)
}
}
export default class {
constructor(app) {
@@ -31,7 +35,7 @@ export default class {
async updateCloudConfig({ id, name, template }) {
const cloudConfig = await this.getCloudConfig(id)
patch(cloudConfig, { name, template })
await this._db.update(cloudConfig)
return this._db.update(cloudConfig)
}
deleteCloudConfig(id) {

View File

@@ -19,31 +19,47 @@ const log = createLogger('xo:jobs')
// -----------------------------------------------------------------------------
class JobsDb extends Collection {
_serialize(job) {
Object.keys(job).forEach(key => {
const value = job[key]
if (typeof value !== 'string') {
job[key] = JSON.stringify(job[key])
const normalize = job => {
Object.keys(job).forEach(key => {
try {
const value = (job[key] = JSON.parse(job[key]))
// userId are always strings, even if the value is numeric, which might to
// them being parsed as numbers.
//
// The issue has been introduced by
// 48b2297bc151df582160be7c1bf1e8ee160320b8.
if (key === 'userId' && typeof value === 'number') {
job[key] = String(value)
}
})
} catch (_) {}
})
return job
}
const serialize = job => {
Object.keys(job).forEach(key => {
const value = job[key]
if (typeof value !== 'string') {
job[key] = JSON.stringify(job[key])
}
})
return job
}
class JobsDb extends Collection {
async create(job) {
return normalize(await this.add(serialize(job)))
}
_unserialize(job) {
Object.keys(job).forEach(key => {
try {
const value = (job[key] = JSON.parse(job[key]))
async save(job) {
await this.update(serialize(job))
}
// userId are always strings, even if the value is numeric, which might to
// them being parsed as numbers.
//
// The issue has been introduced by
// 48b2297bc151df582160be7c1bf1e8ee160320b8.
if (key === 'userId' && typeof value === 'number') {
job[key] = String(value)
}
} catch (_) {}
})
async get(properties) {
const jobs = await super.get(properties)
jobs.forEach(normalize)
return jobs
}
}
@@ -74,7 +90,7 @@ export default class Jobs {
app.addConfigManager(
'jobs',
() => jobsDb.get(),
jobs => jobsDb.update(jobs),
jobs => Promise.all(jobs.map(job => jobsDb.save(job))),
['users']
)
})
@@ -134,7 +150,7 @@ export default class Jobs {
}
createJob(job) {
return this._jobs.add(job)
return this._jobs.create(job)
}
async updateJob(job, merge = true) {
@@ -143,7 +159,7 @@ export default class Jobs {
job = await this.getJob(id)
patch(job, props)
}
await this._jobs.update(job)
return /* await */ this._jobs.save(job)
}
registerJobExecutor(type, executor) {
@@ -171,7 +187,7 @@ export default class Jobs {
const runJobId = logger.notice(`Starting execution of ${id}.`, {
data:
type === 'backup' || type === 'metadataBackup' || type === 'mirrorBackup'
type === 'backup' || type === 'metadataBackup'
? {
mode: job.mode,
reportWhen: job.settings['']?.reportWhen ?? 'failure',

View File

@@ -154,7 +154,7 @@ export default class MigrateVm {
}
#connectToEsxi(host, user, password, sslVerify) {
return Task.run({ properties: { name: `connecting to ${host}` } }, async () => {
return Task.run({ data: { name: `connecting to ${host}` } }, async () => {
const esxi = new Esxi(host, user, password, sslVerify)
await fromEvent(esxi, 'ready')
return esxi
@@ -174,7 +174,7 @@ export default class MigrateVm {
const app = this._app
const esxi = await this.#connectToEsxi(host, user, password, sslVerify)
const esxiVmMetadata = await Task.run({ properties: { name: `get metadata of ${vmId}` } }, async () => {
const esxiVmMetadata = await Task.run({ data: { name: `get metadata of ${vmId}` } }, async () => {
return esxi.getTransferableVmMetadata(vmId)
})
@@ -182,7 +182,7 @@ export default class MigrateVm {
const isRunning = powerState !== 'poweredOff'
const chainsByNodes = await Task.run(
{ properties: { name: `build disks and snapshots chains for ${vmId}` } },
{ data: { name: `build disks and snapshots chains for ${vmId}` } },
async () => {
return this.#buildDiskChainByNode(disks, snapshots)
}
@@ -191,7 +191,7 @@ export default class MigrateVm {
const sr = app.getXapiObject(srId)
const xapi = sr.$xapi
const vm = await Task.run({ properties: { name: 'creating MV on XCP side' } }, async () => {
const vm = await Task.run({ data: { name: 'creating MV on XCP side' } }, async () => {
// got data, ready to start creating
const vm = await xapi._getOrWaitObject(
await xapi.VM_create({
@@ -236,7 +236,7 @@ export default class MigrateVm {
const vhds = await Promise.all(
Object.keys(chainsByNodes).map(async (node, userdevice) =>
Task.run({ properties: { name: `Cold import of disks ${node}` } }, async () => {
Task.run({ data: { name: `Cold import of disks ${node}` } }, async () => {
const chainByNode = chainsByNodes[node]
const vdi = await xapi._getOrWaitObject(
await xapi.VDI_create({
@@ -289,11 +289,11 @@ export default class MigrateVm {
if (isRunning && stopSource) {
// it the vm was running, we stop it and transfer the data in the active disk
await Task.run({ properties: { name: 'powering down source VM' } }, () => esxi.powerOff(vmId))
await Task.run({ data: { name: 'powering down source VM' } }, () => esxi.powerOff(vmId))
await Promise.all(
Object.keys(chainsByNodes).map(async (node, userdevice) => {
await Task.run({ properties: { name: `Transfering deltas of ${userdevice}` } }, async () => {
await Task.run({ data: { name: `Transfering deltas of ${userdevice}` } }, async () => {
const chainByNode = chainsByNodes[node]
const disk = chainByNode[chainByNode.length - 1]
const { fileName, path, datastore, isFull } = disk
@@ -322,7 +322,7 @@ export default class MigrateVm {
)
}
await Task.run({ properties: { name: 'Finishing transfer' } }, async () => {
await Task.run({ data: { name: 'Finishing transfer' } }, async () => {
// remove the importing in label
await vm.set_name_label(esxiVmMetadata.name_label)

View File

@@ -47,7 +47,7 @@ export default class {
app.addConfigManager(
'remotes',
() => this._remotes.get(),
remotes => this._remotes.update(remotes)
remotes => Promise.all(remotes.map(remote => this._remotes.update(remote)))
)
})
app.hooks.on('start', async () => {

View File

@@ -7,16 +7,23 @@ import { noSuchObject } from 'xo-common/api-errors.js'
import Collection from '../collection/redis.mjs'
import patch from '../patch.mjs'
const normalize = schedule => {
const { enabled } = schedule
if (typeof enabled !== 'boolean') {
schedule.enabled = enabled === 'true'
}
if ('job' in schedule) {
schedule.jobId = schedule.job
delete schedule.job
}
return schedule
}
class Schedules extends Collection {
_unserialize(schedule) {
const { enabled } = schedule
if (typeof enabled !== 'boolean') {
schedule.enabled = enabled === 'true'
}
if ('job' in schedule) {
schedule.jobId = schedule.job
delete schedule.job
}
async get(properties) {
const schedules = await super.get(properties)
schedules.forEach(normalize)
return schedules
}
}
@@ -48,7 +55,7 @@ export default class Scheduling {
() => db.get(),
schedules =>
asyncMapSettled(schedules, async schedule => {
await db.update(schedule)
await db.update(normalize(schedule))
this._start(schedule.id)
}),
['jobs']

View File

@@ -39,7 +39,7 @@ export default class {
app.addConfigManager(
'groups',
() => groupsDb.get(),
groups => groupsDb.update(groups),
groups => Promise.all(groups.map(group => groupsDb.update(group))),
['users']
)
app.addConfigManager(
@@ -83,7 +83,10 @@ export default class {
properties.pw_hash = await hash(password)
}
return this._users.create(properties)
// TODO: use plain objects
const user = await this._users.create(properties)
return user
}
async deleteUser(id) {
@@ -333,8 +336,11 @@ export default class {
// -----------------------------------------------------------------
createGroup({ name, provider, providerGroupId }) {
return this._groups.create(name, provider, providerGroupId)
async createGroup({ name, provider, providerGroupId }) {
// TODO: use plain objects.
const group = await this._groups.create(name, provider, providerGroupId)
return group
}
async deleteGroup(id) {

View File

@@ -2703,8 +2703,6 @@ const messages = {
proxiesNeedUpgrade: 'Some proxies need to be upgraded.',
upgradeNeededForProxies: 'Some proxies need to be upgraded. Click here to get more information.',
xoProxyConcreteGuide: 'XO Proxy: a concrete guide',
someProxiesHaveErrors:
'{n, number} prox{n, plural, one {y} other {ies}} ha{n, plural, one {s} other {ve}} error{n, plural, one {} other {s}}',
// ----- Utils -----
secondsFormat: '{seconds, plural, one {# second} other {# seconds}}',

View File

@@ -512,14 +512,7 @@ subscribeHostMissingPatches.forceRefresh = host => {
const proxiesApplianceUpdaterState = {}
export const subscribeProxiesApplianceUpdaterState = (proxyId, cb) => {
if (proxiesApplianceUpdaterState[proxyId] === undefined) {
proxiesApplianceUpdaterState[proxyId] = createSubscription(async () => {
try {
return await getProxyApplianceUpdaterState(proxyId)
} catch (error) {
console.error(error)
return { state: 'error' }
}
})
proxiesApplianceUpdaterState[proxyId] = createSubscription(() => getProxyApplianceUpdaterState(proxyId))
}
return proxiesApplianceUpdaterState[proxyId](cb)
}

View File

@@ -32,7 +32,7 @@ import {
getXoaState,
isAdmin,
} from 'selectors'
import { countBy, every, forEach, identity, isEmpty, isEqual, map, pick, size, some } from 'lodash'
import { every, forEach, identity, isEmpty, isEqual, map, pick, size, some } from 'lodash'
import styles from './index.css'
@@ -111,10 +111,7 @@ export default class Menu extends Component {
() => this.state.proxyStates,
proxyStates => some(proxyStates, state => state.endsWith('-upgrade-needed'))
)
_getNProxiesErrors = createSelector(
() => this.state.proxyStates,
proxyStates => countBy(proxyStates).error
)
_checkPermissions = createSelector(
() => this.props.isAdmin,
() => this.props.permissions,
@@ -216,7 +213,6 @@ export default class Menu extends Component {
const noOperatablePools = this._getNoOperatablePools()
const noResourceSets = this._getNoResourceSets()
const noNotifications = this._getNoNotifications()
const nProxiesErrors = this._getNProxiesErrors()
const missingPatchesWarning = this._hasMissingPatches() ? (
<Tooltip content={_('homeMissingPatches')}>
@@ -472,10 +468,6 @@ export default class Menu extends Component {
]}
/>
</Tooltip>
) : nProxiesErrors > 0 ? (
<Tooltip content={_('someProxiesHaveErrors', { n: nProxiesErrors })}>
<span className='tag tag-pill tag-danger'>{nProxiesErrors}</span>
</Tooltip>
) : null,
],
},

1474
yarn.lock

File diff suppressed because it is too large Load Diff