feat(lite): upgrade deps + root eslint config (#7292)

This commit is contained in:
Thierry Goettelmann 2024-01-15 11:12:53 +01:00 committed by GitHub
parent b0c37df8d7
commit ea19b0851f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
301 changed files with 7192 additions and 8639 deletions

View File

@ -15,9 +15,10 @@ module.exports = {
overrides: [
{
files: ['cli.{,c,m}js', '*-cli.{,c,m}js', '**/*cli*/**/*.{,c,m}js'],
files: ['cli.{,c,m}js', '*-cli.{,c,m}js', '**/*cli*/**/*.{,c,m}js', '**/scripts/**.{,c,m}js'],
rules: {
'n/no-process-exit': 'off',
'n/shebang': 'off',
'no-console': 'off',
},
},
@ -46,6 +47,38 @@ module.exports = {
],
},
},
{
files: ['@xen-orchestra/lite/**/*.{vue,ts}'],
parserOptions: {
sourceType: 'module',
},
plugins: ['import'],
extends: [
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:vue/vue3-recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier',
],
settings: {
'import/resolver': {
typescript: true,
'eslint-import-resolver-custom-alias': {
alias: {
'@': './src',
},
extensions: ['.ts'],
packages: ['@xen-orchestra/lite'],
},
},
},
rules: {
'no-void': 'off',
'n/no-missing-import': 'off', // using 'import' plugin instead to support TS aliases
'@typescript-eslint/no-explicit-any': 'off',
'vue/require-default-prop': 'off', // https://github.com/vuejs/eslint-plugin-vue/issues/2051
},
},
],
parserOptions: {

1
.gitignore vendored
View File

@ -30,6 +30,7 @@ pnpm-debug.log.*
yarn-error.log
yarn-error.log.*
.env
*.tsbuildinfo
# code coverage
.nyc_output/

View File

@ -1,29 +0,0 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution");
module.exports = {
globals: {
XO_LITE_GIT_HEAD: true,
XO_LITE_VERSION: true,
},
root: true,
env: {
node: true,
},
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-typescript/recommended",
"@vue/eslint-config-prettier",
],
plugins: ["@limegrass/import-alias"],
ignorePatterns: ["scripts/*.mjs"],
rules: {
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-explicit-any": "off",
"@limegrass/import-alias/import-alias": [
"error",
{ aliasConfigPath: require("path").join(__dirname, "tsconfig.json") },
],
},
};

View File

@ -1,4 +0,0 @@
// Keeping this file to prevent applying the global monorepo config for now
module.exports = {
trailingComma: "es5",
};

View File

@ -48,18 +48,16 @@ Note: When reading Vue official doc, don't forget to set "API Preference" toggle
```vue
<script lang="ts" setup>
import { computed, ref } from "vue";
import { computed, ref } from 'vue'
const props = defineProps<{
greetings: string;
}>();
greetings: string
}>()
const firstName = ref("");
const lastName = ref("");
const firstName = ref('')
const lastName = ref('')
const fullName = computed(
() => `${props.greetings} ${firstName.value} ${lastName.value}`
);
const fullName = computed(() => `${props.greetings} ${firstName.value} ${lastName.value}`)
</script>
```
@ -73,9 +71,9 @@ Vue variables can be interpolated with `v-bind`.
```vue
<script lang="ts" setup>
import { ref } from "vue";
import { ref } from 'vue'
const fontSize = ref("2rem");
const fontSize = ref('2rem')
</script>
<style scoped>
@ -105,8 +103,8 @@ Use the `busy` prop to display a loader icon.
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import { faDisplay } from '@fortawesome/free-solid-svg-icons'
</script>
```
@ -140,21 +138,21 @@ For a `foobar` store, create a `store/foobar.store.ts` then use `defineStore('fo
#### Example
```typescript
import { computed, ref } from "vue";
import { computed, ref } from 'vue'
export const useFoobarStore = defineStore("foobar", () => {
const aStateVar = ref(0);
const otherStateVar = ref(0);
const aGetter = computed(() => aStateVar.value * 2);
const anAction = () => (otherStateVar.value += 10);
export const useFoobarStore = defineStore('foobar', () => {
const aStateVar = ref(0)
const otherStateVar = ref(0)
const aGetter = computed(() => aStateVar.value * 2)
const anAction = () => (otherStateVar.value += 10)
return {
aStateVar,
otherStateVar,
aGetter,
anAction,
};
});
}
})
```
### I18n

View File

@ -85,9 +85,9 @@ In your `.story.vue` file, import and use the `ComponentStory` component.
</template>
<script lang="ts" setup>
import MyComponent from "@/components/MyComponent.vue";
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import { prop, event, model, slot, setting } from "@/libs/story/story-param";
import MyComponent from '@/components/MyComponent.vue'
import ComponentStory from '@/components/component-story/ComponentStory.vue'
import { prop, event, model, slot, setting } from '@/libs/story/story-param'
</script>
```
@ -119,27 +119,27 @@ Let's take this Vue component:
<script lang="ts" setup>
withDefaults(
defineProps<{
imString: string;
imNumber: number;
imOptional?: string;
imOptionalWithDefault?: string;
modelValue?: string;
customModel?: number;
imString: string
imNumber: number
imOptional?: string
imOptionalWithDefault?: string
modelValue?: string
customModel?: number
}>(),
{ imOptionalWithDefault: "Hi World" }
);
{ imOptionalWithDefault: 'Hi World' }
)
const emit = defineEmits<{
(event: "click"): void;
(event: "clickWithArg", id: string): void;
(event: "update:modelValue", value: string): void;
(event: "update:customModel", value: number): void;
}>();
(event: 'click'): void
(event: 'clickWithArg', id: string): void
(event: 'update:modelValue', value: string): void
(event: 'update:customModel', value: number): void
}>()
const moonDistance = 384400;
const moonDistance = 384400
const handleClick = () => emit("click");
const handleClickWithArg = (id: string) => emit("clickWithArg", id);
const handleClick = () => emit('click')
const handleClickWithArg = (id: string) => emit('clickWithArg', id)
</script>
```
@ -150,53 +150,33 @@ Here is how to document it with a Component Story:
<ComponentStory
v-slot="{ properties, settings }"
:params="[
prop('imString')
.str()
.required()
.preset('Example')
.widget()
.help('This is a required string prop'),
prop('imNumber')
.num()
.required()
.preset(42)
.widget()
.help('This is a required number prop'),
prop('imString').str().required().preset('Example').widget().help('This is a required string prop'),
prop('imNumber').num().required().preset(42).widget().help('This is a required number prop'),
prop('imOptional').str().widget().help('This is an optional string prop'),
prop('imOptionalWithDefault')
.str()
.default('Hi World')
.widget()
.default('My default value'),
model().prop((p) => p.str()),
model('customModel').prop((p) => p.num()),
prop('imOptionalWithDefault').str().default('Hi World').widget().default('My default value'),
model().prop(p => p.str()),
model('customModel').prop(p => p.num()),
event('click').help('Emitted when the user clicks the first button'),
event('clickWithArg')
.args({ id: 'string' })
.help('Emitted when the user clicks the second button'),
event('clickWithArg').args({ id: 'string' }).help('Emitted when the user clicks the second button'),
slot().help('This is the default slot'),
slot('namedSlot').help('This is a named slot'),
slot('namedScopedSlot')
.prop('moon-distance', 'number')
.help('This is a named slot'),
slot('namedScopedSlot').prop('moon-distance', 'number').help('This is a named slot'),
setting('contentExample').widget(text()).preset('Some content'),
]"
>
<MyComponent v-bind="properties">
{{ settings.contentExample }}
<template #named-slot>Named slot content</template>
<template #named-scoped-slot="{ moonDistance }">
Moon distance is {{ moonDistance }} meters.
</template>
<template #named-scoped-slot="{ moonDistance }"> Moon distance is {{ moonDistance }} meters. </template>
</MyComponent>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from "@/components/component-story/ComponentStory.vue";
import MyComponent from "@/components/MyComponent.vue";
import { event, model, prop, setting, slot } from "@/libs/story/story-param";
import { text } from "@/libs/story/story-widget";
import ComponentStory from '@/components/component-story/ComponentStory.vue'
import MyComponent from '@/components/MyComponent.vue'
import { event, model, prop, setting, slot } from '@/libs/story/story-param'
import { text } from '@/libs/story/story-widget'
</script>
```

View File

@ -20,45 +20,45 @@ This will return an object with the following methods:
### Static modal
```ts
useModal(MyModal);
useModal(MyModal)
```
### Modal with props
```ts
useModal(MyModal, { message: "Hello world!" });
useModal(MyModal, { message: 'Hello world!' })
```
### Handle modal approval
```ts
const { onApprove } = useModal(MyModal, { message: "Hello world!" });
const { onApprove } = useModal(MyModal, { message: 'Hello world!' })
onApprove(() => console.log("Modal approved"));
onApprove(() => console.log('Modal approved'))
```
### Handle modal approval with payload
```ts
const { onApprove } = useModal(MyModal, { message: "Hello world!" });
const { onApprove } = useModal(MyModal, { message: 'Hello world!' })
onApprove((payload) => console.log("Modal approved with payload", payload));
onApprove(payload => console.log('Modal approved with payload', payload))
```
### Handle modal decline
```ts
const { onDecline } = useModal(MyModal, { message: "Hello world!" });
const { onDecline } = useModal(MyModal, { message: 'Hello world!' })
onDecline(() => console.log("Modal declined"));
onDecline(() => console.log('Modal declined'))
```
### Handle modal close
```ts
const { onClose } = useModal(MyModal, { message: "Hello world!" });
const { onClose } = useModal(MyModal, { message: 'Hello world!' })
onClose(() => console.log("Modal closed"));
onClose(() => console.log('Modal closed'))
```
## Modal controller
@ -66,7 +66,7 @@ onClose(() => console.log("Modal closed"));
Inside the modal component, you can inject the modal controller with `inject(IK_MODAL)!`.
```ts
const modal = inject(IK_MODAL)!;
const modal = inject(IK_MODAL)!
```
You can then use the following properties and methods on the `modal` object:

View File

@ -36,7 +36,7 @@ They are stored in `src/composables/xen-api-collection/*-collection.composable.t
```typescript
// src/composables/xen-api-collection/console-collection.composable.ts
export const useConsoleCollection = () => useXenApiCollection("console");
export const useConsoleCollection = () => useXenApiCollection('console')
```
If you want to allow the user to defer the subscription, you can propagate the options to `useXenApiCollection`.
@ -44,19 +44,16 @@ If you want to allow the user to defer the subscription, you can propagate the o
```typescript
// console-collection.composable.ts
export const useConsoleCollection = <
Immediate extends boolean = true,
>(options?: {
immediate?: Immediate;
}) => useXenApiCollection("console", options);
export const useConsoleCollection = <Immediate extends boolean = true>(options?: { immediate?: Immediate }) =>
useXenApiCollection('console', options)
```
```typescript
// MyComponent.vue
const collection = useConsoleCollection({ immediate: false });
const collection = useConsoleCollection({ immediate: false })
setTimeout(() => collection.start(), 10000);
setTimeout(() => collection.start(), 10000)
```
## Alter the collection
@ -68,10 +65,10 @@ You can alter the collection by overriding parts of it.
```typescript
// xen-api.ts
export interface XenApiConsole extends XenApiRecord<"console"> {
export interface XenApiConsole extends XenApiRecord<'console'> {
// ... existing props
someProp: string;
someOtherProp: number;
someProp: string
someOtherProp: number
}
```
@ -79,27 +76,27 @@ export interface XenApiConsole extends XenApiRecord<"console"> {
// console-collection.composable.ts
export const useConsoleCollection = () => {
const collection = useXenApiCollection("console");
const collection = useXenApiCollection('console')
const records = computed(() => {
return collection.records.value.map((console) => ({
return collection.records.value.map(console => ({
...console,
someProp: "Some value",
someProp: 'Some value',
someOtherProp: 42,
}));
});
}))
})
return {
...collection,
records,
};
};
}
}
```
```typescript
const consoleCollection = useConsoleCollection();
const consoleCollection = useConsoleCollection()
consoleCollection.getByUuid("...").someProp; // "Some value"
consoleCollection.getByUuid('...').someProp // "Some value"
```
### Example 2: Adding props to the collection
@ -108,17 +105,13 @@ consoleCollection.getByUuid("...").someProp; // "Some value"
// vm-collection.composable.ts
export const useVmCollection = () => {
const collection = useXenApiCollection("VM");
const collection = useXenApiCollection('VM')
return {
...collection,
runningVms: computed(() =>
collection.records.value.filter(
(vm) => vm.power_state === POWER_STATE.RUNNING
)
),
};
};
runningVms: computed(() => collection.records.value.filter(vm => vm.power_state === POWER_STATE.RUNNING)),
}
}
```
### Example 3, filtering and sorting the collection
@ -127,18 +120,15 @@ export const useVmCollection = () => {
// vm-collection.composable.ts
export const useVmCollection = () => {
const collection = useXenApiCollection("VM");
const collection = useXenApiCollection('VM')
return {
...collection,
records: computed(() =>
collection.records.value
.filter(
(vm) =>
!vm.is_a_snapshot && !vm.is_a_template && !vm.is_control_domain
)
.filter(vm => !vm.is_a_snapshot && !vm.is_a_template && !vm.is_control_domain)
.sort((vm1, vm2) => vm1.name_label.localeCompare(vm2.name_label))
),
};
};
}
}
```

View File

@ -2,5 +2,5 @@
/// <reference types="json-rpc-2.0/dist" />
/// <reference types="vite-plugin-pages/client" />
declare const XO_LITE_VERSION: string;
declare const XO_LITE_GIT_HEAD: string;
declare const XO_LITE_VERSION: string
declare const XO_LITE_GIT_HEAD: string

View File

@ -1,43 +1,41 @@
{
"name": "@xen-orchestra/lite",
"version": "0.1.7",
"type": "module",
"scripts": {
"dev": "GIT_HEAD=$(git rev-parse HEAD) vite",
"build": "run-p type-check build-only",
"preview": "vite preview --port 4173",
"release": "zx ./scripts/release.mjs",
"release": "./scripts/release.mjs",
"build-only": "yarn release --build",
"deploy": "yarn release --build --deploy",
"gh-release": "yarn release --build --tarball --gh-release",
"test": "yarn run type-check",
"type-check": "vue-tsc --noEmit"
"type-check": "vue-tsc --build --force tsconfig.type-check.json"
},
"devDependencies": {
"@fontsource/poppins": "^5.0.8",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-regular-svg-icons": "^6.4.2",
"@fortawesome/free-solid-svg-icons": "^6.4.2",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-regular-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.5",
"@intlify/unplugin-vue-i18n": "^1.5.0",
"@limegrass/eslint-plugin-import-alias": "^1.1.0",
"@intlify/unplugin-vue-i18n": "^2.0.0",
"@novnc/novnc": "^1.4.0",
"@rushstack/eslint-patch": "^1.5.1",
"@tsconfig/node18": "^18.2.2",
"strip-json-comments": "^5.0.1",
"@types/d3-time-format": "^4.0.3",
"@types/file-saver": "^2.0.7",
"@types/lodash-es": "^4.17.11",
"@types/node": "^18.18.9",
"@vitejs/plugin-vue": "^4.4.0",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.4.0",
"@vueuse/core": "^10.5.0",
"@vueuse/math": "^10.5.0",
"@types/lodash-es": "^4.17.12",
"@types/node": "^18.19.5",
"@vitejs/plugin-vue": "^5.0.2",
"@vue/tsconfig": "^0.5.1",
"@vueuse/core": "^10.7.1",
"@vueuse/math": "^10.7.1",
"@vueuse/shared": "^10.7.1",
"complex-matcher": "^0.7.1",
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",
"echarts": "^5.4.3",
"eslint-plugin-vue": "^9.18.1",
"file-saver": "^2.0.5",
"highlight.js": "^11.9.0",
"human-format": "^1.2.0",
@ -48,17 +46,18 @@
"lodash-es": "^4.17.21",
"make-error": "^1.3.6",
"marked": "^9.1.5",
"minimist": "^1.2.8",
"npm-run-all": "^4.1.5",
"pinia": "^2.1.7",
"placement.js": "^1.0.0-beta.5",
"postcss": "^8.4.31",
"postcss": "^8.4.33",
"postcss-custom-media": "^10.0.2",
"postcss-nested": "^6.0.1",
"typescript": "^5.2.2",
"vite": "^4.5.0",
"vue": "^3.3.8",
"vue-echarts": "^6.6.1",
"vue-i18n": "^9.6.5",
"typescript": "^5.3.3",
"vite": "^5.0.11",
"vue": "^3.4.7",
"vue-echarts": "^6.6.8",
"vue-i18n": "^9.9.0",
"vue-router": "^4.2.5",
"vue-tsc": "^1.8.22",
"zx": "^7.2.3"

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: {
"postcss-nested": {},
"postcss-custom-media": {},
},
};

View File

@ -0,0 +1,6 @@
export default {
plugins: {
'postcss-nested': {},
'postcss-custom-media': {},
},
}

281
@xen-orchestra/lite/scripts/release.mjs Normal file → Executable file
View File

@ -1,53 +1,34 @@
#!/usr/bin/env zx
#!/usr/bin/env node
import argv from "minimist";
import { tmpdir } from "os";
import argv from 'minimist'
import { tmpdir } from 'os'
import { fileURLToPath, URL } from 'url'
import { $, cd, chalk, fs, path, question, within } from 'zx'
$.verbose = false;
$.verbose = false
const DEPLOY_SERVER = "www-xo.gpn.vates.fr";
const DEPLOY_SERVER = 'www-xo.gpn.vates.fr'
const { version: pkgVersion } = await fs.readJson("./package.json");
const { version: pkgVersion } = await fs.readJson('./package.json')
const opts = argv(process.argv, {
boolean: ["help", "build", "deploy", "ghRelease", "tarball"],
string: [
"base",
"dist",
"ghToken",
"tarballDest",
"tarballName",
"username",
"version",
],
boolean: ['help', 'build', 'deploy', 'ghRelease', 'tarball'],
string: ['base', 'dist', 'ghToken', 'tarballDest', 'tarballName', 'username', 'version'],
alias: {
u: "username",
h: "help",
"gh-release": "ghRelease",
"gh-token": "ghToken",
"tarball-dest": "tarballDest",
"tarball-name": "tarballName",
u: 'username',
h: 'help',
'gh-release': 'ghRelease',
'gh-token': 'ghToken',
'tarball-dest': 'tarballDest',
'tarball-name': 'tarballName',
},
default: {
dist: "dist",
dist: 'dist',
version: pkgVersion,
},
});
})
let {
base,
build,
deploy,
dist,
ghRelease,
ghToken,
help,
tarball,
tarballDest,
tarballName,
username,
version,
} = opts;
let { base, build, deploy, dist, ghRelease, ghToken, help, tarball, tarballDest, tarballName, username, version } = opts
const usage = () => {
console.log(
@ -78,172 +59,164 @@ const usage = () => {
--username|-u <LDAP username>
]
`
);
};
if (help) {
usage();
process.exit(0);
)
}
const yes = async (q) =>
["y", "yes"].includes((await question(q + " [y/N] ")).toLowerCase());
if (help) {
usage()
process.exit(0)
}
const no = async (q) => !(await yes(q));
const yes = async q => ['y', 'yes'].includes((await question(q + ' [y/N] ')).toLowerCase())
const step = (s) => console.log(chalk.green.bold(`\n${s}\n`));
const no = async q => !(await yes(q))
const step = s => console.log(chalk.green.bold(`\n${s}\n`))
const stop = () => {
console.log(chalk.yellow("Stopping"));
process.exit(0);
};
console.log(chalk.yellow('Stopping'))
process.exit(0)
}
const ghApiCall = async (path, method = "GET", data) => {
const ghApiCall = async (path, method = 'GET', data) => {
const opts = {
method,
headers: {
Accept: "application/vnd.github+json",
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${ghToken}`,
"X-GitHub-Api-Version": "2022-11-28",
'X-GitHub-Api-Version': '2022-11-28',
},
};
if (data !== undefined) {
opts.body = typeof data === "object" ? JSON.stringify(data) : data;
}
const res = await fetch(
"https://api.github.com/repos/vatesfr/xen-orchestra" + path,
opts
);
if (data !== undefined) {
opts.body = typeof data === 'object' ? JSON.stringify(data) : data
}
const res = await fetch('https://api.github.com/repos/vatesfr/xen-orchestra' + path, opts)
if (res.status === 404 || res.status === 422) {
return;
return
}
if (!res.ok) {
console.log(chalk.red(await res.text()));
throw new Error(`GitHub API error: ${res.statusText}`);
console.log(chalk.red(await res.text()))
throw new Error(`GitHub API error: ${res.statusText}`)
}
try {
// Return undefined if response is not JSON
return JSON.parse(await res.text());
return JSON.parse(await res.text())
} catch {}
};
}
const ghApiUploadReleaseAsset = async (releaseId, assetName, file) => {
const opts = {
method: "POST",
method: 'POST',
body: fs.createReadStream(file),
headers: {
Accept: "application/vnd.github+json",
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${ghToken}`,
"Content-Length": (await fs.stat(file)).size,
"Content-Type": "application/vnd.cncf.helm.chart.content.v1.tar+gzip",
"X-GitHub-Api-Version": "2022-11-28",
'Content-Length': (await fs.stat(file)).size,
'Content-Type': 'application/vnd.cncf.helm.chart.content.v1.tar+gzip',
'X-GitHub-Api-Version': '2022-11-28',
},
};
}
const res = await fetch(
`https://uploads.github.com/repos/vatesfr/xen-orchestra/releases/${releaseId}/assets?name=${encodeURIComponent(
assetName
)}`,
opts
);
)
if (!res.ok) {
console.log(chalk.red(await res.text()));
throw new Error(`GitHub API error: ${res.statusText}`);
console.log(chalk.red(await res.text()))
throw new Error(`GitHub API error: ${res.statusText}`)
}
return JSON.parse(await res.text());
};
return JSON.parse(await res.text())
}
// Validate args and assign defaults -------------------------------------------
const headSha = (await $`git rev-parse HEAD`).stdout.trim();
const headSha = (await $`git rev-parse HEAD`).stdout.trim()
if (!build && !deploy && !tarball && !ghRelease) {
console.log(
chalk.yellow(
"Nothing to do! Use --build, --deploy, --tarball and/or --gh-release"
)
);
process.exit(0);
console.log(chalk.yellow('Nothing to do! Use --build, --deploy, --tarball and/or --gh-release'))
process.exit(0)
}
if (deploy && ghRelease) {
throw new Error("--deploy and --gh-release cannot be used together");
throw new Error('--deploy and --gh-release cannot be used together')
}
if (deploy && username === undefined) {
throw new Error("--username is required when --deploy is used");
throw new Error('--username is required when --deploy is used')
}
if (ghRelease && ghToken === undefined) {
throw new Error("--gh-token is required to upload a release to GitHub");
throw new Error('--gh-token is required to upload a release to GitHub')
}
if (base === undefined) {
base = deploy ? "https://lite.xen-orchestra.com/dist/" : "/";
base = deploy ? 'https://lite.xen-orchestra.com/dist/' : '/'
}
if (tarball) {
if (tarballDest === undefined) {
tarballDest = path.join(tmpdir(), `xo-lite-${new Date().toISOString()}`);
tarballDest = path.join(tmpdir(), `xo-lite-${new Date().toISOString()}`)
}
if (tarballName === undefined) {
tarballName = `xo-lite-${version}.tar.gz`;
tarballName = `xo-lite-${version}.tar.gz`
}
}
if (tarballDest !== undefined) {
tarballDest = path.resolve(tarballDest);
tarballDest = path.resolve(tarballDest)
}
if (ghRelease && (tarballDest === undefined || tarballName === undefined)) {
throw new Error(
"In order to release to GitHub, either use --tarball to generate the tarball or provide the tarball with --tarball-dest and --tarball-name"
);
'In order to release to GitHub, either use --tarball to generate the tarball or provide the tarball with --tarball-dest and --tarball-name'
)
}
let tarballPath;
let tarballExists = false;
let tarballPath
let tarballExists = false
if (tarballDest !== undefined && tarballName !== undefined) {
tarballPath = path.join(tarballDest, tarballName);
tarballPath = path.join(tarballDest, tarballName)
try {
if ((await fs.stat(tarballPath)).isFile()) {
tarballExists = true;
tarballExists = true
}
} catch (err) {
if (err.code !== "ENOENT") {
throw err;
if (err.code !== 'ENOENT') {
throw err
}
}
}
if (ghRelease && !tarball && !tarballExists) {
throw new Error(`No such file ${tarballPath}`);
throw new Error(`No such file ${tarballPath}`)
}
if (tarball && tarballExists) {
if (await no(`Tarball ${tarballPath} already exists. Overwrite?`)) {
stop();
stop()
}
}
const tag = `xo-lite-v${version}`;
const tag = `xo-lite-v${version}`
if (ghRelease) {
const remoteTag = await ghApiCall(`/git/ref/tags/${encodeURIComponent(tag)}`);
const remoteTag = await ghApiCall(`/git/ref/tags/${encodeURIComponent(tag)}`)
if (remoteTag === undefined) {
if ((await ghApiCall(`/commits/${headSha}`)) === undefined) {
throw new Error(
`Tag ${tag} and commit ${headSha} not found on GitHub. At least one needs to exist to use it as a release target.`
);
)
}
if (
@ -251,7 +224,7 @@ if (ghRelease) {
`Tag ${tag} not found on GitHub. The GitHub release will be attached to the current commit and the tag will be created automatically when the release is published. Continue?`
)
) {
stop();
stop()
}
} else {
if (
@ -260,17 +233,14 @@ if (ghRelease) {
`Commit SHA of tag ${tag} on GitHub (${remoteTag.object.sha}) is different from current commit SHA (${headSha}). Continue?`
))
) {
stop();
stop()
}
if (
!(await $`git tag --points-at HEAD`).stdout
.trim()
.split("\n")
.includes(tag) &&
!(await $`git tag --points-at HEAD`).stdout.trim().split('\n').includes(tag) &&
(await no(`Tag ${tag} not found on current commit. Continue?`))
) {
stop();
stop()
}
}
}
@ -278,67 +248,62 @@ if (ghRelease) {
// Build -----------------------------------------------------------------------
if (build) {
step("Build");
step('Build')
console.log(`Building XO Lite ${version} into ${dist}`);
console.log(`Building XO Lite ${version} into ${dist}`)
$.verbose = true;
$.verbose = true
await within(async () => {
cd("../..");
await $`yarn`;
});
await $`GIT_HEAD=${headSha} vite build --base=${base}`;
$.verbose = false;
cd('../..')
await $`yarn`
})
await $`GIT_HEAD=${headSha} vite build --base=${base}`
$.verbose = false
}
// License and index.js --------------------------------------------------------
if (ghRelease || deploy) {
step("Prepare dist");
step('Prepare dist')
if (ghRelease) {
console.log(`Adding LICENSE file to ${dist}`);
await fs.copy(
path.join(__dirname, "agpl-3.0.txt"),
path.join(dist, "LICENSE")
);
console.log(`Adding LICENSE file to ${dist}`)
await fs.copy(fileURLToPath(new URL('agpl-3.0.txt', import.meta.url)), path.join(dist, 'LICENSE'))
}
if (deploy) {
console.log(`Adding index.js file to ${dist}`);
console.log(`Adding index.js file to ${dist}`)
// Concatenate a URL (absolute or relative) and paths
// e.g.: joinUrl('http://example.com/', 'foo/bar') => 'http://example.com/foo/bar
// `path.join` isn't made for URLs and deduplicates the slashes in URL
// schemes (http:// becomes http:/). `.replace()` reverts this.
const joinUrl = (...parts) =>
path.join(...parts).replace(/^(https?:\/)/, "$1/");
const joinUrl = (...parts) => path.join(...parts).replace(/^(https?:\/)/, '$1/')
// Use of document.write is discouraged but seems to work consistently.
// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#document.write()
await fs.writeFile(
path.join(dist, "index.js"),
path.join(dist, 'index.js'),
`(async () => {
document.open();
document.write(
await (await fetch("${joinUrl(base, "index.html")}")).text()
await (await fetch("${joinUrl(base, 'index.html')}")).text()
);
document.close();
})();
`
);
)
}
}
// Tarball ---------------------------------------------------------------------
if (tarball) {
step("Tarball");
step('Tarball')
console.log(`Generating tarball ${tarballPath}`);
console.log(`Generating tarball ${tarballPath}`)
await fs.mkdirp(tarballDest);
await fs.mkdirp(tarballDest)
// The file is called xo-lite-X.Y.Z.tar.gz by default
// The archive contains the following tree:
@ -348,17 +313,15 @@ if (tarball) {
// ├ index.html
// ├ assets/
// └ ...
await $`tar -c -z -f ${tarballPath} --transform='s|^${dist}|xo-lite-${version}|' ${dist}`;
await $`tar -c -z -f ${tarballPath} --transform='s|^${dist}|xo-lite-${version}|' ${dist}`
}
// Create GitHub release -------------------------------------------------------
if (ghRelease) {
step("GitHub release");
step('GitHub release')
let release = (await ghApiCall("/releases")).find(
(release) => release.tag_name === tag
);
let release = (await ghApiCall('/releases')).find(release => release.tag_name === tag)
if (release !== undefined) {
if (
@ -368,37 +331,33 @@ if (ghRelease) {
)}). Skip and proceed with upload?`
)
) {
stop();
stop()
}
} else {
release = await ghApiCall("/releases", "POST", {
release = await ghApiCall('/releases', 'POST', {
tag_name: tag,
target_commitish: headSha,
name: tag,
draft: true,
});
})
console.log(
`Created GitHub release ${tag}: ${chalk.blue(release.html_url)}`
);
console.log(`Created GitHub release ${tag}: ${chalk.blue(release.html_url)}`)
}
console.log(`Uploading tarball ${tarballPath} to GitHub`);
console.log(`Uploading tarball ${tarballPath} to GitHub`)
let asset = release.assets.find((asset) => asset.name === tarballName);
let asset = release.assets.find(asset => asset.name === tarballName)
if (
asset !== undefined &&
(await yes(
`An asset called ${tarballName} already exists on that release. Replace it?`
))
(await yes(`An asset called ${tarballName} already exists on that release. Replace it?`))
) {
await ghApiCall(`/releases/assets/${asset.id}`, "DELETE");
asset = undefined;
await ghApiCall(`/releases/assets/${asset.id}`, 'DELETE')
asset = undefined
}
if (asset === undefined) {
console.log("Uploading…");
asset = await ghApiUploadReleaseAsset(release.id, tarballName, tarballPath);
console.log('Uploading…')
asset = await ghApiUploadReleaseAsset(release.id, tarballName, tarballPath)
}
if (release.draft) {
@ -406,18 +365,18 @@ if (ghRelease) {
chalk.yellow(
'The release is in DRAFT. To make it public, visit the release URL above, edit the release and click on "Publish release".'
)
);
)
}
}
// Deploy ----------------------------------------------------------------------
if (deploy) {
step("Deploy");
step('Deploy')
console.log(`Deploying XO Lite from ${dist} to ${DEPLOY_SERVER}`);
console.log(`Deploying XO Lite from ${dist} to ${DEPLOY_SERVER}`)
await $`rsync -r --delete ${dist}/ ${username}@${DEPLOY_SERVER}:xo-lite`;
await $`rsync -r --delete ${dist}/ ${username}@${DEPLOY_SERVER}:xo-lite`
console.log(`
XO Lite files sent to server
@ -430,5 +389,5 @@ if (deploy) {
Then run the following command to move the files to the \`latest\` folder:
\trsync -r --delete /home/${username}/xo-lite/ /home/xo-lite/public/latest
`);
`)
}

View File

@ -16,75 +16,67 @@
</template>
<script lang="ts" setup>
import favicon from "@/assets/favicon.svg";
import AppHeader from "@/components/AppHeader.vue";
import AppLogin from "@/components/AppLogin.vue";
import AppNavigation from "@/components/AppNavigation.vue";
import AppTooltips from "@/components/AppTooltips.vue";
import ModalList from "@/components/ui/modals/ModalList.vue";
import { useChartTheme } from "@/composables/chart-theme.composable";
import { useUnreachableHosts } from "@/composables/unreachable-hosts.composable";
import { useUiStore } from "@/stores/ui.store";
import { usePoolCollection } from "@/stores/xen-api/pool.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import { useActiveElement, useMagicKeys, whenever } from "@vueuse/core";
import { logicAnd } from "@vueuse/math";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import favicon from '@/assets/favicon.svg'
import AppHeader from '@/components/AppHeader.vue'
import AppLogin from '@/components/AppLogin.vue'
import AppNavigation from '@/components/AppNavigation.vue'
import AppTooltips from '@/components/AppTooltips.vue'
import ModalList from '@/components/ui/modals/ModalList.vue'
import { useChartTheme } from '@/composables/chart-theme.composable'
import { useUnreachableHosts } from '@/composables/unreachable-hosts.composable'
import { useUiStore } from '@/stores/ui.store'
import { usePoolCollection } from '@/stores/xen-api/pool.store'
import { useXenApiStore } from '@/stores/xen-api.store'
import { useActiveElement, useMagicKeys, whenever } from '@vueuse/core'
import { logicAnd } from '@vueuse/math'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
let link = document.querySelector(
"link[rel~='icon']"
) as HTMLLinkElement | null;
let link = document.querySelector("link[rel~='icon']") as HTMLLinkElement | null
if (link == null) {
link = document.createElement("link");
link.rel = "icon";
document.getElementsByTagName("head")[0].appendChild(link);
link = document.createElement('link')
link.rel = 'icon'
document.getElementsByTagName('head')[0].appendChild(link)
}
link.href = favicon;
link.href = favicon
const xenApiStore = useXenApiStore();
const xenApiStore = useXenApiStore()
const { pool } = usePoolCollection();
const { pool } = usePoolCollection()
useChartTheme();
const uiStore = useUiStore();
useChartTheme()
const uiStore = useUiStore()
if (import.meta.env.DEV) {
const { locale } = useI18n();
const activeElement = useActiveElement();
const { D, L } = useMagicKeys();
const { locale } = useI18n()
const activeElement = useActiveElement()
const { D, L } = useMagicKeys()
const canToggle = computed(() => {
if (activeElement.value == null) {
return true;
return true
}
return !["INPUT", "TEXTAREA"].includes(activeElement.value.tagName);
});
return !['INPUT', 'TEXTAREA'].includes(activeElement.value.tagName)
})
whenever(
logicAnd(D, canToggle),
() => (uiStore.colorMode = uiStore.colorMode === "dark" ? "light" : "dark")
);
whenever(logicAnd(D, canToggle), () => (uiStore.colorMode = uiStore.colorMode === 'dark' ? 'light' : 'dark'))
whenever(
logicAnd(L, canToggle),
() => (locale.value = locale.value === "en" ? "fr" : "en")
);
whenever(logicAnd(L, canToggle), () => (locale.value = locale.value === 'en' ? 'fr' : 'en'))
}
whenever(
() => pool.value?.$ref,
(poolRef) => {
xenApiStore.getXapi().startWatching(poolRef);
poolRef => {
xenApiStore.getXapi().startWatching(poolRef)
}
);
)
useUnreachableHosts();
useUnreachableHosts()
</script>
<style lang="postcss">
@import "@/assets/base.css";
@import '@/assets/base.css';
</style>
<style lang="postcss" scoped>

View File

@ -1,11 +1,11 @@
@import "reset.css";
@import "theme.css";
@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";
@import 'reset.css';
@import 'theme.css';
@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';
body {
min-height: 100vh;
@ -23,8 +23,7 @@ a {
code,
code *,
pre {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
}
.card-view {

View File

@ -50,13 +50,11 @@
--background-color-red-vates: #f5e8e9;
--shadow-100: 0 0.1rem 0.1rem rgba(20, 20, 30, 0.06);
--shadow-200: 0 0.1rem 0.3rem rgba(20, 20, 30, 0.1),
0 0.2rem 0.1rem rgba(20, 20, 30, 0.06),
--shadow-200: 0 0.1rem 0.3rem rgba(20, 20, 30, 0.1), 0 0.2rem 0.1rem rgba(20, 20, 30, 0.06),
0 0.1rem 0.1rem rgba(20, 20, 30, 0.08);
--shadow-300: 0 0.3rem 0.5rem rgba(20, 20, 30, 0.1),
0 0.1rem 1.8rem rgba(20, 20, 30, 0.06), 0 0.6rem 1rem rgba(20, 20, 30, 0.08);
--shadow-400: 0 1.1rem 1.5rem rgba(20, 20, 30, 0.1),
0 0.9rem 4.6rem rgba(20, 20, 30, 0.06),
--shadow-300: 0 0.3rem 0.5rem rgba(20, 20, 30, 0.1), 0 0.1rem 1.8rem rgba(20, 20, 30, 0.06),
0 0.6rem 1rem rgba(20, 20, 30, 0.08);
--shadow-400: 0 1.1rem 1.5rem rgba(20, 20, 30, 0.1), 0 0.9rem 4.6rem rgba(20, 20, 30, 0.06),
0 2.4rem 3.8rem rgba(20, 20, 30, 0.04);
}
@ -80,12 +78,10 @@
--background-color-red-vates: #3c1a28;
--shadow-100: 0 0.1rem 0.1rem rgba(20, 20, 30, 0.12);
--shadow-200: 0 0.1rem 0.3rem rgba(20, 20, 30, 0.2),
0 0.2rem 0.1rem rgba(20, 20, 30, 0.12),
--shadow-200: 0 0.1rem 0.3rem rgba(20, 20, 30, 0.2), 0 0.2rem 0.1rem rgba(20, 20, 30, 0.12),
0 0.1rem 0.1rem rgba(20, 20, 30, 0.16);
--shadow-300: 0 0.3rem 0.5rem rgba(20, 20, 30, 0.2),
0 0.1rem 1.8rem rgba(20, 20, 30, 0.12), 0 0.6rem 1rem rgba(20, 20, 30, 0.16);
--shadow-400: 0 1.1rem 1.5rem rgba(20, 20, 30, 0.2),
0 0.9rem 4.6rem rgba(20, 20, 30, 0.12),
--shadow-300: 0 0.3rem 0.5rem rgba(20, 20, 30, 0.2), 0 0.1rem 1.8rem rgba(20, 20, 30, 0.12),
0 0.6rem 1rem rgba(20, 20, 30, 0.16);
--shadow-400: 0 1.1rem 1.5rem rgba(20, 20, 30, 0.2), 0 0.9rem 4.6rem rgba(20, 20, 30, 0.12),
0 2.4rem 3.8rem rgba(20, 20, 30, 0.08);
}

View File

@ -6,54 +6,44 @@
<UiIcon :icon="faAngleDown" class="dropdown-icon" />
</button>
</template>
<MenuItem :icon="faGear" @click="openSettings">{{
$t("settings")
}}</MenuItem>
<MenuItem :icon="faGear" @click="openSettings">{{ $t('settings') }}</MenuItem>
<MenuItem :icon="faMessage" @click="openFeedbackUrl">
{{ $t("send-us-feedback") }}
{{ $t('send-us-feedback') }}
</MenuItem>
<MenuItem
:icon="faArrowRightFromBracket"
class="menu-item-logout"
@click="logout"
>
{{ $t("log-out") }}
<MenuItem :icon="faArrowRightFromBracket" class="menu-item-logout" @click="logout">
{{ $t('log-out') }}
</MenuItem>
</AppMenu>
</template>
<script lang="ts" setup>
import { nextTick } from "vue";
import { useRouter } from "vue-router";
import { nextTick } from 'vue'
import { useRouter } from 'vue-router'
import {
faAngleDown,
faArrowRightFromBracket,
faCircleUser,
faGear,
faMessage,
} from "@fortawesome/free-solid-svg-icons";
import AppMenu from "@/components/menu/AppMenu.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useXenApiStore } from "@/stores/xen-api.store";
} from '@fortawesome/free-solid-svg-icons'
import AppMenu from '@/components/menu/AppMenu.vue'
import MenuItem from '@/components/menu/MenuItem.vue'
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import { useXenApiStore } from '@/stores/xen-api.store'
const router = useRouter();
const router = useRouter()
const logout = () => {
const xenApiStore = useXenApiStore();
xenApiStore.disconnect();
nextTick(() => router.push({ name: "home" }));
};
const xenApiStore = useXenApiStore()
xenApiStore.disconnect()
nextTick(() => router.push({ name: 'home' }))
}
const openFeedbackUrl = () => {
window.open(
"https://xcp-ng.org/forum/topic/4731/xen-orchestra-lite",
"_blank",
"noopener"
);
};
window.open('https://xcp-ng.org/forum/topic/4731/xen-orchestra-lite', '_blank', 'noopener')
}
const openSettings = () => router.push({ name: "settings" });
const openSettings = () => router.push({ name: 'settings' })
</script>
<style scoped>

View File

@ -1,11 +1,6 @@
<template>
<header class="app-header">
<UiIcon
v-if="isMobile"
ref="navigationTrigger"
:icon="faBars"
class="toggle-navigation"
/>
<UiIcon v-if="isMobile" ref="navigationTrigger" :icon="faBars" class="toggle-navigation" />
<RouterLink :to="{ name: 'home' }">
<img v-if="isMobile" alt="XO Lite" src="../assets/logo.svg" />
<TextLogo v-else />
@ -14,7 +9,7 @@
<div class="right">
<PoolOverrideWarning as-tooltip />
<UiButton v-if="isDesktop" :icon="faDownload" @click="openXoaDeploy">
{{ $t("deploy-xoa") }}
{{ $t('deploy-xoa') }}
</UiButton>
<AccountButton />
</div>
@ -22,26 +17,26 @@
</template>
<script lang="ts" setup>
import AccountButton from "@/components/AccountButton.vue";
import PoolOverrideWarning from "@/components/PoolOverrideWarning.vue";
import TextLogo from "@/components/TextLogo.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useNavigationStore } from "@/stores/navigation.store";
import { useRouter } from "vue-router";
import { useUiStore } from "@/stores/ui.store";
import { faBars, faDownload } from "@fortawesome/free-solid-svg-icons";
import { storeToRefs } from "pinia";
import AccountButton from '@/components/AccountButton.vue'
import PoolOverrideWarning from '@/components/PoolOverrideWarning.vue'
import TextLogo from '@/components/TextLogo.vue'
import UiButton from '@/components/ui/UiButton.vue'
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import { useNavigationStore } from '@/stores/navigation.store'
import { useRouter } from 'vue-router'
import { useUiStore } from '@/stores/ui.store'
import { faBars, faDownload } from '@fortawesome/free-solid-svg-icons'
import { storeToRefs } from 'pinia'
const router = useRouter();
const router = useRouter()
const openXoaDeploy = () => router.push({ name: "xoa.deploy" });
const openXoaDeploy = () => router.push({ name: 'xoa.deploy' })
const uiStore = useUiStore();
const { isMobile, isDesktop } = storeToRefs(uiStore);
const uiStore = useUiStore()
const { isMobile, isDesktop } = storeToRefs(uiStore)
const navigationStore = useNavigationStore();
const { trigger: navigationTrigger } = storeToRefs(navigationStore);
const navigationStore = useNavigationStore()
const { trigger: navigationTrigger } = storeToRefs(navigationStore)
</script>
<style lang="postcss" scoped>

View File

@ -5,7 +5,7 @@
<PoolOverrideWarning />
<p v-if="isHostIsSlaveErr(error)" class="error">
<UiIcon :icon="faExclamationCircle" />
{{ $t("login-only-on-master") }}
{{ $t('login-only-on-master') }}
<a :href="masterUrl.href">{{ masterUrl.hostname }}</a>
</p>
<template v-else>
@ -13,10 +13,10 @@
<FormInput v-model="login" name="login" readonly type="text" />
</FormInputWrapper>
<FormInput
name="password"
ref="passwordRef"
type="password"
v-model="password"
name="password"
type="password"
:class="{ error: isInvalidPassword }"
:placeholder="$t('password')"
:readonly="isConnecting"
@ -25,10 +25,10 @@
<LoginError :error="error" />
<label class="remember-me-label">
<FormCheckbox v-model="rememberMe" />
{{ $t("keep-me-logged") }}
{{ $t('keep-me-logged') }}
</label>
<UiButton type="submit" :busy="isConnecting">
{{ $t("login") }}
{{ $t('login') }}
</UiButton>
</template>
</form>
@ -36,69 +36,68 @@
</template>
<script lang="ts" setup>
import { usePageTitleStore } from "@/stores/page-title.store";
import { storeToRefs } from "pinia";
import { onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useLocalStorage, whenever } from "@vueuse/core";
import { usePageTitleStore } from '@/stores/page-title.store'
import { storeToRefs } from 'pinia'
import { onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useLocalStorage, whenever } from '@vueuse/core'
import FormCheckbox from "@/components/form/FormCheckbox.vue";
import FormInput from "@/components/form/FormInput.vue";
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
import LoginError from "@/components/LoginError.vue";
import PoolOverrideWarning from "@/components/PoolOverrideWarning.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { XenApiError } from "@/libs/xen-api/xen-api.types";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
import { useXenApiStore } from "@/stores/xen-api.store";
import FormCheckbox from '@/components/form/FormCheckbox.vue'
import FormInput from '@/components/form/FormInput.vue'
import FormInputWrapper from '@/components/form/FormInputWrapper.vue'
import LoginError from '@/components/LoginError.vue'
import PoolOverrideWarning from '@/components/PoolOverrideWarning.vue'
import UiButton from '@/components/ui/UiButton.vue'
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import type { XenApiError } from '@/libs/xen-api/xen-api.types'
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'
import { useXenApiStore } from '@/stores/xen-api.store'
const { t } = useI18n();
usePageTitleStore().setTitle(t("login"));
const xenApiStore = useXenApiStore();
const { isConnecting } = storeToRefs(xenApiStore);
const login = ref("root");
const password = ref("");
const error = ref<XenApiError>();
const passwordRef = ref<InstanceType<typeof FormInput>>();
const isInvalidPassword = ref(false);
const masterUrl = ref(new URL(window.origin));
const rememberMe = useLocalStorage("rememberMe", false);
const { t } = useI18n()
usePageTitleStore().setTitle(t('login'))
const xenApiStore = useXenApiStore()
const { isConnecting } = storeToRefs(xenApiStore)
const login = ref('root')
const password = ref('')
const error = ref<XenApiError>()
const passwordRef = ref<InstanceType<typeof FormInput>>()
const isInvalidPassword = ref(false)
const masterUrl = ref(new URL(window.origin))
const rememberMe = useLocalStorage('rememberMe', false)
const focusPasswordInput = () => passwordRef.value?.focus();
const isHostIsSlaveErr = (err: XenApiError | undefined) =>
err?.message === "HOST_IS_SLAVE";
const focusPasswordInput = () => passwordRef.value?.focus()
const isHostIsSlaveErr = (err: XenApiError | undefined) => err?.message === 'HOST_IS_SLAVE'
onMounted(() => {
if (rememberMe.value) {
xenApiStore.reconnect();
xenApiStore.reconnect()
} else {
focusPasswordInput();
focusPasswordInput()
}
});
})
watch(password, () => {
isInvalidPassword.value = false;
error.value = undefined;
});
isInvalidPassword.value = false
error.value = undefined
})
whenever(
() => isHostIsSlaveErr(error.value),
() => (masterUrl.value.hostname = error.value!.data)
);
)
async function handleSubmit() {
try {
await xenApiStore.connect(login.value, password.value);
await xenApiStore.connect(login.value, password.value)
} catch (err: any) {
if (err.message === "SESSION_AUTHENTICATION_FAILED") {
focusPasswordInput();
isInvalidPassword.value = true;
if (err.message === 'SESSION_AUTHENTICATION_FAILED') {
focusPasswordInput()
isInvalidPassword.value = true
} else {
console.error(error);
console.error(error)
}
error.value = err;
error.value = err
}
}
</script>

View File

@ -1,39 +1,40 @@
<template>
<!-- eslint-disable vue/no-v-html -->
<div ref="rootElement" class="app-markdown" v-html="html" />
<!-- eslint-enable vue/no-v-html -->
</template>
<script lang="ts" setup>
import markdown from "@/libs/markdown";
import { useEventListener } from "@vueuse/core";
import { computed, type Ref, ref } from "vue";
import markdown from '@/libs/markdown'
import { useEventListener } from '@vueuse/core'
import { computed, type Ref, ref } from 'vue'
const rootElement = ref() as Ref<HTMLElement>;
const rootElement = ref() as Ref<HTMLElement>
const props = defineProps<{
content: string;
}>();
content: string
}>()
const html = computed(() => markdown.parse(props.content ?? ""));
const html = computed(() => markdown.parse(props.content ?? ''))
useEventListener(
rootElement,
"click",
'click',
(event: MouseEvent) => {
const target = event.target as HTMLElement;
const target = event.target as HTMLElement
if (!target.classList.contains("copy-button")) {
return;
if (!target.classList.contains('copy-button')) {
return
}
const copyable =
target.parentElement!.querySelector<HTMLElement>(".copyable");
const copyable = target.parentElement!.querySelector<HTMLElement>('.copyable')
if (copyable !== null) {
navigator.clipboard.writeText(copyable.innerText);
navigator.clipboard.writeText(copyable.innerText)
}
},
{ capture: true }
);
)
</script>
<style lang="postcss" scoped>

View File

@ -1,11 +1,6 @@
<template>
<transition name="slide">
<nav
v-if="isDesktop || isOpen"
ref="navElement"
:class="{ collapsible: isMobile }"
class="app-navigation"
>
<nav v-if="isDesktop || isOpen" ref="navElement" :class="{ collapsible: isMobile }" class="app-navigation">
<StoryMenu v-if="$route.meta.hasStoryNav" />
<InfraPoolList v-else />
</nav>
@ -13,34 +8,34 @@
</template>
<script lang="ts" setup>
import StoryMenu from "@/components/component-story/StoryMenu.vue";
import InfraPoolList from "@/components/infra/InfraPoolList.vue";
import { useNavigationStore } from "@/stores/navigation.store";
import { useUiStore } from "@/stores/ui.store";
import { onClickOutside, whenever } from "@vueuse/core";
import { storeToRefs } from "pinia";
import { ref } from "vue";
import StoryMenu from '@/components/component-story/StoryMenu.vue'
import InfraPoolList from '@/components/infra/InfraPoolList.vue'
import { useNavigationStore } from '@/stores/navigation.store'
import { useUiStore } from '@/stores/ui.store'
import { onClickOutside, whenever } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { ref } from 'vue'
const uiStore = useUiStore();
const { isMobile, isDesktop } = storeToRefs(uiStore);
const uiStore = useUiStore()
const { isMobile, isDesktop } = storeToRefs(uiStore)
const navigationStore = useNavigationStore();
const { isOpen, trigger } = storeToRefs(navigationStore);
const navigationStore = useNavigationStore()
const { isOpen, trigger } = storeToRefs(navigationStore)
const navElement = ref();
const navElement = ref()
whenever(isOpen, () => {
const unregisterEvent = onClickOutside(
navElement,
() => {
isOpen.value = false;
unregisterEvent?.();
isOpen.value = false
unregisterEvent?.()
},
{
ignore: [trigger],
}
);
});
)
})
</script>
<style lang="postcss" scoped>

View File

@ -6,33 +6,31 @@
</template>
<script lang="ts" setup>
import type { TooltipOptions } from "@/stores/tooltip.store";
import { isString } from "lodash-es";
import place from "placement.js";
import { computed, ref, watchEffect } from "vue";
import type { TooltipOptions } from '@/stores/tooltip.store'
import { isString } from 'lodash-es'
import place from 'placement.js'
import { computed, ref, watchEffect } from 'vue'
const props = defineProps<{
target: HTMLElement;
options: TooltipOptions;
}>();
target: HTMLElement
options: TooltipOptions
}>()
const tooltipElement = ref<HTMLElement>();
const tooltipElement = ref<HTMLElement>()
const isDisabled = computed(() =>
isString(props.options.content)
? props.options.content.trim() === ""
: props.options.content === false
);
isString(props.options.content) ? props.options.content.trim() === '' : props.options.content === false
)
const placement = computed(() => props.options.placement ?? "top");
const placement = computed(() => props.options.placement ?? 'top')
watchEffect(() => {
if (tooltipElement.value) {
place(props.target, tooltipElement.value, {
placement: placement.value,
});
})
}
});
})
</script>
<style lang="postcss" scoped>
@ -56,7 +54,7 @@ watchEffect(() => {
height: 1.875em;
}
[data-placement^="top"] {
[data-placement^='top'] {
margin-bottom: 0.625em;
.triangle {
@ -65,7 +63,7 @@ watchEffect(() => {
}
}
[data-placement^="right"] {
[data-placement^='right'] {
margin-left: 0.625em;
.triangle {
@ -74,7 +72,7 @@ watchEffect(() => {
}
}
[data-placement^="bottom"] {
[data-placement^='bottom'] {
margin-top: 0.625em;
.triangle {
@ -82,7 +80,7 @@ watchEffect(() => {
}
}
[data-placement^="left"] {
[data-placement^='left'] {
margin-right: 0.625em;
.triangle {
@ -91,51 +89,51 @@ watchEffect(() => {
}
}
[data-placement="top-start"] .triangle {
[data-placement='top-start'] .triangle {
left: 0;
}
[data-placement="top-center"] .triangle {
[data-placement='top-center'] .triangle {
left: 50%;
margin-left: -0.9375em;
}
[data-placement="top-end"] .triangle {
[data-placement='top-end'] .triangle {
right: 0;
}
[data-placement="left-start"] .triangle {
[data-placement='left-start'] .triangle {
top: -0.25em;
}
[data-placement="left-center"] .triangle {
[data-placement='left-center'] .triangle {
top: 50%;
margin-top: -0.9375em;
}
[data-placement="left-end"] .triangle {
[data-placement='left-end'] .triangle {
bottom: -0.25em;
}
[data-placement="right-start"] .triangle {
[data-placement='right-start'] .triangle {
top: -0.25em;
}
[data-placement="right-center"] .triangle {
[data-placement='right-center'] .triangle {
top: 50%;
margin-top: -0.9375em;
}
[data-placement="right-end"] .triangle {
[data-placement='right-end'] .triangle {
bottom: -0.25em;
}
[data-placement="bottom-center"] .triangle {
[data-placement='bottom-center'] .triangle {
left: 50%;
margin-left: -0.9375em;
}
[data-placement="bottom-end"] .triangle {
[data-placement='bottom-end'] .triangle {
right: 0;
}
@ -144,7 +142,7 @@ watchEffect(() => {
width: 100%;
height: 100%;
margin-top: 1.875em;
content: "";
content: '';
transform: rotate(45deg) skew(20deg, 20deg);
border-radius: 0.3125em;
background-color: var(--color-blue-scale-100);

View File

@ -1,19 +1,14 @@
<template>
<AppTooltip
v-for="tooltip in tooltips"
:key="tooltip.key"
:options="tooltip.options"
:target="tooltip.target"
/>
<AppTooltip v-for="tooltip in tooltips" :key="tooltip.key" :options="tooltip.options" :target="tooltip.target" />
</template>
<script lang="ts" setup>
import { storeToRefs } from "pinia";
import AppTooltip from "@/components/AppTooltip.vue";
import { useTooltipStore } from "@/stores/tooltip.store";
import { storeToRefs } from 'pinia'
import AppTooltip from '@/components/AppTooltip.vue'
import { useTooltipStore } from '@/stores/tooltip.store'
const tooltipStore = useTooltipStore();
const { tooltips } = storeToRefs(tooltipStore);
const tooltipStore = useTooltipStore()
const { tooltips } = storeToRefs(tooltipStore)
</script>
<style scoped></style>

View File

@ -1,33 +1,33 @@
<template>
<!-- eslint-disable vue/no-v-html -->
<pre class="code-highlight hljs"><code v-html="codeAsHtml"></code></pre>
<!-- eslint-enable vue/no-v-html -->
</template>
<script lang="ts" setup>
import { type AcceptedLanguage, highlight } from "@/libs/highlight";
import { computed } from "vue";
import { type AcceptedLanguage, highlight } from '@/libs/highlight'
import { computed } from 'vue'
const props = withDefaults(
defineProps<{
code?: any;
lang?: AcceptedLanguage;
code?: any
lang?: AcceptedLanguage
}>(),
{ lang: "typescript" }
);
{ lang: 'typescript' }
)
const codeAsText = computed(() => {
switch (typeof props.code) {
case "string":
return props.code;
case "function":
return String(props.code);
case 'string':
return props.code
case 'function':
return String(props.code)
default:
return JSON.stringify(props.code, undefined, 2);
return JSON.stringify(props.code, undefined, 2)
}
});
})
const codeAsHtml = computed(
() => highlight(codeAsText.value, { language: props.lang }).value
);
const codeAsHtml = computed(() => highlight(codeAsText.value, { language: props.lang }).value)
</script>
<style lang="postcss" scoped>

View File

@ -10,42 +10,39 @@
</UiFilter>
<UiActionButton :icon="faPlus" class="add-filter" @click="openModal()">
{{ $t("add-filter") }}
{{ $t('add-filter') }}
</UiActionButton>
</UiFilterGroup>
</template>
<script lang="ts" setup>
import UiActionButton from "@/components/ui/UiActionButton.vue";
import UiFilter from "@/components/ui/UiFilter.vue";
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
import { useModal } from "@/composables/modal.composable";
import type { Filters } from "@/types/filter";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import UiActionButton from '@/components/ui/UiActionButton.vue'
import UiFilter from '@/components/ui/UiFilter.vue'
import UiFilterGroup from '@/components/ui/UiFilterGroup.vue'
import { useModal } from '@/composables/modal.composable'
import type { Filters } from '@/types/filter'
import { faPlus } from '@fortawesome/free-solid-svg-icons'
const props = defineProps<{
activeFilters: string[];
availableFilters: Filters;
}>();
activeFilters: string[]
availableFilters: Filters
}>()
const emit = defineEmits<{
(event: "addFilter", filter: string): void;
(event: "removeFilter", filter: string): void;
}>();
(event: 'addFilter', filter: string): void
(event: 'removeFilter', filter: string): void
}>()
const openModal = (editedFilter?: string) => {
const { onApprove } = useModal<string>(
() => import("@/components/modals/CollectionFilterModal.vue"),
{
availableFilters: props.availableFilters,
editedFilter,
}
);
const { onApprove } = useModal<string>(() => import('@/components/modals/CollectionFilterModal.vue'), {
availableFilters: props.availableFilters,
editedFilter,
})
if (editedFilter !== undefined) {
onApprove(() => emit("removeFilter", editedFilter));
onApprove(() => emit('removeFilter', editedFilter))
}
onApprove((newFilter) => emit("addFilter", newFilter));
};
onApprove(newFilter => emit('addFilter', newFilter))
}
</script>

View File

@ -1,31 +1,21 @@
<template>
<div class="collection-filter-row">
<span class="or">{{ $t("or") }}</span>
<span class="or">{{ $t('or') }}</span>
<FormWidget v-if="newFilter.isAdvanced" class="form-widget-advanced">
<input v-model="newFilter.content" />
</FormWidget>
<template v-else>
<FormWidget :before="currentFilterIcon">
<select v-model="newFilter.builder.property">
<option v-if="!newFilter.builder.property" value="">
- {{ $t("property") }} -
</option>
<option
v-for="(filter, property) in availableFilters"
:key="property"
:value="property"
>
<option v-if="!newFilter.builder.property" value="">- {{ $t('property') }} -</option>
<option v-for="(filter, property) in availableFilters" :key="property" :value="property">
{{ filter.label ?? property }}
</option>
</select>
</FormWidget>
<FormWidget v-if="hasComparisonSelect">
<select v-model="newFilter.builder.comparison">
<option
v-for="(label, type) in comparisons"
:key="type"
:value="type"
>
<option v-for="(label, type) in comparisons" :key="type" :value="type">
{{ label }}
</option>
</select>
@ -38,112 +28,88 @@
</option>
</select>
</FormWidget>
<FormWidget
v-else-if="hasValueInput"
:after="valueInputAfter"
:before="valueInputBefore"
>
<FormWidget v-else-if="hasValueInput" :after="valueInputAfter" :before="valueInputBefore">
<input v-model="newFilter.builder.value" />
</FormWidget>
</template>
<UiActionButton
v-if="!newFilter.isAdvanced"
:icon="faPencil"
@click="enableAdvancedMode"
/>
<UiActionButton v-if="!newFilter.isAdvanced" :icon="faPencil" @click="enableAdvancedMode" />
<UiActionButton :icon="faRemove" @click="emit('remove', newFilter.id)" />
</div>
</template>
<script lang="ts" setup>
import FormWidget from "@/components/FormWidget.vue";
import UiActionButton from "@/components/ui/UiActionButton.vue";
import { buildComplexMatcherNode } from "@/libs/complex-matcher.utils";
import { getFilterIcon } from "@/libs/utils";
import type {
Filter,
FilterComparisons,
FilterComparisonType,
Filters,
FilterType,
NewFilter,
} from "@/types/filter";
import { faPencil, faRemove } from "@fortawesome/free-solid-svg-icons";
import { useVModel } from "@vueuse/core";
import { computed, type Ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import FormWidget from '@/components/FormWidget.vue'
import UiActionButton from '@/components/ui/UiActionButton.vue'
import { buildComplexMatcherNode } from '@/libs/complex-matcher.utils'
import { getFilterIcon } from '@/libs/utils'
import type { Filter, FilterComparisons, FilterComparisonType, Filters, FilterType, NewFilter } from '@/types/filter'
import { faPencil, faRemove } from '@fortawesome/free-solid-svg-icons'
import { useVModel } from '@vueuse/core'
import { computed, type Ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const props = defineProps<{
availableFilters: Filters;
modelValue: NewFilter;
}>();
availableFilters: Filters
modelValue: NewFilter
}>()
const emit = defineEmits<{
(event: "update:modelValue", value: NewFilter): void;
(event: "remove", filterId: number): void;
}>();
(event: 'update:modelValue', value: NewFilter): void
(event: 'remove', filterId: number): void
}>()
const { t } = useI18n();
const { t } = useI18n()
const newFilter: Ref<NewFilter> = useVModel(props, "modelValue", emit);
const newFilter: Ref<NewFilter> = useVModel(props, 'modelValue', emit)
const getDefaultComparisonType = () => {
const defaultTypes: { [key in FilterType]: FilterComparisonType } = {
string: "stringContains",
boolean: "booleanTrue",
number: "numberEquals",
enum: "enumIs",
};
string: 'stringContains',
boolean: 'booleanTrue',
number: 'numberEquals',
enum: 'enumIs',
}
return defaultTypes[
props.availableFilters[newFilter.value.builder.property].type
];
};
return defaultTypes[props.availableFilters[newFilter.value.builder.property].type]
}
watch(
() => newFilter.value.builder.property,
() => {
newFilter.value.builder.comparison = getDefaultComparisonType();
newFilter.value.builder.value = "";
newFilter.value.builder.comparison = getDefaultComparisonType()
newFilter.value.builder.value = ''
}
);
)
const currentFilter = computed<Filter>(
() => props.availableFilters[newFilter.value.builder.property]
);
const currentFilter = computed<Filter>(() => props.availableFilters[newFilter.value.builder.property])
const currentFilterIcon = computed(() => getFilterIcon(currentFilter.value));
const currentFilterIcon = computed(() => getFilterIcon(currentFilter.value))
const hasValueInput = computed(() =>
["string", "number"].includes(currentFilter.value?.type)
);
const hasValueInput = computed(() => ['string', 'number'].includes(currentFilter.value?.type))
const hasComparisonSelect = computed(
() => newFilter.value.builder.property !== ""
);
const hasComparisonSelect = computed(() => newFilter.value.builder.property !== '')
const enumChoices = computed(() => {
if (!newFilter.value.builder.property) {
return [];
return []
}
const availableFilter =
props.availableFilters[newFilter.value.builder.property];
const availableFilter = props.availableFilters[newFilter.value.builder.property]
if (availableFilter.type !== "enum") {
return [];
if (availableFilter.type !== 'enum') {
return []
}
return availableFilter.choices;
});
return availableFilter.choices
})
const generatedFilter = computed(() => {
if (newFilter.value.isAdvanced) {
return newFilter.value.content;
return newFilter.value.content
}
if (!newFilter.value.builder.comparison) {
return "";
return ''
}
try {
@ -151,68 +117,64 @@ const generatedFilter = computed(() => {
newFilter.value.builder.comparison,
newFilter.value.builder.property,
newFilter.value.builder.value
);
)
if (node) {
return node.toString();
return node.toString()
}
return "";
return ''
} catch (e) {
return "";
return ''
}
});
})
const enableAdvancedMode = () => {
newFilter.value.content = generatedFilter.value;
newFilter.value.isAdvanced = true;
};
newFilter.value.content = generatedFilter.value
newFilter.value.isAdvanced = true
}
watch(generatedFilter, (value) => {
newFilter.value.content = value;
});
watch(generatedFilter, value => {
newFilter.value.content = value
})
const comparisons = computed<FilterComparisons>(() => {
const comparisonsByType = {
string: {
stringContains: t("filter.comparison.contains"),
stringEquals: t("filter.comparison.equals"),
stringStartsWith: t("filter.comparison.starts-with"),
stringEndsWith: t("filter.comparison.ends-with"),
stringMatchesRegex: t("filter.comparison.matches-regex"),
stringDoesNotContain: t("filter.comparison.not-contain"),
stringDoesNotEqual: t("filter.comparison.not-equal"),
stringDoesNotStartWith: t("filter.comparison.not-start-with"),
stringDoesNotEndWith: t("filter.comparison.not-end-with"),
stringDoesNotMatchRegex: t("filter.comparison.not-match-regex"),
stringContains: t('filter.comparison.contains'),
stringEquals: t('filter.comparison.equals'),
stringStartsWith: t('filter.comparison.starts-with'),
stringEndsWith: t('filter.comparison.ends-with'),
stringMatchesRegex: t('filter.comparison.matches-regex'),
stringDoesNotContain: t('filter.comparison.not-contain'),
stringDoesNotEqual: t('filter.comparison.not-equal'),
stringDoesNotStartWith: t('filter.comparison.not-start-with'),
stringDoesNotEndWith: t('filter.comparison.not-end-with'),
stringDoesNotMatchRegex: t('filter.comparison.not-match-regex'),
},
boolean: {
booleanTrue: t("filter.comparison.is-true"),
booleanFalse: t("filter.comparison.is-false"),
booleanTrue: t('filter.comparison.is-true'),
booleanFalse: t('filter.comparison.is-false'),
},
number: {
numberLessThan: "<",
numberLessThanOrEquals: "<=",
numberEquals: "=",
numberGreaterThanOrEquals: ">=",
numberGreaterThan: ">",
numberLessThan: '<',
numberLessThanOrEquals: '<=',
numberEquals: '=',
numberGreaterThanOrEquals: '>=',
numberGreaterThan: '>',
},
enum: {
enumIs: t("filter.comparison.is"),
enumIsNot: t("filter.comparison.is-not"),
enumIs: t('filter.comparison.is'),
enumIsNot: t('filter.comparison.is-not'),
},
};
}
return comparisonsByType[currentFilter.value.type];
});
return comparisonsByType[currentFilter.value.type]
})
const valueInputBefore = computed(() =>
newFilter.value.builder.comparison === "stringMatchesRegex" ? "/" : undefined
);
const valueInputBefore = computed(() => (newFilter.value.builder.comparison === 'stringMatchesRegex' ? '/' : undefined))
const valueInputAfter = computed(() =>
newFilter.value.builder.comparison === "stringMatchesRegex" ? "/i" : undefined
);
const valueInputAfter = computed(() => (newFilter.value.builder.comparison === 'stringMatchesRegex' ? '/i' : undefined))
</script>
<style lang="postcss" scoped>

View File

@ -13,46 +13,39 @@
</UiFilter>
<UiActionButton :icon="faPlus" class="add-sort" @click="openModal()">
{{ $t("add-sort") }}
{{ $t('add-sort') }}
</UiActionButton>
</UiFilterGroup>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiActionButton from "@/components/ui/UiActionButton.vue";
import UiFilter from "@/components/ui/UiFilter.vue";
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
import { useModal } from "@/composables/modal.composable";
import type { ActiveSorts, NewSort, Sorts } from "@/types/sort";
import {
faCaretDown,
faCaretUp,
faPlus,
} from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import UiActionButton from '@/components/ui/UiActionButton.vue'
import UiFilter from '@/components/ui/UiFilter.vue'
import UiFilterGroup from '@/components/ui/UiFilterGroup.vue'
import { useModal } from '@/composables/modal.composable'
import type { ActiveSorts, NewSort, Sorts } from '@/types/sort'
import { faCaretDown, faCaretUp, faPlus } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue'
const props = defineProps<{
availableSorts: Sorts;
activeSorts: ActiveSorts<Record<string, any>>;
}>();
availableSorts: Sorts
activeSorts: ActiveSorts<Record<string, any>>
}>()
const emit = defineEmits<{
(event: "toggleSortDirection", property: string): void;
(event: "addSort", property: string, isAscending: boolean): void;
(event: "removeSort", property: string): void;
}>();
(event: 'toggleSortDirection', property: string): void
(event: 'addSort', property: string, isAscending: boolean): void
(event: 'removeSort', property: string): void
}>()
const openModal = () => {
const { onApprove } = useModal<NewSort>(
() => import("@/components/modals/CollectionSorterModal.vue"),
{ availableSorts: computed(() => props.availableSorts) }
);
const { onApprove } = useModal<NewSort>(() => import('@/components/modals/CollectionSorterModal.vue'), {
availableSorts: computed(() => props.availableSorts),
})
onApprove(({ property, isAscending }) =>
emit("addSort", property, isAscending)
);
};
onApprove(({ property, isAscending }) => emit('addSort', property, isAscending))
}
</script>
<style lang="postcss" scoped>

View File

@ -39,59 +39,52 @@
</template>
<script generic="T extends XenApiRecord<any>" lang="ts" setup>
import CollectionFilter from "@/components/CollectionFilter.vue";
import CollectionSorter from "@/components/CollectionSorter.vue";
import UiTable from "@/components/ui/UiTable.vue";
import useCollectionFilter from "@/composables/collection-filter.composable";
import useCollectionSorter from "@/composables/collection-sorter.composable";
import useFilteredCollection from "@/composables/filtered-collection.composable";
import useMultiSelect from "@/composables/multi-select.composable";
import useSortedCollection from "@/composables/sorted-collection.composable";
import type { XenApiRecord } from "@/libs/xen-api/xen-api.types";
import type { Filters } from "@/types/filter";
import type { Sorts } from "@/types/sort";
import { computed, toRef, watch } from "vue";
import CollectionFilter from '@/components/CollectionFilter.vue'
import CollectionSorter from '@/components/CollectionSorter.vue'
import UiTable from '@/components/ui/UiTable.vue'
import useCollectionFilter from '@/composables/collection-filter.composable'
import useCollectionSorter from '@/composables/collection-sorter.composable'
import useFilteredCollection from '@/composables/filtered-collection.composable'
import useMultiSelect from '@/composables/multi-select.composable'
import useSortedCollection from '@/composables/sorted-collection.composable'
import type { XenApiRecord } from '@/libs/xen-api/xen-api.types'
import type { Filters } from '@/types/filter'
import type { Sorts } from '@/types/sort'
import { computed, toRef, watch } from 'vue'
const props = defineProps<{
modelValue?: T["$ref"][];
availableFilters?: Filters;
availableSorts?: Sorts;
collection: T[];
}>();
modelValue?: T['$ref'][]
availableFilters?: Filters
availableSorts?: Sorts
collection: T[]
}>()
const emit = defineEmits<{
(event: "update:modelValue", selectedRefs: T["$ref"][]): void;
}>();
(event: 'update:modelValue', selectedRefs: T['$ref'][]): void
}>()
const isSelectable = computed(() => props.modelValue !== undefined);
const isSelectable = computed(() => props.modelValue !== undefined)
const { filters, addFilter, removeFilter, predicate } = useCollectionFilter({
queryStringParam: "filter",
});
const { sorts, addSort, removeSort, toggleSortDirection, compareFn } =
useCollectionSorter<Record<string, any>>({ queryStringParam: "sort" });
queryStringParam: 'filter',
})
const { sorts, addSort, removeSort, toggleSortDirection, compareFn } = useCollectionSorter<Record<string, any>>({
queryStringParam: 'sort',
})
const filteredCollection = useFilteredCollection(
toRef(props, "collection"),
predicate
);
const filteredCollection = useFilteredCollection(toRef(props, 'collection'), predicate)
const filteredAndSortedCollection = useSortedCollection(
filteredCollection,
compareFn
);
const filteredAndSortedCollection = useSortedCollection(filteredCollection, compareFn)
const usableRefs = computed(() => props.collection.map((item) => item["$ref"]));
const usableRefs = computed(() => props.collection.map(item => item.$ref))
const selectableRefs = computed(() =>
filteredAndSortedCollection.value.map((item) => item["$ref"])
);
const selectableRefs = computed(() => filteredAndSortedCollection.value.map(item => item.$ref))
const { selected, areAllSelected } = useMultiSelect(usableRefs, selectableRefs);
const { selected, areAllSelected } = useMultiSelect(usableRefs, selectableRefs)
watch(selected, (selected) => emit("update:modelValue", selected), {
watch(selected, selected => emit('update:modelValue', selected), {
immediate: true,
});
})
</script>
<style lang="postcss" scoped>

View File

@ -10,12 +10,12 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
defineProps<{
icon?: IconDefinition;
}>();
icon?: IconDefinition
}>()
</script>
<style lang="postcss" scoped>

View File

@ -26,18 +26,17 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
defineProps<{
before?: IconDefinition | string | object; // "object" added as workaround
after?: IconDefinition | string | object; // See https://github.com/vuejs/core/issues/4294
label?: string;
inline?: boolean;
}>();
before?: IconDefinition | string | object // "object" added as workaround
after?: IconDefinition | string | object // See https://github.com/vuejs/core/issues/4294
label?: string
inline?: boolean
}>()
const isIcon = (maybeIcon: any): maybeIcon is IconDefinition =>
typeof maybeIcon === "object";
const isIcon = (maybeIcon: any): maybeIcon is IconDefinition => typeof maybeIcon === 'object'
</script>
<style lang="postcss" scoped>
@ -103,7 +102,7 @@ const isIcon = (maybeIcon: any): maybeIcon is IconDefinition =>
}
}
:slotted(input[type="checkbox"]) {
:slotted(input[type='checkbox']) {
font: inherit;
display: grid;
flex: 1.5rem 0 0;
@ -121,7 +120,7 @@ const isIcon = (maybeIcon: any): maybeIcon is IconDefinition =>
&::before {
width: 0.65em;
height: 0.65em;
content: "";
content: '';
transition: 120ms transform ease-in-out;
transform: scale(0);
transform-origin: center;

View File

@ -27,35 +27,35 @@
</template>
<script lang="ts" setup>
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import UiCounter from "@/components/ui/UiCounter.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import UiTable from "@/components/ui/UiTable.vue";
import type { XenApiPatchWithHostRefs } from "@/composables/host-patches.composable";
import { vTooltip } from "@/directives/tooltip.directive";
import { useUiStore } from "@/stores/ui.store";
import { computed } from "vue";
import UiCardSpinner from '@/components/ui/UiCardSpinner.vue'
import UiCounter from '@/components/ui/UiCounter.vue'
import UiSpinner from '@/components/ui/UiSpinner.vue'
import UiTable from '@/components/ui/UiTable.vue'
import type { XenApiPatchWithHostRefs } from '@/composables/host-patches.composable'
import { vTooltip } from '@/directives/tooltip.directive'
import { useUiStore } from '@/stores/ui.store'
import { computed } from 'vue'
const props = defineProps<{
patches: XenApiPatchWithHostRefs[];
hasMultipleHosts: boolean;
areAllLoaded: boolean;
areSomeLoaded: boolean;
}>();
patches: XenApiPatchWithHostRefs[]
hasMultipleHosts: boolean
areAllLoaded: boolean
areSomeLoaded: boolean
}>()
const sortedPatches = computed(() =>
[...props.patches].sort((patch1, patch2) => {
if (patch1.changelog == null) {
return 1;
return 1
} else if (patch2.changelog == null) {
return -1;
return -1
}
return patch1.changelog.date - patch2.changelog.date;
return patch1.changelog.date - patch2.changelog.date
})
);
)
const { isDesktop } = useUiStore();
const { isDesktop } = useUiStore()
</script>
<style lang="postcss" scoped>

View File

@ -1,23 +1,23 @@
<template>
<div class="error" v-if="error !== undefined">
<div v-if="error !== undefined" class="error">
<UiIcon :icon="faExclamationCircle" />
<span v-if="error.message === 'SESSION_AUTHENTICATION_FAILED'">
{{ $t("password-invalid") }}
{{ $t('password-invalid') }}
</span>
<span v-else>
{{ $t("error-occurred") }}
{{ $t('error-occurred') }}
</span>
</div>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { XenApiError } from "@/libs/xen-api/xen-api.types";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import type { XenApiError } from '@/libs/xen-api/xen-api.types'
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'
defineProps<{
error: XenApiError | undefined;
}>();
error: XenApiError | undefined
}>()
</script>
<style lang="postcss" scoped>

View File

@ -1,7 +1,7 @@
<template>
<div class="no-data">
<img alt="No data" class="img" src="@/assets/undraw-bug-fixing.svg" />
<p class="text-error">{{ $t("error-no-data") }}</p>
<p class="text-error">{{ $t('error-no-data') }}</p>
</div>
</template>

View File

@ -1,7 +1,7 @@
<template>
<div class="no-result">
<img alt="" class="img" src="@/assets/no-result.svg" />
<p class="text-info">{{ $t("no-result") }}</p>
<p class="text-info">{{ $t('no-result') }}</p>
</div>
</template>

View File

@ -12,80 +12,75 @@
</template>
<script generic="T extends ObjectType" lang="ts" setup>
import UiSpinner from "@/components/ui/UiSpinner.vue";
import type {
ObjectType,
ObjectTypeToRecord,
} from "@/libs/xen-api/xen-api.types";
import { useHostStore } from "@/stores/xen-api/host.store";
import { usePoolStore } from "@/stores/xen-api/pool.store";
import { useSrStore } from "@/stores/xen-api/sr.store";
import { useVmStore } from "@/stores/xen-api/vm.store";
import type { StoreDefinition } from "pinia";
import { computed, onUnmounted, watch } from "vue";
import type { RouteRecordName } from "vue-router";
import UiSpinner from '@/components/ui/UiSpinner.vue'
import type { ObjectType, ObjectTypeToRecord } from '@/libs/xen-api/xen-api.types'
import { useHostStore } from '@/stores/xen-api/host.store'
import { usePoolStore } from '@/stores/xen-api/pool.store'
import { useSrStore } from '@/stores/xen-api/sr.store'
import { useVmStore } from '@/stores/xen-api/vm.store'
import type { StoreDefinition } from 'pinia'
import { computed, onUnmounted, watch } from 'vue'
import type { RouteRecordName } from 'vue-router'
type HandledTypes = "host" | "vm" | "sr" | "pool";
type XRecord = ObjectTypeToRecord<T>;
type HandledTypes = 'host' | 'vm' | 'sr' | 'pool'
type XRecord = ObjectTypeToRecord<T>
type Config = Partial<
Record<
ObjectType,
{
useStore: StoreDefinition<any, any, any, any>;
routeName: RouteRecordName | undefined;
useStore: StoreDefinition<any, any, any, any>
routeName: RouteRecordName | undefined
}
>
>;
>
const props = defineProps<{
type: T;
uuid: XRecord["uuid"];
}>();
type: T
uuid: XRecord['uuid']
}>()
const config: Config = {
host: { useStore: useHostStore, routeName: "host.dashboard" },
vm: { useStore: useVmStore, routeName: "vm.console" },
host: { useStore: useHostStore, routeName: 'host.dashboard' },
vm: { useStore: useVmStore, routeName: 'vm.console' },
sr: { useStore: useSrStore, routeName: undefined },
pool: { useStore: usePoolStore, routeName: "pool.dashboard" },
} satisfies Record<HandledTypes, any>;
pool: { useStore: usePoolStore, routeName: 'pool.dashboard' },
} satisfies Record<HandledTypes, any>
const store = computed(() => config[props.type]?.useStore());
const store = computed(() => config[props.type]?.useStore())
const subscriptionId = Symbol();
const subscriptionId = Symbol('OBJECT_LINK_SUBSCRIPTION_ID')
watch(
store,
(nextStore, previousStore) => {
previousStore?.unsubscribe(subscriptionId);
nextStore?.subscribe(subscriptionId);
previousStore?.unsubscribe(subscriptionId)
nextStore?.subscribe(subscriptionId)
},
{ immediate: true }
);
)
onUnmounted(() => {
store.value?.unsubscribe(subscriptionId);
});
store.value?.unsubscribe(subscriptionId)
})
const record = computed<ObjectTypeToRecord<HandledTypes> | undefined>(
() => store.value?.getByUuid(props.uuid as any)
);
const record = computed<ObjectTypeToRecord<HandledTypes> | undefined>(() => store.value?.getByUuid(props.uuid as any))
const isReady = computed(() => {
return store.value?.isReady ?? true;
});
return store.value?.isReady ?? true
})
const objectRoute = computed(() => {
const { routeName } = config[props.type] ?? {};
const { routeName } = config[props.type] ?? {}
if (routeName === undefined) {
return;
return
}
return {
name: routeName,
params: { uuid: props.uuid },
};
});
}
})
</script>
<style lang="postcss" scoped>

View File

@ -6,30 +6,24 @@
<slot v-else />
</template>
<script
generic="T extends XenApiRecord<ObjectType>, I extends T['uuid']"
lang="ts"
setup
>
import UiSpinner from "@/components/ui/UiSpinner.vue";
import type { ObjectType, XenApiRecord } from "@/libs/xen-api/xen-api.types";
import ObjectNotFoundView from "@/views/ObjectNotFoundView.vue";
import { computed } from "vue";
import { useRouter } from "vue-router";
<script generic="T extends XenApiRecord<ObjectType>, I extends T['uuid']" lang="ts" setup>
import UiSpinner from '@/components/ui/UiSpinner.vue'
import type { ObjectType, XenApiRecord } from '@/libs/xen-api/xen-api.types'
import ObjectNotFoundView from '@/views/ObjectNotFoundView.vue'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps<{
isReady: boolean;
uuidChecker: (uuid: I) => boolean;
id?: I;
}>();
isReady: boolean
uuidChecker: (uuid: I) => boolean
id?: I
}>()
const { currentRoute } = useRouter();
const { currentRoute } = useRouter()
const id = computed(() => props.id ?? (currentRoute.value.params.uuid as I));
const id = computed(() => props.id ?? (currentRoute.value.params.uuid as I))
const isRecordNotFound = computed(
() => props.isReady && !props.uuidChecker(id.value)
);
const isRecordNotFound = computed(() => props.isReady && !props.uuidChecker(id.value))
</script>
<style scoped>

View File

@ -5,21 +5,21 @@
:title="$t('xo-lite-under-construction')"
>
<p class="contact">
{{ $t("do-you-have-needs") }}
{{ $t('do-you-have-needs') }}
<a
href="https://xcp-ng.org/forum/topic/5018/xo-lite-building-an-embedded-ui-in-xcp-ng"
rel="noopener noreferrer"
target="_blank"
>
{{ $t("here") }}
{{ $t('here') }}
</a>
</p>
</UiStatusPanel>
</template>
<script lang="ts" setup>
import underConstruction from "@/assets/under-construction.svg";
import UiStatusPanel from "@/components/ui/UiStatusPanel.vue";
import underConstruction from '@/assets/under-construction.svg'
import UiStatusPanel from '@/components/ui/UiStatusPanel.vue'
</script>
<style lang="postcss" scoped>

View File

@ -1,8 +1,6 @@
<template>
<div
v-if="xenApi.isPoolOverridden"
class="warning-not-current-pool"
@click="xenApi.resetPoolMasterIp"
v-tooltip="
asTooltip && {
placement: 'right',
@ -12,6 +10,8 @@
`,
}
"
class="warning-not-current-pool"
@click="xenApi.resetPoolMasterIp"
>
<div class="wrapper">
<UiIcon :icon="faWarning" />
@ -20,26 +20,26 @@
<strong>{{ masterSessionStorage }}</strong>
</i18n-t>
<br />
{{ $t("click-to-return-default-pool") }}
{{ $t('click-to-return-default-pool') }}
</p>
</div>
</div>
</template>
<script lang="ts" setup>
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { useSessionStorage } from "@vueuse/core";
import { faWarning } from '@fortawesome/free-solid-svg-icons'
import { useSessionStorage } from '@vueuse/core'
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useXenApiStore } from "@/stores/xen-api.store";
import { vTooltip } from "@/directives/tooltip.directive";
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import { useXenApiStore } from '@/stores/xen-api.store'
import { vTooltip } from '@/directives/tooltip.directive'
defineProps<{
asTooltip?: boolean;
}>();
asTooltip?: boolean
}>()
const xenApi = useXenApiStore();
const masterSessionStorage = useSessionStorage("master", null);
const xenApi = useXenApiStore()
const masterSessionStorage = useSessionStorage('master', null)
</script>
<style lang="postcss" scoped>

View File

@ -3,31 +3,25 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import {
faMoon,
faPause,
faPlay,
faQuestion,
faStop,
} from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import { VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
import { faMoon, faPause, faPlay, faQuestion, faStop } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue'
const props = defineProps<{
state: VM_POWER_STATE;
}>();
state: VM_POWER_STATE
}>()
const icons = {
[VM_POWER_STATE.RUNNING]: faPlay,
[VM_POWER_STATE.PAUSED]: faPause,
[VM_POWER_STATE.SUSPENDED]: faMoon,
[VM_POWER_STATE.HALTED]: faStop,
};
}
const icon = computed(() => icons[props.state] ?? faQuestion);
const icon = computed(() => icons[props.state] ?? faQuestion)
const className = computed(() => `state-${props.state.toLocaleLowerCase()}`);
const className = computed(() => `state-${props.state.toLocaleLowerCase()}`)
</script>
<style lang="postcss" scoped>

View File

@ -1,9 +1,5 @@
<template>
<svg
class="progress-circle"
viewBox="0 0 36 36"
xmlns="http://www.w3.org/2000/svg"
>
<svg class="progress-circle" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
<path
class="progress-circle-background"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
@ -12,31 +8,29 @@
class="progress-circle-fill"
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
/>
<text class="progress-circle-text" text-anchor="middle" x="50%" y="50%">
{{ progress }}%
</text>
<text class="progress-circle-text" text-anchor="middle" x="50%" y="50%">{{ progress }}%</text>
</svg>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import { computed } from 'vue'
interface Props {
value: number;
maxValue?: number;
value: number
maxValue?: number
}
const props = withDefaults(defineProps<Props>(), {
maxValue: 100,
});
})
const progress = computed(() => {
if (props.maxValue === 0) {
return 0;
return 0
}
return Math.round((props.value / props.maxValue) * 100);
});
return Math.round((props.value / props.maxValue) * 100)
})
</script>
<style lang="postcss" scoped>

View File

@ -3,18 +3,18 @@
</template>
<script lang="ts" setup>
import useRelativeTime from "@/composables/relative-time.composable";
import { parseDateTime } from "@/libs/utils";
import { useNow } from "@vueuse/core";
import { computed } from "vue";
import useRelativeTime from '@/composables/relative-time.composable'
import { parseDateTime } from '@/libs/utils'
import { useNow } from '@vueuse/core'
import { computed } from 'vue'
const props = defineProps<{
date: Date | number | string;
}>();
date: Date | number | string
}>()
const date = computed(() => new Date(parseDateTime(props.date)));
const now = useNow({ interval: 1000 });
const relativeTime = useRelativeTime(date, now);
const date = computed(() => new Date(parseDateTime(props.date)))
const now = useNow({ interval: 1000 })
const relativeTime = useRelativeTime(date, now)
</script>
<style lang="postcss" scoped></style>

View File

@ -3,112 +3,103 @@
</template>
<script lang="ts" setup>
import { useXenApiStore } from "@/stores/xen-api.store";
import VncClient from "@novnc/novnc/core/rfb";
import { promiseTimeout } from "@vueuse/shared";
import { fibonacci } from "iterable-backoff";
import { computed, onBeforeUnmount, ref, watchEffect } from "vue";
import { useXenApiStore } from '@/stores/xen-api.store'
import VncClient from '@novnc/novnc/core/rfb'
import { promiseTimeout } from '@vueuse/shared'
import { fibonacci } from 'iterable-backoff'
import { computed, onBeforeUnmount, ref, watchEffect } from 'vue'
const N_TOTAL_TRIES = 8;
const FIBONACCI_MS_ARRAY: number[] = Array.from(
fibonacci().toMs().take(N_TOTAL_TRIES)
);
const N_TOTAL_TRIES = 8
const FIBONACCI_MS_ARRAY: number[] = Array.from(fibonacci().toMs().take(N_TOTAL_TRIES))
const props = defineProps<{
location: string;
isConsoleAvailable: boolean;
}>();
location: string
isConsoleAvailable: boolean
}>()
const vmConsoleContainer = ref<HTMLDivElement>();
const xenApiStore = useXenApiStore();
const vmConsoleContainer = ref<HTMLDivElement>()
const xenApiStore = useXenApiStore()
const url = computed(() => {
if (xenApiStore.currentSessionId == null) {
return;
return
}
const _url = new URL(props.location);
_url.protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
_url.searchParams.set("session_id", xenApiStore.currentSessionId);
return _url;
});
const _url = new URL(props.location)
_url.protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
_url.searchParams.set('session_id', xenApiStore.currentSessionId)
return _url
})
let vncClient: VncClient | undefined;
let nConnectionAttempts = 0;
let vncClient: VncClient | undefined
let nConnectionAttempts = 0
const handleDisconnectionEvent = () => {
clearVncClient();
clearVncClient()
if (props.isConsoleAvailable) {
nConnectionAttempts++;
nConnectionAttempts++
if (nConnectionAttempts > N_TOTAL_TRIES) {
console.error(
"The number of reconnection attempts has been exceeded for:",
props.location
);
return;
console.error('The number of reconnection attempts has been exceeded for:', props.location)
return
}
console.error(
`Connection lost for the remote console: ${
props.location
}. New attempt in ${FIBONACCI_MS_ARRAY[nConnectionAttempts - 1]}ms`
);
createVncConnection();
`Connection lost for the remote console: ${props.location}. New attempt in ${
FIBONACCI_MS_ARRAY[nConnectionAttempts - 1]
}ms`
)
createVncConnection()
}
};
const handleConnectionEvent = () => (nConnectionAttempts = 0);
}
const handleConnectionEvent = () => (nConnectionAttempts = 0)
const clearVncClient = () => {
if (vncClient === undefined) {
return;
return
}
vncClient.removeEventListener("disconnect", handleDisconnectionEvent);
vncClient.removeEventListener("connect", handleConnectionEvent);
vncClient.removeEventListener('disconnect', handleDisconnectionEvent)
vncClient.removeEventListener('connect', handleConnectionEvent)
if (vncClient._rfbConnectionState !== "disconnected") {
vncClient.disconnect();
if (vncClient._rfbConnectionState !== 'disconnected') {
vncClient.disconnect()
}
vncClient = undefined;
};
vncClient = undefined
}
const createVncConnection = async () => {
if (nConnectionAttempts !== 0) {
await promiseTimeout(FIBONACCI_MS_ARRAY[nConnectionAttempts - 1]);
await promiseTimeout(FIBONACCI_MS_ARRAY[nConnectionAttempts - 1])
}
vncClient = new VncClient(vmConsoleContainer.value!, url.value!.toString(), {
wsProtocols: ["binary"],
});
vncClient.scaleViewport = true;
wsProtocols: ['binary'],
})
vncClient.scaleViewport = true
vncClient.addEventListener("disconnect", handleDisconnectionEvent);
vncClient.addEventListener("connect", handleConnectionEvent);
};
vncClient.addEventListener('disconnect', handleDisconnectionEvent)
vncClient.addEventListener('connect', handleConnectionEvent)
}
watchEffect(() => {
if (
url.value === undefined ||
vmConsoleContainer.value === undefined ||
!props.isConsoleAvailable
) {
return;
if (url.value === undefined || vmConsoleContainer.value === undefined || !props.isConsoleAvailable) {
return
}
nConnectionAttempts = 0;
nConnectionAttempts = 0
clearVncClient();
createVncConnection();
});
clearVncClient()
createVncConnection()
})
onBeforeUnmount(() => {
clearVncClient();
});
clearVncClient()
})
defineExpose({
sendCtrlAltDel: () => vncClient?.sendCtrlAltDel(),
});
})
</script>
<style lang="postcss" scoped>

View File

@ -7,20 +7,20 @@
</template>
<script lang="ts" setup>
import { useContext } from "@/composables/context.composable";
import { DisabledContext } from "@/context";
import type { RouteLocationRaw } from "vue-router";
import UiTab from "@/components/ui/UiTab.vue";
import { useContext } from '@/composables/context.composable'
import { DisabledContext } from '@/context'
import type { RouteLocationRaw } from 'vue-router'
import UiTab from '@/components/ui/UiTab.vue'
const props = withDefaults(
defineProps<{
to: RouteLocationRaw;
disabled?: boolean;
to: RouteLocationRaw
disabled?: boolean
}>(),
{ disabled: undefined }
);
)
const isDisabled = useContext(DisabledContext, () => props.disabled);
const isDisabled = useContext(DisabledContext, () => props.disabled)
</script>
<style lang="postcss" scoped></style>

View File

@ -19,12 +19,8 @@
<g>
<path d="M476.32,675.94h20.81v10.04h-33.47v-63.14h12.66v53.1Z" />
<path d="M517.84,622.84v63.14h-12.66v-63.14h12.66Z" />
<path
d="M573.29,622.84v10.22h-16.82v52.92h-12.66v-52.92h-16.83v-10.22h46.31Z"
/>
<path
d="M595.18,633.06v15.83h21.26v10.04h-21.26v16.73h23.97v10.31h-36.64v-63.23h36.64v10.31h-23.97Z"
/>
<path d="M573.29,622.84v10.22h-16.82v52.92h-12.66v-52.92h-16.83v-10.22h46.31Z" />
<path d="M595.18,633.06v15.83h21.26v10.04h-21.26v16.73h23.97v10.31h-36.64v-63.23h36.64v10.31h-23.97Z" />
</g>
</g>
</svg>

View File

@ -11,12 +11,12 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
defineProps<{
icon: IconDefinition;
}>();
icon: IconDefinition
}>()
</script>
<style lang="postcss" scoped>

View File

@ -10,49 +10,46 @@
class="progress-item"
>
<UiProgressBar :value="item.value" color="custom" />
<UiProgressLegend
:label="item.label"
:value="item.badgeLabel ?? `${item.value}%`"
/>
<UiProgressLegend :label="item.label" :value="item.badgeLabel ?? `${item.value}%`" />
</div>
<slot :total-percent="computedData.totalPercentUsage" name="footer" />
</div>
</template>
<script lang="ts" setup>
import UiProgressBar from "@/components/ui/progress/UiProgressBar.vue";
import UiProgressLegend from "@/components/ui/progress/UiProgressLegend.vue";
import type { StatData } from "@/types/stat";
import { computed } from "vue";
import UiProgressBar from '@/components/ui/progress/UiProgressBar.vue'
import UiProgressLegend from '@/components/ui/progress/UiProgressLegend.vue'
import type { StatData } from '@/types/stat'
import { computed } from 'vue'
interface Props {
data: StatData[];
nItems?: number;
data: StatData[]
nItems?: number
}
const MIN_WARNING_VALUE = 80;
const MIN_DANGEROUS_VALUE = 90;
const MIN_WARNING_VALUE = 80
const MIN_DANGEROUS_VALUE = 90
const props = defineProps<Props>();
const props = defineProps<Props>()
const computedData = computed(() => {
const _data = props.data;
let totalPercentUsage = 0;
const _data = props.data
let totalPercentUsage = 0
return {
sortedArray: _data
?.map((item) => {
const value = Math.round((item.value / (item.maxValue ?? 100)) * 100);
totalPercentUsage += value;
?.map(item => {
const value = Math.round((item.value / (item.maxValue ?? 100)) * 100)
totalPercentUsage += value
return {
...item,
value,
};
}
})
.sort((item, nextItem) => nextItem.value - item.value)
.slice(0, props.nItems ?? _data.length),
totalPercentUsage,
};
});
}
})
</script>
<style lang="postcss" scoped>

View File

@ -6,28 +6,28 @@
</template>
<script lang="ts" setup>
import type { LinearChartData } from "@/types/chart";
import LinearChart from "@/components/charts/LinearChart.vue";
import type { LinearChartData } from '@/types/chart'
import LinearChart from '@/components/charts/LinearChart.vue'
const data: LinearChartData = [
{
label: "First series",
label: 'First series',
data: [
{ timestamp: 1670478371123, value: 1234 },
{ timestamp: 1670478519751, value: 1234 },
],
},
{
label: "Second series",
label: 'Second series',
data: [
{ timestamp: 1670478519751, value: 1234 },
{ timestamp: 167047555000, value: 1234 },
],
},
];
]
const customValueFormatter = (value: number) => {
return `${value} (Doubled: ${value * 2})`;
};
return `${value} (Doubled: ${value * 2})`
}
</script>
```

View File

@ -3,81 +3,70 @@
</template>
<script lang="ts" setup>
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_CHART_VALUE_FORMATTER } from "@/types/injection-keys";
import { utcFormat } from "d3-time-format";
import type { EChartsOption } from "echarts";
import { LineChart } from "echarts/charts";
import {
GridComponent,
LegendComponent,
TooltipComponent,
} from "echarts/components";
import { use } from "echarts/core";
import { CanvasRenderer } from "echarts/renderers";
import { computed, provide } from "vue";
import VueCharts from "vue-echarts";
import type { LinearChartData, ValueFormatter } from '@/types/chart'
import { IK_CHART_VALUE_FORMATTER } from '@/types/injection-keys'
import { utcFormat } from 'd3-time-format'
import type { EChartsOption } from 'echarts'
import { LineChart } from 'echarts/charts'
import { GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { computed, provide } from 'vue'
import VueCharts from 'vue-echarts'
const Y_AXIS_MAX_VALUE = 200;
const Y_AXIS_MAX_VALUE = 200
const props = defineProps<{
data: LinearChartData;
valueFormatter?: ValueFormatter;
maxValue?: number;
}>();
data: LinearChartData
valueFormatter?: ValueFormatter
maxValue?: number
}>()
const valueFormatter = computed<ValueFormatter>(() => {
const formatter = props.valueFormatter;
const formatter = props.valueFormatter
return (value) => {
return value => {
if (formatter === undefined) {
return value.toString();
return value.toString()
}
return formatter(value);
};
});
return formatter(value)
}
})
provide(IK_CHART_VALUE_FORMATTER, valueFormatter);
provide(IK_CHART_VALUE_FORMATTER, valueFormatter)
use([
CanvasRenderer,
LineChart,
GridComponent,
TooltipComponent,
LegendComponent,
]);
use([CanvasRenderer, LineChart, GridComponent, TooltipComponent, LegendComponent])
const option = computed<EChartsOption>(() => ({
legend: {
data: props.data.map((series) => series.label),
data: props.data.map(series => series.label),
},
tooltip: {
valueFormatter: (v) => valueFormatter.value(v as number),
valueFormatter: v => valueFormatter.value(v as number),
},
xAxis: {
type: "time",
type: 'time',
axisLabel: {
formatter: (timestamp: number) =>
utcFormat("%a\n%I:%M\n%p")(new Date(timestamp)),
formatter: (timestamp: number) => utcFormat('%a\n%I:%M\n%p')(new Date(timestamp)),
showMaxLabel: false,
showMinLabel: false,
},
},
yAxis: {
type: "value",
type: 'value',
axisLabel: {
formatter: valueFormatter.value,
},
max: props.maxValue ?? Y_AXIS_MAX_VALUE,
},
series: props.data.map((series, index) => ({
type: "line",
type: 'line',
name: series.label,
zlevel: index + 1,
data: series.data.map((item) => [item.timestamp, item.value]),
data: series.data.map(item => [item.timestamp, item.value]),
})),
}));
}))
</script>
<style lang="postcss" scoped>

View File

@ -3,31 +3,18 @@
<UiTab v-bind="tab(TAB.PROPS, propParams)">Props</UiTab>
<UiTab class="event-tab" v-bind="tab(TAB.EVENTS, eventParams)">
Events
<UiCounter
v-if="unreadEventsCount > 0"
:value="unreadEventsCount"
color="success"
/>
<UiCounter v-if="unreadEventsCount > 0" :value="unreadEventsCount" color="success" />
</UiTab>
<UiTab v-bind="tab(TAB.SLOTS, slotParams)">Slots</UiTab>
<UiTab v-bind="tab(TAB.SETTINGS, settingParams)">Settings</UiTab>
<AppMenu placement="bottom" shadow>
<template #trigger="{ open, isOpen }">
<UiTab
:active="isOpen"
:disabled="presets === undefined"
class="preset-tab"
@click="open"
>
<UiTab :active="isOpen" :disabled="presets === undefined" class="preset-tab" @click="open">
<UiIcon :icon="faSliders" />
Presets
</UiTab>
</template>
<MenuItem
v-for="(preset, label) in presets"
:key="label"
@click="applyPreset(preset)"
>
<MenuItem v-for="(preset, label) in presets" :key="label" @click="applyPreset(preset)">
{{ label }}
</MenuItem>
</AppMenu>
@ -38,16 +25,9 @@
<i>No configuration defined</i>
</UiCard>
<UiCard v-else-if="selectedTab === TAB.PROPS" class="tab-content">
<StoryPropParams
v-model="propValues"
:params="propParams"
@reset="resetProps"
/>
<StoryPropParams v-model="propValues" :params="propParams" @reset="resetProps" />
</UiCard>
<div
v-else-if="selectedTab === TAB.EVENTS"
class="tab-content event-tab-content"
>
<div v-else-if="selectedTab === TAB.EVENTS" class="tab-content event-tab-content">
<UiCard>
<StoryEventParams :params="eventParams" />
</UiCard>
@ -55,22 +35,11 @@
<UiCardTitle>
Logs
<template #right>
<UiButton
v-if="eventsLog.length > 0"
transparent
@click="eventsLog = []"
>
Clear
</UiButton>
<UiButton v-if="eventsLog.length > 0" transparent @click="eventsLog = []"> Clear </UiButton>
</template>
</UiCardTitle>
<div class="events-log">
<CodeHighlight
v-for="event in eventLogRows"
:key="event.id"
:code="event.args"
class="event-log"
/>
<CodeHighlight v-for="event in eventLogRows" :key="event.id" :code="event.args" class="event-log" />
</div>
</UiCard>
</div>
@ -78,11 +47,7 @@
<StorySlotParams :params="slotParams" />
</UiCard>
<UiCard v-else-if="selectedTab === TAB.SETTINGS" class="tab-content">
<StorySettingParams
v-model="settingValues"
:params="settingParams"
@reset="resetSettings"
/>
<StorySettingParams v-model="settingValues" :params="settingParams" @reset="resetSettings" />
</UiCard>
<UiCard class="tab-content story-result">
<slot :properties="slotProperties" :settings="slotSettings" />
@ -94,21 +59,21 @@
</template>
<script lang="ts" setup>
import AppMarkdown from "@/components/AppMarkdown.vue";
import CodeHighlight from "@/components/CodeHighlight.vue";
import StoryEventParams from "@/components/component-story/StoryEventParams.vue";
import StoryPropParams from "@/components/component-story/StoryPropParams.vue";
import StorySettingParams from "@/components/component-story/StorySettingParams.vue";
import StorySlotParams from "@/components/component-story/StorySlotParams.vue";
import AppMenu from "@/components/menu/AppMenu.vue";
import MenuItem from "@/components/menu/MenuItem.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiCounter from "@/components/ui/UiCounter.vue";
import UiTab from "@/components/ui/UiTab.vue";
import UiTabBar from "@/components/ui/UiTabBar.vue";
import AppMarkdown from '@/components/AppMarkdown.vue'
import CodeHighlight from '@/components/CodeHighlight.vue'
import StoryEventParams from '@/components/component-story/StoryEventParams.vue'
import StoryPropParams from '@/components/component-story/StoryPropParams.vue'
import StorySettingParams from '@/components/component-story/StorySettingParams.vue'
import StorySlotParams from '@/components/component-story/StorySlotParams.vue'
import AppMenu from '@/components/menu/AppMenu.vue'
import MenuItem from '@/components/menu/MenuItem.vue'
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import UiButton from '@/components/ui/UiButton.vue'
import UiCard from '@/components/ui/UiCard.vue'
import UiCardTitle from '@/components/ui/UiCardTitle.vue'
import UiCounter from '@/components/ui/UiCounter.vue'
import UiTab from '@/components/ui/UiTab.vue'
import UiTabBar from '@/components/ui/UiTabBar.vue'
import {
isEventParam,
isModelParam,
@ -117,31 +82,12 @@ import {
isSlotParam,
ModelParam,
type Param,
} from "@/libs/story/story-param";
import { faSliders } from "@fortawesome/free-solid-svg-icons";
import "highlight.js/styles/github-dark.css";
import { uniqueId, upperFirst } from "lodash-es";
import { computed, reactive, ref, watch, watchEffect } from "vue";
import { useRoute } from "vue-router";
const tab = (tab: TAB, params: Param[]) =>
reactive({
onClick: () => (selectedTab.value = tab),
active: computed(() => selectedTab.value === tab),
disabled: computed(() => params.length === 0),
});
const props = defineProps<{
params: (Param | ModelParam)[];
presets?: Record<
string,
{
props?: Record<string, any>;
settings?: Record<string, any>;
}
>;
fullWidthComponent?: boolean;
}>();
} from '@/libs/story/story-param'
import { faSliders } from '@fortawesome/free-solid-svg-icons'
import 'highlight.js/styles/github-dark.css'
import { uniqueId, upperFirst } from 'lodash-es'
import { computed, reactive, ref, watch, watchEffect } from 'vue'
import { useRoute } from 'vue-router'
enum TAB {
NONE,
@ -151,149 +97,159 @@ enum TAB {
SETTINGS,
}
const modelParams = computed(() => props.params.filter(isModelParam));
const tab = (tab: TAB, params: Param[]) =>
reactive({
onClick: () => (selectedTab.value = tab),
active: computed(() => selectedTab.value === tab),
disabled: computed(() => params.length === 0),
})
const props = defineProps<{
params: (Param | ModelParam)[]
presets?: Record<
string,
{
props?: Record<string, any>
settings?: Record<string, any>
}
>
fullWidthComponent?: boolean
}>()
const modelParams = computed(() => props.params.filter(isModelParam))
const propParams = computed(() => [
...props.params.filter(isPropParam),
...modelParams.value.map((modelParam) => modelParam.getPropParam()),
]);
...modelParams.value.map(modelParam => modelParam.getPropParam()),
])
const eventParams = computed(() => [
...props.params.filter(isEventParam),
...modelParams.value.map((modelParam) => modelParam.getEventParam()),
]);
...modelParams.value.map(modelParam => modelParam.getEventParam()),
])
const settingParams = computed(() => props.params.filter(isSettingParam));
const slotParams = computed(() => props.params.filter(isSlotParam));
const settingParams = computed(() => props.params.filter(isSettingParam))
const slotParams = computed(() => props.params.filter(isSlotParam))
const selectedTab = ref<TAB>(TAB.NONE);
const selectedTab = ref<TAB>(TAB.NONE)
if (propParams.value.length !== 0) {
selectedTab.value = TAB.PROPS;
selectedTab.value = TAB.PROPS
} else if (eventParams.value.length !== 0) {
selectedTab.value = TAB.EVENTS;
selectedTab.value = TAB.EVENTS
} else if (slotParams.value.length !== 0) {
selectedTab.value = TAB.SLOTS;
selectedTab.value = TAB.SLOTS
} else if (settingParams.value.length !== 0) {
selectedTab.value = TAB.SETTINGS;
selectedTab.value = TAB.SETTINGS
}
const propValues = ref<Record<string, any>>({});
const settingValues = ref<Record<string, any>>({});
const eventsLog = ref<
{ id: string; name: string; args: { name: string; value: any }[] }[]
>([]);
const unreadEventsCount = ref(0);
const propValues = ref<Record<string, any>>({})
const settingValues = ref<Record<string, any>>({})
const eventsLog = ref<{ id: string; name: string; args: { name: string; value: any }[] }[]>([])
const unreadEventsCount = ref(0)
const resetProps = () => {
propParams.value.forEach((param) => {
propValues.value[param.name] = param.getPresetValue();
});
};
propParams.value.forEach(param => {
propValues.value[param.name] = param.getPresetValue()
})
}
const resetSettings = () => {
settingParams.value.forEach((param) => {
settingValues.value[param.name] = param.getPresetValue();
});
};
settingParams.value.forEach(param => {
settingValues.value[param.name] = param.getPresetValue()
})
}
watchEffect(() => {
resetProps();
resetSettings();
});
resetProps()
resetSettings()
})
watch(selectedTab, (tab) => {
watch(selectedTab, tab => {
if (tab === TAB.EVENTS) {
unreadEventsCount.value = 0;
unreadEventsCount.value = 0
}
});
})
const logEvent = (name: string, args: { name: string; value: any }[]) => {
if (selectedTab.value !== TAB.EVENTS) {
unreadEventsCount.value += 1;
unreadEventsCount.value += 1
}
eventsLog.value.unshift({
id: uniqueId("event-log"),
id: uniqueId('event-log'),
name,
args,
});
};
})
}
const eventLogRows = computed(() => {
return eventsLog.value.map((eventLog) => {
const args = eventLog.args
.map((arg) => `${arg.name}: ${JSON.stringify(arg.value)}`)
.join(", ");
return eventsLog.value.map(eventLog => {
const args = eventLog.args.map(arg => `${arg.name}: ${JSON.stringify(arg.value)}`).join(', ')
return {
id: eventLog.id,
name: eventLog.name,
args: `${eventLog.name}(${args})`,
};
});
});
}
})
})
const slotProperties = computed(() => {
const properties: Record<string, any> = {};
const properties: Record<string, any> = {}
propParams.value.forEach(({ name }) => {
properties[name] = propValues.value[name];
});
properties[name] = propValues.value[name]
})
eventParams.value.forEach((eventParam) => {
eventParams.value.forEach(eventParam => {
properties[`on${upperFirst(eventParam.name)}`] = (...args: any[]) => {
if (eventParam.isVModel()) {
propValues.value[eventParam.rawName] = args[0];
propValues.value[eventParam.rawName] = args[0]
}
const logArgs = Object.keys(eventParam.getArguments()).map(
(argName, index) => ({
name: argName,
value: args[index],
})
);
eventParam.getPresetValue()?.(...args);
logEvent(eventParam.name, logArgs);
};
});
const logArgs = Object.keys(eventParam.getArguments()).map((argName, index) => ({
name: argName,
value: args[index],
}))
eventParam.getPresetValue()?.(...args)
logEvent(eventParam.name, logArgs)
}
})
return properties;
});
return properties
})
const slotSettings = computed(() => {
const result: Record<string, any> = {};
const result: Record<string, any> = {}
settingParams.value.forEach(({ name }) => {
result[name] = settingValues.value[name];
});
result[name] = settingValues.value[name]
})
return result;
});
return result
})
const documentation = ref();
const documentation = ref()
const route = useRoute();
const route = useRoute()
route.meta.storyMdLoader?.().then((md) => {
documentation.value = md;
});
route.meta.storyMdLoader?.().then(md => {
documentation.value = md
})
const applyPreset = (preset: {
props?: Record<string, any>;
settings?: Record<string, any>;
}) => {
const applyPreset = (preset: { props?: Record<string, any>; settings?: Record<string, any> }) => {
if (preset.props !== undefined) {
Object.entries(preset.props).forEach(([name, value]) => {
propValues.value[name] = value;
});
propValues.value[name] = value
})
}
if (preset.settings !== undefined) {
Object.entries(preset.settings).forEach(([name, value]) => {
settingValues.value[name] = value;
});
settingValues.value[name] = value
})
}
};
}
</script>
<style lang="postcss" scoped>

View File

@ -24,11 +24,11 @@
</template>
<script lang="ts" setup>
import CodeHighlight from "@/components/CodeHighlight.vue";
import StoryParamsTable from "@/components/component-story/StoryParamsTable.vue";
import type { EventParam } from "@/libs/story/story-param";
import CodeHighlight from '@/components/CodeHighlight.vue'
import StoryParamsTable from '@/components/component-story/StoryParamsTable.vue'
import type { EventParam } from '@/libs/story/story-param'
defineProps<{
params: EventParam[];
}>();
params: EventParam[]
}>()
</script>

View File

@ -18,51 +18,39 @@
<div>Optional string prop: {{ imOptional }}</div>
<div>Optional string prop with default: {{ imOptionalWithDefault }}</div>
Input for default v-model:
<input
:value="modelValue"
@input="
emit('update:modelValue', ($event.target as HTMLInputElement)?.value)
"
/>
<input :value="modelValue" @input="emit('update:modelValue', ($event.target as HTMLInputElement)?.value)" />
Input for v-model:customModel:
<input
:value="customModel"
@input="
emit('update:customModel', ($event.target as HTMLInputElement)?.value)
"
/>
<input :value="customModel" @input="emit('update:customModel', ($event.target as HTMLInputElement)?.value)" />
Event with no arguments:
<button type="button" @click="emit('click')">Click me</button>
Event with argument:
<button type="button" @click="emit('clickWithArg', 'my-id')">
Click me
</button>
<button type="button" @click="emit('clickWithArg', 'my-id')">Click me</button>
</div>
</template>
<script lang="ts" setup>
const moonDistance = 384400;
const moonDistance = 384400
withDefaults(
defineProps<{
imString: string;
imNumber: number;
imOptional?: string;
imOptionalWithDefault?: string;
modelValue?: string;
customModel?: string;
imString: string
imNumber: number
imOptional?: string
imOptionalWithDefault?: string
modelValue?: string
customModel?: string
}>(),
{
imOptionalWithDefault: "My default value",
imOptionalWithDefault: 'My default value',
}
);
)
const emit = defineEmits<{
(event: "update:modelValue", value: string): void;
(event: "update:customModel", value: string): void;
(event: "click"): void;
(event: "clickWithArg", id: string): void;
}>();
(event: 'update:modelValue', value: string): void
(event: 'update:customModel', value: string): void
(event: 'click'): void
(event: 'clickWithArg', id: string): void
}>()
</script>
<style lang="postcss" scoped>

View File

@ -2,80 +2,73 @@
<RouterLink :to="{ name: 'story' }">
<UiTitle type="h4">Stories</UiTitle>
</RouterLink>
<StoryMenuTree
:tree="tree"
@toggle-directory="toggleDirectory"
:opened-directories="openedDirectories"
/>
<StoryMenuTree :tree="tree" :opened-directories="openedDirectories" @toggle-directory="toggleDirectory" />
</template>
<script lang="ts" setup>
import StoryMenuTree from "@/components/component-story/StoryMenuTree.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import { type RouteRecordNormalized, useRoute, useRouter } from "vue-router";
import { ref } from "vue";
import StoryMenuTree from '@/components/component-story/StoryMenuTree.vue'
import UiTitle from '@/components/ui/UiTitle.vue'
import { type RouteRecordNormalized, useRoute, useRouter } from 'vue-router'
import { ref } from 'vue'
const { getRoutes } = useRouter();
const { getRoutes } = useRouter()
const routes = getRoutes().filter((route) => route.meta.isStory);
const routes = getRoutes().filter(route => route.meta.isStory)
export type StoryTree = Map<
string,
{ path: string; directory: string; children: StoryTree }
>;
export type StoryTree = Map<string, { path: string; directory: string; children: StoryTree }>
function createTree(routes: RouteRecordNormalized[]) {
const tree: StoryTree = new Map();
const tree: StoryTree = new Map()
for (const route of routes) {
const parts = route.path.slice(7).split("/");
let currentNode = tree;
let currentPath = "";
const parts = route.path.slice(7).split('/')
let currentNode = tree
let currentPath = ''
for (const part of parts) {
currentPath = currentPath ? `${currentPath}/${part}` : part;
currentPath = currentPath ? `${currentPath}/${part}` : part
if (!currentNode.has(part)) {
currentNode.set(part, {
children: new Map(),
path: route.path,
directory: currentPath,
});
})
}
currentNode = currentNode.get(part)!.children;
currentNode = currentNode.get(part)!.children
}
}
return tree;
return tree
}
const tree = createTree(routes);
const tree = createTree(routes)
const currentRoute = useRoute();
const currentRoute = useRoute()
const getDefaultOpenedDirectories = (): Set<string> => {
if (!currentRoute.meta.isStory) {
return new Set<string>();
return new Set<string>()
}
const openedDirectories = new Set<string>();
const parts = currentRoute.path.split("/").slice(2);
let currentPath = "";
const openedDirectories = new Set<string>()
const parts = currentRoute.path.split('/').slice(2)
let currentPath = ''
for (const part of parts) {
currentPath = currentPath ? `${currentPath}/${part}` : part;
openedDirectories.add(currentPath);
currentPath = currentPath ? `${currentPath}/${part}` : part
openedDirectories.add(currentPath)
}
return openedDirectories;
};
return openedDirectories
}
const openedDirectories = ref(getDefaultOpenedDirectories());
const openedDirectories = ref(getDefaultOpenedDirectories())
const toggleDirectory = (directory: string) => {
if (openedDirectories.value.has(directory)) {
openedDirectories.value.delete(directory);
openedDirectories.value.delete(directory)
} else {
openedDirectories.value.add(directory);
openedDirectories.value.add(directory)
}
};
}
</script>

View File

@ -1,14 +1,8 @@
<template>
<ul class="story-menu-tree">
<li v-for="[key, node] in tree" :key="key">
<span
v-if="node.children.size > 0"
class="directory"
@click="emit('toggle-directory', node.directory)"
>
<UiIcon
:icon="isOpen(node.directory) ? faFolderOpen : faFolderClosed"
/>
<span v-if="node.children.size > 0" class="directory" @click="emit('toggle-directory', node.directory)">
<UiIcon :icon="isOpen(node.directory) ? faFolderOpen : faFolderClosed" />
{{ formatName(key) }}
</span>
<RouterLink v-else :to="node.path" class="link">
@ -19,37 +13,33 @@
<StoryMenuTree
v-if="isOpen(node.directory)"
:tree="node.children"
@toggle-directory="emit('toggle-directory', $event)"
:opened-directories="openedDirectories"
@toggle-directory="emit('toggle-directory', $event)"
/>
</li>
</ul>
</template>
<script lang="ts" setup>
import type { StoryTree } from "@/components/component-story/StoryMenu.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import {
faFile,
faFolderClosed,
faFolderOpen,
} from "@fortawesome/free-regular-svg-icons";
import type { StoryTree } from '@/components/component-story/StoryMenu.vue'
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import { faFile, faFolderClosed, faFolderOpen } from '@fortawesome/free-regular-svg-icons'
const props = defineProps<{
tree: StoryTree;
openedDirectories: Set<string>;
}>();
tree: StoryTree
openedDirectories: Set<string>
}>()
const emit = defineEmits<{
(event: "toggle-directory", directory: string): void;
}>();
(event: 'toggle-directory', directory: string): void
}>()
const isOpen = (directory: string) => props.openedDirectories.has(directory);
const isOpen = (directory: string) => props.openedDirectories.has(directory)
const formatName = (name: string) => {
const parts = name.split("-");
return parts.map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
};
const parts = name.split('-')
return parts.map(part => part[0].toUpperCase() + part.slice(1)).join(' ')
}
</script>
<style lang="postcss" scoped>

View File

@ -18,12 +18,7 @@
</tr>
</tfoot>
<tbody>
<tr
v-for="param in params"
:key="param.name"
:class="{ required: param.isRequired() }"
class="row"
>
<tr v-for="param in params" :key="param.name" :class="{ required: param.isRequired() }" class="row">
<th class="name">
{{ param.getFullName() }}
<sup
@ -48,11 +43,7 @@
</td>
<td class="reset-param">
<UiIcon
v-if="
param.hasWidget() &&
!param.isRequired() &&
model[param.name] !== undefined
"
v-if="param.hasWidget() && !param.isRequired() && model[param.name] !== undefined"
:icon="faClose"
class="reset-icon"
@click="model[param.name] = undefined"
@ -74,12 +65,8 @@
</span>
</td>
<td>
<code
v-if="!param.isRequired()"
:class="{ active: model[param.name] === undefined }"
class="default-value"
>
{{ JSON.stringify(param.getDefaultValue()) ?? "undefined" }}
<code v-if="!param.isRequired()" :class="{ active: model[param.name] === undefined }" class="default-value">
{{ JSON.stringify(param.getDefaultValue()) ?? 'undefined' }}
</code>
<span v-else>-</span>
</td>
@ -92,42 +79,42 @@
</template>
<script lang="ts" setup>
import CodeHighlight from "@/components/CodeHighlight.vue";
import StoryParamsTable from "@/components/component-story/StoryParamsTable.vue";
import StoryWidget from "@/components/component-story/StoryWidget.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useModal } from "@/composables/modal.composable";
import useSortedCollection from "@/composables/sorted-collection.composable";
import { vTooltip } from "@/directives/tooltip.directive";
import type { PropParam } from "@/libs/story/story-param";
import { faClose, faRepeat } from "@fortawesome/free-solid-svg-icons";
import { useVModel } from "@vueuse/core";
import { toRef } from "vue";
import CodeHighlight from '@/components/CodeHighlight.vue'
import StoryParamsTable from '@/components/component-story/StoryParamsTable.vue'
import StoryWidget from '@/components/component-story/StoryWidget.vue'
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import { useModal } from '@/composables/modal.composable'
import useSortedCollection from '@/composables/sorted-collection.composable'
import { vTooltip } from '@/directives/tooltip.directive'
import type { PropParam } from '@/libs/story/story-param'
import { faClose, faRepeat } from '@fortawesome/free-solid-svg-icons'
import { useVModel } from '@vueuse/core'
import { toRef } from 'vue'
const props = defineProps<{
params: PropParam[];
modelValue: Record<string, any>;
}>();
params: PropParam[]
modelValue: Record<string, any>
}>()
const params = useSortedCollection(toRef(props, "params"), (p1, p2) => {
const params = useSortedCollection(toRef(props, 'params'), (p1, p2) => {
if (p1.isRequired() === p2.isRequired()) {
return 0;
return 0
}
return p1.isRequired() ? -1 : 1;
});
return p1.isRequired() ? -1 : 1
})
const emit = defineEmits<{
(event: "reset"): void;
(event: "update:modelValue", value: any): void;
}>();
(event: 'reset'): void
(event: 'update:modelValue', value: any): void
}>()
const model = useVModel(props, "modelValue", emit);
const model = useVModel(props, 'modelValue', emit)
const openRawValueModal = (code: string) =>
useModal(() => import("@/components/modals/CodeHighlightModal.vue"), {
useModal(() => import('@/components/modals/CodeHighlightModal.vue'), {
code,
});
})
</script>
<style lang="postcss" scoped>

View File

@ -35,22 +35,22 @@
</template>
<script lang="ts" setup>
import { useVModel } from "@vueuse/core";
import StoryParamsTable from "@/components/component-story/StoryParamsTable.vue";
import StoryWidget from "@/components/component-story/StoryWidget.vue";
import type { SettingParam } from "@/libs/story/story-param";
import { useVModel } from '@vueuse/core'
import StoryParamsTable from '@/components/component-story/StoryParamsTable.vue'
import StoryWidget from '@/components/component-story/StoryWidget.vue'
import type { SettingParam } from '@/libs/story/story-param'
const props = defineProps<{
params: SettingParam[];
modelValue?: Record<string, any>;
}>();
params: SettingParam[]
modelValue?: Record<string, any>
}>()
const emit = defineEmits<{
(event: "reset"): void;
(event: "update:modelValue", value: any): void;
}>();
(event: 'reset'): void
(event: 'update:modelValue', value: any): void
}>()
const model = useVModel(props, "modelValue", emit);
const model = useVModel(props, 'modelValue', emit)
</script>
<style lang="postcss">

View File

@ -25,11 +25,11 @@
</template>
<script lang="ts" setup>
import CodeHighlight from "@/components/CodeHighlight.vue";
import StoryParamsTable from "@/components/component-story/StoryParamsTable.vue";
import type { SlotParam } from "@/libs/story/story-param";
import CodeHighlight from '@/components/CodeHighlight.vue'
import StoryParamsTable from '@/components/component-story/StoryParamsTable.vue'
import type { SlotParam } from '@/libs/story/story-param'
defineProps<{
params: SlotParam[];
}>();
params: SlotParam[]
}>()
</script>

View File

@ -1,15 +1,7 @@
<template>
<FormSelect
v-if="isSelectWidget(widget)"
v-model="model"
:wrapper-attrs="{ class: 'full-width' }"
>
<FormSelect v-if="isSelectWidget(widget)" v-model="model" :wrapper-attrs="{ class: 'full-width' }">
<option v-if="!required && model === undefined" :value="undefined" />
<option
v-for="choice in widget.choices"
:key="choice.label"
:value="choice.value"
>
<option v-for="choice in widget.choices" :key="choice.label" :value="choice.value">
{{ choice.label }}
</option>
</FormSelect>
@ -22,11 +14,7 @@
<div v-else-if="isBooleanWidget(widget)">
<FormCheckbox v-model="model" />
</div>
<FormInput
v-else-if="isNumberWidget(widget)"
v-model.number="model"
type="number"
/>
<FormInput v-else-if="isNumberWidget(widget)" v-model.number="model" type="number" />
<FormInput v-else-if="isTextWidget(widget)" v-model="model" />
<FormJson v-else-if="isObjectWidget(widget)" v-model="model" />
</template>
@ -40,40 +28,28 @@ import {
isSelectWidget,
isTextWidget,
type Widget,
} from "@/libs/story/story-widget";
import { useVModel } from "@vueuse/core";
import { defineAsyncComponent } from "vue";
} from '@/libs/story/story-widget'
import { useVModel } from '@vueuse/core'
import { defineAsyncComponent } from 'vue'
const FormJson = defineAsyncComponent(
() => import("@/components/form/FormJson.vue")
);
const FormSelect = defineAsyncComponent(
() => import("@/components/form/FormSelect.vue")
);
const FormCheckbox = defineAsyncComponent(
() => import("@/components/form/FormCheckbox.vue")
);
const FormInput = defineAsyncComponent(
() => import("@/components/form/FormInput.vue")
);
const FormInputWrapper = defineAsyncComponent(
() => import("@/components/form/FormInputWrapper.vue")
);
const FormRadio = defineAsyncComponent(
() => import("@/components/form/FormRadio.vue")
);
const FormJson = defineAsyncComponent(() => import('@/components/form/FormJson.vue'))
const FormSelect = defineAsyncComponent(() => import('@/components/form/FormSelect.vue'))
const FormCheckbox = defineAsyncComponent(() => import('@/components/form/FormCheckbox.vue'))
const FormInput = defineAsyncComponent(() => import('@/components/form/FormInput.vue'))
const FormInputWrapper = defineAsyncComponent(() => import('@/components/form/FormInputWrapper.vue'))
const FormRadio = defineAsyncComponent(() => import('@/components/form/FormRadio.vue'))
const props = defineProps<{
widget: Widget;
modelValue: any;
required?: boolean;
}>();
widget: Widget
modelValue: any
required?: boolean
}>()
const emit = defineEmits<{
(event: "update:modelValue", value: any): void;
}>();
(event: 'update:modelValue', value: any): void
}>()
const model = useVModel(props, "modelValue", emit);
const model = useVModel(props, 'modelValue', emit)
</script>
<style lang="postcss" scoped>

View File

@ -2,11 +2,7 @@
<FormInputGroup>
<FormNumber v-model="sizeInput" :max-decimals="3" />
<FormSelect v-model="prefixInput">
<option
v-for="currentPrefix in availablePrefixes"
:key="currentPrefix"
:value="currentPrefix"
>
<option v-for="currentPrefix in availablePrefixes" :key="currentPrefix" :value="currentPrefix">
{{ currentPrefix }}B
</option>
</FormSelect>
@ -14,67 +10,66 @@
</template>
<script lang="ts" setup>
import FormInputGroup from "@/components/form/FormInputGroup.vue";
import FormNumber from "@/components/form/FormNumber.vue";
import FormSelect from "@/components/form/FormSelect.vue";
import { useVModel } from "@vueuse/core";
import humanFormat, { type Prefix } from "human-format";
import { ref, watch } from "vue";
import FormInputGroup from '@/components/form/FormInputGroup.vue'
import FormNumber from '@/components/form/FormNumber.vue'
import FormSelect from '@/components/form/FormSelect.vue'
import { useVModel } from '@vueuse/core'
import format, { type Prefix } from 'human-format'
import { ref, watch } from 'vue'
const props = defineProps<{
modelValue: number | undefined;
}>();
modelValue: number | undefined
}>()
const emit = defineEmits<{
(event: "update:modelValue", value: number): number;
}>();
(event: 'update:modelValue', value: number): number
}>()
const availablePrefixes: Prefix<"binary">[] = ["Ki", "Mi", "Gi"];
const availablePrefixes: Prefix<'binary'>[] = ['Ki', 'Mi', 'Gi']
const model = useVModel(props, "modelValue", emit, {
shouldEmit: (value) => value !== props.modelValue,
});
const model = useVModel(props, 'modelValue', emit, {
shouldEmit: value => value !== props.modelValue,
})
const sizeInput = ref();
const prefixInput = ref();
const sizeInput = ref()
const prefixInput = ref()
const scale = humanFormat.Scale.create(availablePrefixes, 1024, 1);
const scale = format.Scale.create(availablePrefixes, 1024, 1)
watch([sizeInput, prefixInput], ([newSize, newPrefix]) => {
if (newSize === "" || newSize === undefined) {
return;
if (newSize === '' || newSize === undefined) {
return
}
model.value = humanFormat.parse(`${newSize || 0} ${newPrefix || "Ki"}`, {
model.value = format.parse(`${newSize || 0} ${newPrefix || 'Ki'}`, {
scale,
});
});
})
})
watch(
() => props.modelValue,
(newValue) => {
newValue => {
if (newValue === undefined) {
sizeInput.value = undefined;
sizeInput.value = undefined
if (prefixInput.value === undefined) {
prefixInput.value = availablePrefixes[0];
prefixInput.value = availablePrefixes[0]
}
return;
return
}
const { value, prefix } = humanFormat.raw(newValue, {
const { value, prefix } = format.raw(newValue, {
scale,
prefix: prefixInput.value,
});
console.log(value);
})
sizeInput.value = value;
sizeInput.value = value
if (value !== 0) {
prefixInput.value = prefix;
prefixInput.value = prefix
}
},
{ immediate: true }
);
)
</script>

View File

@ -1,9 +1,5 @@
<template>
<component
:is="hasLabel ? 'span' : 'label'"
:class="`form-${type}`"
v-bind="wrapperAttrs"
>
<component :is="hasLabel ? 'span' : 'label'" :class="`form-${type}`" v-bind="wrapperAttrs">
<input
v-model="value"
:class="{ indeterminate: isIndeterminate }"
@ -19,51 +15,49 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useContext } from "@/composables/context.composable";
import { DisabledContext } from "@/context";
import { IK_CHECKBOX_TYPE, IK_FORM_HAS_LABEL } from "@/types/injection-keys";
import { faCheck, faCircle, faMinus } from "@fortawesome/free-solid-svg-icons";
import { useVModel } from "@vueuse/core";
import { computed, type HTMLAttributes, inject } from "vue";
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import { useContext } from '@/composables/context.composable'
import { DisabledContext } from '@/context'
import { IK_CHECKBOX_TYPE, IK_FORM_HAS_LABEL } from '@/types/injection-keys'
import { faCheck, faCircle, faMinus } from '@fortawesome/free-solid-svg-icons'
import { useVModel } from '@vueuse/core'
import { computed, type HTMLAttributes, inject } from 'vue'
defineOptions({ inheritAttrs: false });
defineOptions({ inheritAttrs: false })
const props = withDefaults(
defineProps<{
modelValue?: unknown;
disabled?: boolean;
wrapperAttrs?: HTMLAttributes;
modelValue?: unknown
disabled?: boolean
wrapperAttrs?: HTMLAttributes
}>(),
{ disabled: undefined }
);
)
const emit = defineEmits<{
(event: "update:modelValue", value: boolean): void;
}>();
(event: 'update:modelValue', value: boolean): void
}>()
const value = useVModel(props, "modelValue", emit);
const type = inject(IK_CHECKBOX_TYPE, "checkbox");
const value = useVModel(props, 'modelValue', emit)
const type = inject(IK_CHECKBOX_TYPE, 'checkbox')
const hasLabel = inject(
IK_FORM_HAS_LABEL,
computed(() => false)
);
const isDisabled = useContext(DisabledContext, () => props.disabled);
)
const isDisabled = useContext(DisabledContext, () => props.disabled)
const icon = computed(() => {
if (type !== "checkbox") {
return faCircle;
if (type !== 'checkbox') {
return faCircle
}
if (value.value === undefined) {
return faMinus;
return faMinus
}
return faCheck;
});
return faCheck
})
const isIndeterminate = computed(
() => (type === "checkbox" || type === "toggle") && value.value === undefined
);
const isIndeterminate = computed(() => (type === 'checkbox' || type === 'toggle') && value.value === undefined)
</script>
<style lang="postcss" scoped>
@ -151,8 +145,7 @@ const isIndeterminate = computed(
position: absolute;
color: var(--color-blue-scale-500);
filter: drop-shadow(0 0.0625em 0.5em rgba(0, 0, 0, 0.1))
drop-shadow(0 0.1875em 0.1875em rgba(0, 0, 0, 0.06))
filter: drop-shadow(0 0.0625em 0.5em rgba(0, 0, 0, 0.1)) drop-shadow(0 0.1875em 0.1875em rgba(0, 0, 0, 0.06))
drop-shadow(0 0.1875em 0.25em rgba(0, 0, 0, 0.08));
}

View File

@ -51,57 +51,48 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useContext } from "@/composables/context.composable";
import { ColorContext, DisabledContext } from "@/context";
import type { Color } from "@/types";
import { IK_INPUT_ID, IK_INPUT_TYPE } from "@/types/injection-keys";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
import { useTextareaAutosize, useVModel } from "@vueuse/core";
import {
computed,
type HTMLAttributes,
inject,
nextTick,
ref,
watch,
} from "vue";
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import { useContext } from '@/composables/context.composable'
import { ColorContext, DisabledContext } from '@/context'
import type { Color } from '@/types'
import { IK_INPUT_ID, IK_INPUT_TYPE } from '@/types/injection-keys'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { faAngleDown } from '@fortawesome/free-solid-svg-icons'
import { useTextareaAutosize, useVModel } from '@vueuse/core'
import { computed, type HTMLAttributes, inject, nextTick, ref, watch } from 'vue'
defineOptions({ inheritAttrs: false });
defineOptions({ inheritAttrs: false })
const props = withDefaults(
defineProps<{
id?: string;
modelValue?: any;
color?: Color;
before?: IconDefinition | string;
after?: IconDefinition | string;
beforeWidth?: string;
afterWidth?: string;
disabled?: boolean;
required?: boolean;
right?: boolean;
wrapperAttrs?: HTMLAttributes;
id?: string
modelValue?: any
color?: Color
before?: IconDefinition | string
after?: IconDefinition | string
beforeWidth?: string
afterWidth?: string
disabled?: boolean
required?: boolean
right?: boolean
wrapperAttrs?: HTMLAttributes
}>(),
{ disabled: undefined }
);
)
const { name: contextColor } = useContext(ColorContext, () => props.color);
const { name: contextColor } = useContext(ColorContext, () => props.color)
const inputElement = ref();
const inputElement = ref()
const emit = defineEmits<{
(event: "update:modelValue", value: any): void;
}>();
(event: 'update:modelValue', value: any): void
}>()
const value = useVModel(props, "modelValue", emit);
const isEmpty = computed(
() => props.modelValue == null || String(props.modelValue).trim() === ""
);
const inputType = inject(IK_INPUT_TYPE, "input");
const value = useVModel(props, 'modelValue', emit)
const isEmpty = computed(() => props.modelValue == null || String(props.modelValue).trim() === '')
const inputType = inject(IK_INPUT_TYPE, 'input')
const isDisabled = useContext(DisabledContext, () => props.disabled);
const isDisabled = useContext(DisabledContext, () => props.disabled)
const wrapperClass = computed(() => [
`form-${inputType}`,
@ -109,32 +100,32 @@ const wrapperClass = computed(() => [
disabled: isDisabled.value,
empty: isEmpty.value,
},
]);
])
const inputClass = computed(() => [
contextColor.value,
{
right: props.right,
"has-before": props.before !== undefined,
"has-after": props.after !== undefined,
'has-before': props.before !== undefined,
'has-after': props.after !== undefined,
},
]);
])
const parentId = inject(IK_INPUT_ID, undefined);
const parentId = inject(IK_INPUT_ID, undefined)
const id = computed(() => props.id ?? parentId?.value);
const id = computed(() => props.id ?? parentId?.value)
const { textarea, triggerResize } = useTextareaAutosize();
const { textarea, triggerResize } = useTextareaAutosize()
watch(value, () => nextTick(() => triggerResize()), {
immediate: true,
});
})
const focus = () => inputElement.value.focus();
const focus = () => inputElement.value.focus()
defineExpose({
focus,
});
})
</script>
<style lang="postcss" scoped>

View File

@ -1,21 +1,13 @@
<template>
<div class="form-input-wrapper">
<div
v-if="label !== undefined || learnMoreUrl !== undefined"
class="label-container"
>
<div v-if="label !== undefined || learnMoreUrl !== undefined" class="label-container">
<label :class="{ light }" :for="id" class="label">
<UiIcon :icon="icon" />
{{ label }}
</label>
<a
v-if="learnMoreUrl !== undefined"
:href="learnMoreUrl"
class="learn-more-url"
target="_blank"
>
<a v-if="learnMoreUrl !== undefined" :href="learnMoreUrl" class="learn-more-url" target="_blank">
<UiIcon :icon="faInfoCircle" />
<span>{{ $t("learn-more") }}</span>
<span>{{ $t('learn-more') }}</span>
</a>
</div>
<div class="input-container">
@ -36,55 +28,55 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useContext } from "@/composables/context.composable";
import { ColorContext, DisabledContext } from "@/context";
import type { Color } from "@/types";
import { IK_FORM_HAS_LABEL, IK_INPUT_ID } from "@/types/injection-keys";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { uniqueId } from "lodash-es";
import { computed, provide, useSlots } from "vue";
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import { useContext } from '@/composables/context.composable'
import { ColorContext, DisabledContext } from '@/context'
import type { Color } from '@/types'
import { IK_FORM_HAS_LABEL, IK_INPUT_ID } from '@/types/injection-keys'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'
import { uniqueId } from 'lodash-es'
import { computed, provide, useSlots } from 'vue'
const slots = useSlots();
const slots = useSlots()
const props = withDefaults(
defineProps<{
label?: string;
id?: string;
icon?: IconDefinition;
learnMoreUrl?: string;
warning?: string;
error?: string;
help?: string;
disabled?: boolean;
light?: boolean;
label?: string
id?: string
icon?: IconDefinition
learnMoreUrl?: string
warning?: string
error?: string
help?: string
disabled?: boolean
light?: boolean
}>(),
{ disabled: undefined }
);
)
const id = computed(() => props.id ?? uniqueId("form-input-"));
provide(IK_INPUT_ID, id);
const id = computed(() => props.id ?? uniqueId('form-input-'))
provide(IK_INPUT_ID, id)
const color = computed<Color | undefined>(() => {
if (props.error !== undefined && props.error.trim() !== "") {
return "error";
if (props.error !== undefined && props.error.trim() !== '') {
return 'error'
}
if (props.warning !== undefined && props.warning.trim() !== "") {
return "warning";
if (props.warning !== undefined && props.warning.trim() !== '') {
return 'warning'
}
return undefined;
});
return undefined
})
provide(
IK_FORM_HAS_LABEL,
computed(() => slots.label !== undefined)
);
)
useContext(ColorContext, color);
useContext(DisabledContext, () => props.disabled);
useContext(ColorContext, color)
useContext(DisabledContext, () => props.disabled)
</script>
<style lang="postcss" scoped>

View File

@ -1,37 +1,31 @@
<template>
<FormInput
:before="faCode"
:model-value="jsonValue"
readonly
@click="openModal()"
/>
<FormInput :before="faCode" :model-value="jsonValue" readonly @click="openModal()" />
</template>
<script lang="ts" setup>
import FormInput from "@/components/form/FormInput.vue";
import { useModal } from "@/composables/modal.composable";
import { faCode } from "@fortawesome/free-solid-svg-icons";
import { useVModel } from "@vueuse/core";
import { computed } from "vue";
import FormInput from '@/components/form/FormInput.vue'
import { useModal } from '@/composables/modal.composable'
import { faCode } from '@fortawesome/free-solid-svg-icons'
import { useVModel } from '@vueuse/core'
import { computed } from 'vue'
const props = defineProps<{
modelValue: any;
}>();
modelValue: any
}>()
const emit = defineEmits<{
(event: "update:modelValue", value: any): void;
}>();
(event: 'update:modelValue', value: any): void
}>()
const model = useVModel(props, "modelValue", emit);
const model = useVModel(props, 'modelValue', emit)
const jsonValue = computed(() => JSON.stringify(model.value, undefined, 2));
const jsonValue = computed(() => JSON.stringify(model.value, undefined, 2))
const openModal = () => {
const { onApprove } = useModal<string>(
() => import("@/components/modals/JsonEditorModal.vue"),
{ initialValue: jsonValue.value }
);
const { onApprove } = useModal<string>(() => import('@/components/modals/JsonEditorModal.vue'), {
initialValue: jsonValue.value,
})
onApprove((newValue) => (model.value = JSON.parse(newValue)));
};
onApprove(newValue => (model.value = JSON.parse(newValue)))
}
</script>

View File

@ -3,75 +3,70 @@
</template>
<script lang="ts" setup>
import FormInput from "@/components/form/FormInput.vue";
import { computed, ref, watch } from "vue";
import FormInput from '@/components/form/FormInput.vue'
import { computed, ref, watch } from 'vue'
const props = defineProps<{
modelValue: number | undefined;
maxDecimals?: number;
}>();
modelValue: number | undefined
maxDecimals?: number
}>()
const emit = defineEmits<{
(event: "update:modelValue", value: number | undefined): void;
}>();
(event: 'update:modelValue', value: number | undefined): void
}>()
const localValue = ref("");
const localValue = ref('')
const hasTrailingDot = ref(false);
const hasTrailingDot = ref(false)
const cleaningRegex = computed(() => {
if (props.maxDecimals === undefined) {
// Any number with optional decimal part
return /(\d*\.?\d*)/;
return /(\d*\.?\d*)/
}
if (props.maxDecimals > 0) {
// Numbers with up to `props.maxDecimals` decimal places
return new RegExp(`(\\d*\\.?\\d{0,${props.maxDecimals}})`);
return new RegExp(`(\\d*\\.?\\d{0,${props.maxDecimals}})`)
}
// Integer numbers only
return /(\d*)/;
});
return /(\d*)/
})
watch(
localValue,
(newLocalValue) => {
newLocalValue => {
const cleanValue =
localValue.value
.replace(",", ".")
.replace(/[^0-9.]/g, "")
.match(cleaningRegex.value)?.[0] ?? "";
.replace(',', '.')
.replace(/[^0-9.]/g, '')
.match(cleaningRegex.value)?.[0] ?? ''
hasTrailingDot.value = cleanValue.endsWith(".");
hasTrailingDot.value = cleanValue.endsWith('.')
if (cleanValue !== newLocalValue) {
localValue.value = cleanValue;
return;
localValue.value = cleanValue
return
}
if (newLocalValue === "") {
emit("update:modelValue", undefined);
return;
if (newLocalValue === '') {
emit('update:modelValue', undefined)
return
}
const parsedValue = parseFloat(cleanValue);
const parsedValue = parseFloat(cleanValue)
emit(
"update:modelValue",
Number.isNaN(parsedValue) ? undefined : parsedValue
);
emit('update:modelValue', Number.isNaN(parsedValue) ? undefined : parsedValue)
},
{ flush: "post" }
);
{ flush: 'post' }
)
watch(
() => props.modelValue,
(newModelValue) => {
localValue.value = `${newModelValue?.toString() ?? ""}${
hasTrailingDot.value ? "." : ""
}`;
newModelValue => {
localValue.value = `${newModelValue?.toString() ?? ''}${hasTrailingDot.value ? '.' : ''}`
},
{ immediate: true }
);
)
</script>

View File

@ -3,9 +3,9 @@
</template>
<script lang="ts" setup>
import FormCheckbox from "@/components/form/FormCheckbox.vue";
import { IK_CHECKBOX_TYPE } from "@/types/injection-keys";
import { provide } from "vue";
import FormCheckbox from '@/components/form/FormCheckbox.vue'
import { IK_CHECKBOX_TYPE } from '@/types/injection-keys'
import { provide } from 'vue'
provide(IK_CHECKBOX_TYPE, "radio");
provide(IK_CHECKBOX_TYPE, 'radio')
</script>

View File

@ -13,41 +13,41 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
import { useVModel, whenever } from "@vueuse/core";
import { computed } from "vue";
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'
import { useVModel, whenever } from '@vueuse/core'
import { computed } from 'vue'
const props = defineProps<{
label: string;
collapsible?: boolean;
collapsed?: boolean;
}>();
label: string
collapsible?: boolean
collapsed?: boolean
}>()
const emit = defineEmits<{
(event: "update:collapsed", value: boolean): void;
}>();
(event: 'update:collapsed', value: boolean): void
}>()
const isCollapsed = useVModel(props, "collapsed", emit);
const isCollapsed = useVModel(props, 'collapsed', emit)
const toggleCollapse = () => {
if (props.collapsible) {
isCollapsed.value = !isCollapsed.value;
isCollapsed.value = !isCollapsed.value
}
};
}
const icon = computed(() => {
if (!props.collapsible) {
return undefined;
return undefined
}
return isCollapsed.value ? faChevronDown : faChevronUp;
});
return isCollapsed.value ? faChevronDown : faChevronUp
})
whenever(
() => !props.collapsible,
() => (isCollapsed.value = false)
);
)
</script>
<style lang="postcss" scoped>

View File

@ -5,11 +5,11 @@
</template>
<script lang="ts" setup>
import FormInput from "@/components/form/FormInput.vue";
import { IK_INPUT_TYPE } from "@/types/injection-keys";
import { provide } from "vue";
import FormInput from '@/components/form/FormInput.vue'
import { IK_INPUT_TYPE } from '@/types/injection-keys'
import { provide } from 'vue'
provide(IK_INPUT_TYPE, "select");
provide(IK_INPUT_TYPE, 'select')
</script>
<style lang="postcss" scoped></style>

View File

@ -3,11 +3,11 @@
</template>
<script lang="ts" setup>
import FormInput from "@/components/form/FormInput.vue";
import { IK_INPUT_TYPE } from "@/types/injection-keys";
import { provide } from "vue";
import FormInput from '@/components/form/FormInput.vue'
import { IK_INPUT_TYPE } from '@/types/injection-keys'
import { provide } from 'vue'
provide(IK_INPUT_TYPE, "textarea");
provide(IK_INPUT_TYPE, 'textarea')
</script>
<style lang="postcss" scoped></style>

View File

@ -3,9 +3,9 @@
</template>
<script lang="ts" setup>
import FormCheckbox from "@/components/form/FormCheckbox.vue";
import { IK_CHECKBOX_TYPE } from "@/types/injection-keys";
import { provide } from "vue";
import FormCheckbox from '@/components/form/FormCheckbox.vue'
import { IK_CHECKBOX_TYPE } from '@/types/injection-keys'
import { provide } from 'vue'
provide(IK_CHECKBOX_TYPE, "toggle");
provide(IK_CHECKBOX_TYPE, 'toggle')
</script>

View File

@ -7,12 +7,12 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
defineProps<{
icon?: IconDefinition;
}>();
icon?: IconDefinition
}>()
</script>
<style lang="postcss" scoped>

View File

@ -5,25 +5,13 @@
:icon="faServer"
:route="{ name: 'host.dashboard', params: { uuid: host.uuid } }"
>
{{ host.name_label || "(Host)" }}
{{ host.name_label || '(Host)' }}
<template #actions>
<InfraAction
v-if="isPoolMaster"
v-tooltip="'Master'"
:icon="faStar"
class="master-icon"
/>
<p
class="vm-count"
v-tooltip="$t('vm-running', { count: vmCount })"
v-if="isReady"
>
<InfraAction v-if="isPoolMaster" v-tooltip="'Master'" :icon="faStar" class="master-icon" />
<p v-if="isReady" v-tooltip="$t('vm-running', { count: vmCount })" class="vm-count">
{{ vmCount }}
</p>
<InfraAction
:icon="isExpanded ? faAngleDown : faAngleUp"
@click="toggle()"
/>
<InfraAction :icon="isExpanded ? faAngleDown : faAngleUp" @click="toggle()" />
</template>
</InfraItemLabel>
@ -32,46 +20,37 @@
</template>
<script lang="ts" setup>
import InfraAction from "@/components/infra/InfraAction.vue";
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
import InfraVmList from "@/components/infra/InfraVmList.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { usePoolCollection } from "@/stores/xen-api/pool.store";
import { vTooltip } from "@/directives/tooltip.directive";
import type { XenApiHost } from "@/libs/xen-api/xen-api.types";
import { useUiStore } from "@/stores/ui.store";
import {
faAngleDown,
faAngleUp,
faServer,
faStar,
} from "@fortawesome/free-solid-svg-icons";
import { useToggle } from "@vueuse/core";
import { computed } from "vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import InfraAction from '@/components/infra/InfraAction.vue'
import InfraItemLabel from '@/components/infra/InfraItemLabel.vue'
import InfraVmList from '@/components/infra/InfraVmList.vue'
import { useHostCollection } from '@/stores/xen-api/host.store'
import { usePoolCollection } from '@/stores/xen-api/pool.store'
import { vTooltip } from '@/directives/tooltip.directive'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import { useUiStore } from '@/stores/ui.store'
import { faAngleDown, faAngleUp, faServer, faStar } from '@fortawesome/free-solid-svg-icons'
import { useToggle } from '@vueuse/core'
import { computed } from 'vue'
import { useVmCollection } from '@/stores/xen-api/vm.store'
const props = defineProps<{
hostOpaqueRef: XenApiHost["$ref"];
}>();
hostOpaqueRef: XenApiHost['$ref']
}>()
const { getByOpaqueRef } = useHostCollection();
const host = computed(() => getByOpaqueRef(props.hostOpaqueRef));
const { getByOpaqueRef } = useHostCollection()
const host = computed(() => getByOpaqueRef(props.hostOpaqueRef))
const { pool } = usePoolCollection();
const isPoolMaster = computed(() => pool.value?.master === props.hostOpaqueRef);
const { pool } = usePoolCollection()
const isPoolMaster = computed(() => pool.value?.master === props.hostOpaqueRef)
const uiStore = useUiStore();
const uiStore = useUiStore()
const isCurrentHost = computed(
() => props.hostOpaqueRef === uiStore.currentHostOpaqueRef
);
const [isExpanded, toggle] = useToggle(true);
const isCurrentHost = computed(() => props.hostOpaqueRef === uiStore.currentHostOpaqueRef)
const [isExpanded, toggle] = useToggle(true)
const { recordsByHostRef, isReady } = useVmCollection();
const { recordsByHostRef, isReady } = useVmCollection()
const vmCount = computed(
() => recordsByHostRef.value.get(props.hostOpaqueRef)?.length ?? 0
);
const vmCount = computed(() => recordsByHostRef.value.get(props.hostOpaqueRef)?.length ?? 0)
</script>
<style lang="postcss" scoped>

View File

@ -1,24 +1,20 @@
<template>
<ul class="infra-host-list">
<li v-if="hasError" class="text-error">
{{ $t("error-no-data") }}
{{ $t('error-no-data') }}
</li>
<li v-else-if="!isReady">{{ $t("loading-hosts") }}</li>
<li v-else-if="!isReady">{{ $t('loading-hosts') }}</li>
<template v-else>
<InfraHostItem
v-for="host in hosts"
:key="host.$ref"
:host-opaque-ref="host.$ref"
/>
<InfraHostItem v-for="host in hosts" :key="host.$ref" :host-opaque-ref="host.$ref" />
</template>
</ul>
</template>
<script lang="ts" setup>
import InfraHostItem from "@/components/infra/InfraHostItem.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
import InfraHostItem from '@/components/infra/InfraHostItem.vue'
import { useHostCollection } from '@/stores/xen-api/host.store'
const { records: hosts, isReady, hasError } = useHostCollection();
const { records: hosts, isReady, hasError } = useHostCollection()
</script>
<style lang="postcss" scoped>

View File

@ -1,13 +1,11 @@
<template>
<RouterLink v-slot="{ isExactActive, href, navigate }" :to="route" custom>
<div
:class="
isExactActive ? 'exact-active' : $props.active ? 'active' : undefined
"
:class="isExactActive ? 'exact-active' : $props.active ? 'active' : undefined"
class="infra-item-label"
v-bind="$attrs"
>
<a :href="href" class="link" @click="navigate" v-tooltip="hasTooltip">
<a v-tooltip="hasTooltip" :href="href" class="link" @click="navigate">
<UiIcon :icon="icon" class="icon" />
<div ref="textElement" class="text">
<slot />
@ -21,21 +19,21 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { vTooltip } from "@/directives/tooltip.directive";
import { hasEllipsis } from "@/libs/utils";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { computed, ref } from "vue";
import type { RouteLocationRaw } from "vue-router";
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import { vTooltip } from '@/directives/tooltip.directive'
import { hasEllipsis } from '@/libs/utils'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { computed, ref } from 'vue'
import type { RouteLocationRaw } from 'vue-router'
defineProps<{
icon: IconDefinition;
route: RouteLocationRaw;
active?: boolean;
}>();
icon: IconDefinition
route: RouteLocationRaw
active?: boolean
}>()
const textElement = ref<HTMLElement>();
const hasTooltip = computed(() => hasEllipsis(textElement.value));
const textElement = ref<HTMLElement>()
const hasTooltip = computed(() => hasEllipsis(textElement.value))
</script>
<style lang="postcss" scoped>

View File

@ -10,12 +10,12 @@
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
defineProps<{
icon: IconDefinition;
}>();
icon: IconDefinition
}>()
</script>
<style lang="postcss" scoped>

View File

@ -1,19 +1,12 @@
<template>
<ul class="infra-pool-list">
<li v-if="hasError" class="text-error">
{{ $t("error-no-data") }}
{{ $t('error-no-data') }}
</li>
<InfraLoadingItem
v-else-if="!isReady || pool === undefined"
:icon="faBuilding"
/>
<InfraLoadingItem v-else-if="!isReady || pool === undefined" :icon="faBuilding" />
<li v-else class="infra-pool-item">
<InfraItemLabel
:icon="faBuilding"
:route="{ name: 'pool.dashboard', params: { uuid: pool.uuid } }"
active
>
{{ pool.name_label || "(Pool)" }}
<InfraItemLabel :icon="faBuilding" :route="{ name: 'pool.dashboard', params: { uuid: pool.uuid } }" active>
{{ pool.name_label || '(Pool)' }}
</InfraItemLabel>
<InfraHostList />
@ -24,14 +17,14 @@
</template>
<script lang="ts" setup>
import InfraHostList from "@/components/infra/InfraHostList.vue";
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
import InfraLoadingItem from "@/components/infra/InfraLoadingItem.vue";
import InfraVmList from "@/components/infra/InfraVmList.vue";
import { usePoolCollection } from "@/stores/xen-api/pool.store";
import { faBuilding } from "@fortawesome/free-regular-svg-icons";
import InfraHostList from '@/components/infra/InfraHostList.vue'
import InfraItemLabel from '@/components/infra/InfraItemLabel.vue'
import InfraLoadingItem from '@/components/infra/InfraLoadingItem.vue'
import InfraVmList from '@/components/infra/InfraVmList.vue'
import { usePoolCollection } from '@/stores/xen-api/pool.store'
import { faBuilding } from '@fortawesome/free-regular-svg-icons'
const { isReady, hasError, pool } = usePoolCollection();
const { isReady, hasError, pool } = usePoolCollection()
</script>
<style lang="postcss" scoped>

View File

@ -1,11 +1,7 @@
<template>
<li v-if="vm !== undefined" ref="rootElement" class="infra-vm-item">
<InfraItemLabel
v-if="isVisible"
:icon="faDisplay"
:route="{ name: 'vm.console', params: { uuid: vm.uuid } }"
>
{{ vm.name_label || "(VM)" }}
<InfraItemLabel v-if="isVisible" :icon="faDisplay" :route="{ name: 'vm.console', params: { uuid: vm.uuid } }">
{{ vm.name_label || '(VM)' }}
<template #actions>
<InfraAction>
<PowerStateIcon :state="vm.power_state" />
@ -16,30 +12,30 @@
</template>
<script lang="ts" setup>
import InfraAction from "@/components/infra/InfraAction.vue";
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
import PowerStateIcon from "@/components/PowerStateIcon.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
import { useIntersectionObserver } from "@vueuse/core";
import { computed, ref } from "vue";
import InfraAction from '@/components/infra/InfraAction.vue'
import InfraItemLabel from '@/components/infra/InfraItemLabel.vue'
import PowerStateIcon from '@/components/PowerStateIcon.vue'
import { useVmCollection } from '@/stores/xen-api/vm.store'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types'
import { faDisplay } from '@fortawesome/free-solid-svg-icons'
import { useIntersectionObserver } from '@vueuse/core'
import { computed, ref } from 'vue'
const props = defineProps<{
vmOpaqueRef: XenApiVm["$ref"];
}>();
vmOpaqueRef: XenApiVm['$ref']
}>()
const { getByOpaqueRef } = useVmCollection();
const vm = computed(() => getByOpaqueRef(props.vmOpaqueRef));
const rootElement = ref();
const isVisible = ref(false);
const { getByOpaqueRef } = useVmCollection()
const vm = computed(() => getByOpaqueRef(props.vmOpaqueRef))
const rootElement = ref()
const isVisible = ref(false)
const { stop } = useIntersectionObserver(rootElement, ([entry]) => {
if (entry.isIntersecting) {
isVisible.value = true;
stop();
isVisible.value = true
stop()
}
});
})
</script>
<style lang="postcss" scoped>

View File

@ -1,6 +1,6 @@
<template>
<ul class="infra-vm-list">
<li v-if="hasError" class="text-error">{{ $t("error-no-data") }}</li>
<li v-if="hasError" class="text-error">{{ $t('error-no-data') }}</li>
<template v-else-if="!isReady">
<InfraLoadingItem v-for="i in 3" :key="i" :icon="faDisplay" />
</template>
@ -9,24 +9,20 @@
</template>
<script lang="ts" setup>
import InfraLoadingItem from "@/components/infra/InfraLoadingItem.vue";
import InfraVmItem from "@/components/infra/InfraVmItem.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import type { XenApiHost } from "@/libs/xen-api/xen-api.types";
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
import InfraLoadingItem from '@/components/infra/InfraLoadingItem.vue'
import InfraVmItem from '@/components/infra/InfraVmItem.vue'
import { useVmCollection } from '@/stores/xen-api/vm.store'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import { faDisplay } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue'
const props = defineProps<{
hostOpaqueRef?: XenApiHost["$ref"];
}>();
hostOpaqueRef?: XenApiHost['$ref']
}>()
const { isReady, recordsByHostRef, hasError } = useVmCollection();
const { isReady, recordsByHostRef, hasError } = useVmCollection()
const vms = computed(() =>
recordsByHostRef.value.get(
props.hostOpaqueRef ?? ("OpaqueRef:NULL" as XenApiHost["$ref"])
)
);
const vms = computed(() => recordsByHostRef.value.get(props.hostOpaqueRef ?? ('OpaqueRef:NULL' as XenApiHost['$ref'])))
</script>
<style lang="postcss" scoped>

View File

@ -1,99 +1,83 @@
<template>
<slot :is-open="isOpen" :open="open" name="trigger" />
<Teleport to="body" :disabled="!shouldTeleport">
<ul
v-if="!hasTrigger || isOpen"
ref="menu"
:class="{ horizontal, shadow }"
class="app-menu"
v-bind="$attrs"
>
<ul v-if="!hasTrigger || isOpen" ref="menu" :class="{ horizontal, shadow }" class="app-menu" v-bind="$attrs">
<slot />
</ul>
</Teleport>
</template>
<script lang="ts" setup>
import { useContext } from "@/composables/context.composable";
import { DisabledContext } from "@/context";
import {
IK_CLOSE_MENU,
IK_MENU_HORIZONTAL,
IK_MENU_TELEPORTED,
} from "@/types/injection-keys";
import placementJs, { type Options } from "placement.js";
import { computed, inject, nextTick, provide, ref, useSlots } from "vue";
import { onClickOutside, unrefElement, whenever } from "@vueuse/core";
import { useContext } from '@/composables/context.composable'
import { DisabledContext } from '@/context'
import { IK_CLOSE_MENU, IK_MENU_HORIZONTAL, IK_MENU_TELEPORTED } from '@/types/injection-keys'
import placementJs, { type Options } from 'placement.js'
import { computed, inject, nextTick, provide, ref, useSlots } from 'vue'
import { onClickOutside, unrefElement, whenever } from '@vueuse/core'
const props = withDefaults(
defineProps<{
horizontal?: boolean;
shadow?: boolean;
disabled?: boolean;
placement?: Options["placement"];
horizontal?: boolean
shadow?: boolean
disabled?: boolean
placement?: Options['placement']
}>(),
{ disabled: undefined }
);
)
defineOptions({
inheritAttrs: false,
});
})
const slots = useSlots();
const isOpen = ref(false);
const menu = ref();
const slots = useSlots()
const isOpen = ref(false)
const menu = ref()
const isParentHorizontal = inject(
IK_MENU_HORIZONTAL,
computed(() => false)
);
)
provide(
IK_MENU_HORIZONTAL,
computed(() => props.horizontal ?? false)
);
)
useContext(DisabledContext, () => props.disabled);
useContext(DisabledContext, () => props.disabled)
let clearClickOutsideEvent: (() => void) | undefined;
let clearClickOutsideEvent: (() => void) | undefined
const hasTrigger = useSlots().trigger !== undefined;
const hasTrigger = useSlots().trigger !== undefined
const shouldTeleport = hasTrigger && !inject(IK_MENU_TELEPORTED, false);
const shouldTeleport = hasTrigger && !inject(IK_MENU_TELEPORTED, false)
if (shouldTeleport) {
provide(IK_MENU_TELEPORTED, true);
provide(IK_MENU_TELEPORTED, true)
}
whenever(
() => !isOpen.value,
() => clearClickOutsideEvent?.()
);
)
if (slots.trigger && inject(IK_CLOSE_MENU, undefined) === undefined) {
provide(IK_CLOSE_MENU, () => (isOpen.value = false));
provide(IK_CLOSE_MENU, () => (isOpen.value = false))
}
const open = (event: MouseEvent) => {
if (isOpen.value) {
return (isOpen.value = false);
return (isOpen.value = false)
}
isOpen.value = true;
isOpen.value = true
nextTick(() => {
clearClickOutsideEvent = onClickOutside(
menu,
() => (isOpen.value = false),
{
ignore: [event.currentTarget as HTMLElement],
}
);
clearClickOutsideEvent = onClickOutside(menu, () => (isOpen.value = false), {
ignore: [event.currentTarget as HTMLElement],
})
placementJs(event.currentTarget as HTMLElement, unrefElement(menu), {
placement:
props.placement ??
(isParentHorizontal.value ? "bottom-start" : "right-start"),
});
});
};
placement: props.placement ?? (isParentHorizontal.value ? 'bottom-start' : 'right-start'),
})
})
}
</script>
<style lang="postcss" scoped>

View File

@ -12,19 +12,9 @@
</MenuTrigger>
<AppMenu v-else :disabled="isDisabled" shadow>
<template #trigger="{ open, isOpen }">
<MenuTrigger
:active="isOpen"
:busy="isBusy"
:disabled="isDisabled"
:icon="icon"
@click="open"
>
<MenuTrigger :active="isOpen" :busy="isBusy" :disabled="isDisabled" :icon="icon" @click="open">
<slot />
<UiIcon
:fixed-width="false"
:icon="submenuIcon"
class="submenu-icon"
/>
<UiIcon :fixed-width="false" :icon="submenuIcon" class="submenu-icon" />
</MenuTrigger>
</template>
<slot name="submenu" />
@ -33,53 +23,51 @@
</template>
<script lang="ts" setup>
import AppMenu from "@/components/menu/AppMenu.vue";
import MenuTrigger from "@/components/menu/MenuTrigger.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useContext } from "@/composables/context.composable";
import { DisabledContext } from "@/context";
import { IK_CLOSE_MENU, IK_MENU_HORIZONTAL } from "@/types/injection-keys";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faAngleDown, faAngleRight } from "@fortawesome/free-solid-svg-icons";
import { computed, inject, ref } from "vue";
import AppMenu from '@/components/menu/AppMenu.vue'
import MenuTrigger from '@/components/menu/MenuTrigger.vue'
import UiIcon from '@/components/ui/icon/UiIcon.vue'
import { useContext } from '@/composables/context.composable'
import { DisabledContext } from '@/context'
import { IK_CLOSE_MENU, IK_MENU_HORIZONTAL } from '@/types/injection-keys'
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
import { faAngleDown, faAngleRight } from '@fortawesome/free-solid-svg-icons'
import { computed, inject, ref } from 'vue'
const props = withDefaults(
defineProps<{
icon?: IconDefinition;
onClick?: () => any;
disabled?: boolean;
busy?: boolean;
icon?: IconDefinition
onClick?: () => any
disabled?: boolean
busy?: boolean
}>(),
{ disabled: undefined }
);
)
const isParentHorizontal = inject(
IK_MENU_HORIZONTAL,
computed(() => false)
);
const isDisabled = useContext(DisabledContext, () => props.disabled);
)
const isDisabled = useContext(DisabledContext, () => props.disabled)
const submenuIcon = computed(() =>
isParentHorizontal.value ? faAngleDown : faAngleRight
);
const submenuIcon = computed(() => (isParentHorizontal.value ? faAngleDown : faAngleRight))
const isHandlingClick = ref(false);
const isBusy = computed(() => isHandlingClick.value || props.busy === true);
const closeMenu = inject(IK_CLOSE_MENU, undefined);
const isHandlingClick = ref(false)
const isBusy = computed(() => isHandlingClick.value || props.busy === true)
const closeMenu = inject(IK_CLOSE_MENU, undefined)
const handleClick = async () => {
if (isDisabled.value || isBusy.value) {
return;
return
}
isHandlingClick.value = true;
isHandlingClick.value = true
try {
await props.onClick?.();
closeMenu?.();
await props.onClick?.()
closeMenu?.()
} finally {
isHandlingClick.value = false;
isHandlingClick.value = false
}
};
}
</script>
<style lang="postcss" scoped>

View File

@ -3,13 +3,13 @@
</template>
<script lang="ts" setup>
import { IK_MENU_HORIZONTAL } from "@/types/injection-keys";
import { computed, inject } from "vue";
import { IK_MENU_HORIZONTAL } from '@/types/injection-keys'
import { computed, inject } from 'vue'
const horizontal = inject(
IK_MENU_HORIZONTAL,
computed(() => false)
);
)
</script>
<style lang="postcss" scoped>

View File

@ -6,15 +6,15 @@
</template>
<script lang="ts" setup>
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import type { IconDefinition } from '@fortawesome/fontawesome-common-types'
import UiIcon from '@/components/ui/icon/UiIcon.vue'
defineProps<{
active?: boolean;
busy?: boolean;
disabled?: boolean;
icon?: IconDefinition;
}>();
active?: boolean
busy?: boolean
disabled?: boolean
icon?: IconDefinition
}>()
</script>
<style lang="postcss" scoped>

View File

@ -7,11 +7,11 @@
</template>
<script lang="ts" setup>
import CodeHighlight from "@/components/CodeHighlight.vue";
import BasicModalLayout from "@/components/ui/modals/layouts/BasicModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import CodeHighlight from '@/components/CodeHighlight.vue'
import BasicModalLayout from '@/components/ui/modals/layouts/BasicModalLayout.vue'
import UiModal from '@/components/ui/modals/UiModal.vue'
defineProps<{
code: any;
}>();
code: any
}>()
</script>

View File

@ -12,17 +12,10 @@
/>
</div>
<div
v-if="newFilters.some((filter) => filter.isAdvanced)"
class="available-properties"
>
{{ $t("available-properties-for-advanced-filter") }}
<div v-if="newFilters.some(filter => filter.isAdvanced)" class="available-properties">
{{ $t('available-properties-for-advanced-filter') }}
<div class="properties">
<UiBadge
v-for="(filter, property) in availableFilters"
:key="property"
:icon="getFilterIcon(filter)"
>
<UiBadge v-for="(filter, property) in availableFilters" :key="property" :icon="getFilterIcon(filter)">
{{ property }}
</UiBadge>
</div>
@ -31,11 +24,11 @@
<template #buttons>
<UiButton transparent @click="addNewFilter()">
{{ $t("add-or") }}
{{ $t('add-or') }}
</UiButton>
<ModalDeclineButton />
<ModalApproveButton :disabled="!isFilterValid">
{{ $t(editedFilter ? "update" : "add") }}
{{ $t(editedFilter ? 'update' : 'add') }}
</ModalApproveButton>
</template>
</ConfirmModalLayout>
@ -43,79 +36,76 @@
</template>
<script lang="ts" setup>
import CollectionFilterRow from "@/components/CollectionFilterRow.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import ModalApproveButton from "@/components/ui/modals/ModalApproveButton.vue";
import ModalDeclineButton from "@/components/ui/modals/ModalDeclineButton.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiBadge from "@/components/ui/UiBadge.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { getFilterIcon } from "@/libs/utils";
import type { Filters, NewFilter } from "@/types/filter";
import { IK_MODAL } from "@/types/injection-keys";
import { Or, parse } from "complex-matcher";
import { computed, inject, onMounted, ref } from "vue";
import CollectionFilterRow from '@/components/CollectionFilterRow.vue'
import ConfirmModalLayout from '@/components/ui/modals/layouts/ConfirmModalLayout.vue'
import ModalApproveButton from '@/components/ui/modals/ModalApproveButton.vue'
import ModalDeclineButton from '@/components/ui/modals/ModalDeclineButton.vue'
import UiModal from '@/components/ui/modals/UiModal.vue'
import UiBadge from '@/components/ui/UiBadge.vue'
import UiButton from '@/components/ui/UiButton.vue'
import { getFilterIcon } from '@/libs/utils'
import type { Filters, NewFilter } from '@/types/filter'
import { IK_MODAL } from '@/types/injection-keys'
import { Or, parse } from 'complex-matcher'
import { computed, inject, onMounted, ref } from 'vue'
const props = defineProps<{
availableFilters: Filters;
editedFilter?: string;
}>();
availableFilters: Filters
editedFilter?: string
}>()
const modal = inject(IK_MODAL)!;
const newFilters = ref<NewFilter[]>([]);
let newFilterId = 0;
const modal = inject(IK_MODAL)!
const newFilters = ref<NewFilter[]>([])
let newFilterId = 0
const addNewFilter = () =>
newFilters.value.push({
id: newFilterId++,
content: "",
content: '',
isAdvanced: false,
builder: { property: "", comparison: "", value: "", negate: false },
});
builder: { property: '', comparison: '', value: '', negate: false },
})
const removeNewFilter = (id: number) => {
const index = newFilters.value.findIndex((newFilter) => newFilter.id === id);
const index = newFilters.value.findIndex(newFilter => newFilter.id === id)
if (index >= 0) {
newFilters.value.splice(index, 1);
newFilters.value.splice(index, 1)
}
};
}
const generatedFilter = computed(() => {
const filters = newFilters.value.filter(
(newFilter) => newFilter.content !== ""
);
const filters = newFilters.value.filter(newFilter => newFilter.content !== '')
if (filters.length === 0) {
return "";
return ''
}
if (filters.length === 1) {
return filters[0].content;
return filters[0].content
}
return `|(${filters.map((filter) => filter.content).join(" ")})`;
});
return `|(${filters.map(filter => filter.content).join(' ')})`
})
const isFilterValid = computed(() => generatedFilter.value !== "");
const isFilterValid = computed(() => generatedFilter.value !== '')
onMounted(() => {
if (props.editedFilter === undefined) {
addNewFilter();
return;
addNewFilter()
return
}
const parsedFilter = parse(props.editedFilter);
const parsedFilter = parse(props.editedFilter)
const nodes =
parsedFilter instanceof Or ? parsedFilter.children : [parsedFilter];
const nodes = parsedFilter instanceof Or ? parsedFilter.children : [parsedFilter]
newFilters.value = nodes.map((node) => ({
newFilters.value = nodes.map(node => ({
id: newFilterId++,
content: node.toString(),
isAdvanced: true,
builder: { property: "", comparison: "", value: "", negate: false },
}));
});
builder: { property: '', comparison: '', value: '', negate: false },
}))
})
</script>
<style lang="postcss" scoped>

View File

@ -6,19 +6,15 @@
<FormWidget :label="$t('sort-by')">
<select v-model="newSortProperty">
<option v-if="!newSortProperty"></option>
<option
v-for="(sort, property) in availableSorts"
:key="property"
:value="property"
>
<option v-for="(sort, property) in availableSorts" :key="property" :value="property">
{{ sort.label ?? property }}
</option>
</select>
</FormWidget>
<FormWidget>
<select v-model="newSortIsAscending">
<option :value="true">{{ $t("ascending") }}</option>
<option :value="false">{{ $t("descending") }}</option>
<option :value="true">{{ $t('ascending') }}</option>
<option :value="false">{{ $t('descending') }}</option>
</select>
</FormWidget>
</div>
@ -26,37 +22,37 @@
<template #buttons>
<ModalDeclineButton />
<ModalApproveButton>{{ $t("add") }}</ModalApproveButton>
<ModalApproveButton>{{ $t('add') }}</ModalApproveButton>
</template>
</ConfirmModalLayout>
</UiModal>
</template>
<script lang="ts" setup>
import FormWidget from "@/components/FormWidget.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import ModalApproveButton from "@/components/ui/modals/ModalApproveButton.vue";
import ModalDeclineButton from "@/components/ui/modals/ModalDeclineButton.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import { IK_MODAL } from "@/types/injection-keys";
import type { NewSort, Sorts } from "@/types/sort";
import { inject, ref } from "vue";
import FormWidget from '@/components/FormWidget.vue'
import ConfirmModalLayout from '@/components/ui/modals/layouts/ConfirmModalLayout.vue'
import ModalApproveButton from '@/components/ui/modals/ModalApproveButton.vue'
import ModalDeclineButton from '@/components/ui/modals/ModalDeclineButton.vue'
import UiModal from '@/components/ui/modals/UiModal.vue'
import { IK_MODAL } from '@/types/injection-keys'
import type { NewSort, Sorts } from '@/types/sort'
import { inject, ref } from 'vue'
defineProps<{
availableSorts: Sorts;
}>();
availableSorts: Sorts
}>()
const newSortProperty = ref();
const newSortIsAscending = ref<boolean>(true);
const newSortProperty = ref()
const newSortIsAscending = ref<boolean>(true)
const modal = inject(IK_MODAL)!;
const modal = inject(IK_MODAL)!
const handleSubmit = () => {
modal.approve<NewSort>({
property: newSortProperty.value,
isAscending: newSortIsAscending.value,
});
};
})
}
</script>
<style lang="postcss" scoped>

View File

@ -1,7 +1,7 @@
<template>
<UiModal color="error" @submit="modal.approve()">
<ConfirmModalLayout :icon="faExclamationCircle">
<template #title>{{ $t("invalid-field") }}</template>
<template #title>{{ $t('invalid-field') }}</template>
<template #default>
{{ message }}
@ -9,7 +9,7 @@
<template #buttons>
<ModalApproveButton>
{{ $t("ok") }}
{{ $t('ok') }}
</ModalApproveButton>
</template>
</ConfirmModalLayout>
@ -17,18 +17,18 @@
</template>
<script lang="ts" setup>
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import ModalApproveButton from "@/components/ui/modals/ModalApproveButton.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import { IK_MODAL } from "@/types/injection-keys";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
import { inject } from "vue";
import ConfirmModalLayout from '@/components/ui/modals/layouts/ConfirmModalLayout.vue'
import ModalApproveButton from '@/components/ui/modals/ModalApproveButton.vue'
import UiModal from '@/components/ui/modals/UiModal.vue'
import { IK_MODAL } from '@/types/injection-keys'
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'
import { inject } from 'vue'
defineProps<{
message: string;
}>();
message: string
}>()
const modal = inject(IK_MODAL)!;
const modal = inject(IK_MODAL)!
</script>
<style lang="postcss" scoped></style>

View File

@ -1,8 +1,5 @@
<template>
<UiModal
:color="isJsonValid ? 'success' : 'error'"
@submit.prevent="handleSubmit()"
>
<UiModal :color="isJsonValid ? 'success' : 'error'" @submit.prevent="handleSubmit()">
<FormModalLayout :icon="faCode" class="layout">
<template #default>
<FormTextarea v-model="editedJson" class="modal-textarea" />
@ -10,11 +7,11 @@
<template #buttons>
<UiButton transparent @click="formatJson()">
{{ $t("reformat") }}
{{ $t('reformat') }}
</UiButton>
<ModalDeclineButton />
<ModalApproveButton :disabled="!isJsonValid">
{{ $t("save") }}
{{ $t('save') }}
</ModalApproveButton>
</template>
</FormModalLayout>
@ -22,49 +19,49 @@
</template>
<script lang="ts" setup>
import FormTextarea from "@/components/form/FormTextarea.vue";
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
import ModalApproveButton from "@/components/ui/modals/ModalApproveButton.vue";
import ModalDeclineButton from "@/components/ui/modals/ModalDeclineButton.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { IK_MODAL } from "@/types/injection-keys";
import { faCode } from "@fortawesome/free-solid-svg-icons";
import { computed, inject, ref } from "vue";
import FormTextarea from '@/components/form/FormTextarea.vue'
import FormModalLayout from '@/components/ui/modals/layouts/FormModalLayout.vue'
import ModalApproveButton from '@/components/ui/modals/ModalApproveButton.vue'
import ModalDeclineButton from '@/components/ui/modals/ModalDeclineButton.vue'
import UiModal from '@/components/ui/modals/UiModal.vue'
import UiButton from '@/components/ui/UiButton.vue'
import { IK_MODAL } from '@/types/injection-keys'
import { faCode } from '@fortawesome/free-solid-svg-icons'
import { computed, inject, ref } from 'vue'
const props = defineProps<{
initialValue?: string;
}>();
initialValue?: string
}>()
const editedJson = ref<string>(props.initialValue ?? "");
const modal = inject(IK_MODAL)!;
const editedJson = ref<string>(props.initialValue ?? '')
const modal = inject(IK_MODAL)!
const isJsonValid = computed(() => {
try {
JSON.parse(editedJson.value);
return true;
JSON.parse(editedJson.value)
return true
} catch {
return false;
return false
}
});
})
const formatJson = () => {
if (!isJsonValid.value) {
return;
return
}
editedJson.value = JSON.stringify(JSON.parse(editedJson.value), undefined, 2);
};
editedJson.value = JSON.stringify(JSON.parse(editedJson.value), undefined, 2)
}
const handleSubmit = () => {
if (!isJsonValid.value) {
return;
return
}
formatJson();
formatJson()
modal.approve(editedJson.value);
};
modal.approve(editedJson.value)
}
</script>
<style lang="postcss" scoped>

View File

@ -1,12 +1,12 @@
<template>
<UiModal color="error" @submit="modal.approve()">
<ConfirmModalLayout :icon="faServer">
<template #title>{{ $t("unreachable-hosts") }}</template>
<template #title>{{ $t('unreachable-hosts') }}</template>
<template #default>
<div class="description">
<p>{{ $t("following-hosts-unreachable") }}</p>
<p>{{ $t("allow-self-signed-ssl") }}</p>
<p>{{ $t('following-hosts-unreachable') }}</p>
<p>{{ $t('allow-self-signed-ssl') }}</p>
<ul>
<li v-for="url in urls" :key="url">
<a :href="url" class="link" rel="noopener" target="_blank">
@ -20,7 +20,7 @@
<template #buttons>
<ModalDeclineButton />
<ModalApproveButton>
{{ $t("unreachable-hosts-reload-page") }}
{{ $t('unreachable-hosts-reload-page') }}
</ModalApproveButton>
</template>
</ConfirmModalLayout>
@ -28,19 +28,19 @@
</template>
<script lang="ts" setup>
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import ModalApproveButton from "@/components/ui/modals/ModalApproveButton.vue";
import ModalDeclineButton from "@/components/ui/modals/ModalDeclineButton.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import { IK_MODAL } from "@/types/injection-keys";
import { faServer } from "@fortawesome/free-solid-svg-icons";
import { inject } from "vue";
import ConfirmModalLayout from '@/components/ui/modals/layouts/ConfirmModalLayout.vue'
import ModalApproveButton from '@/components/ui/modals/ModalApproveButton.vue'
import ModalDeclineButton from '@/components/ui/modals/ModalDeclineButton.vue'
import UiModal from '@/components/ui/modals/UiModal.vue'
import { IK_MODAL } from '@/types/injection-keys'
import { faServer } from '@fortawesome/free-solid-svg-icons'
import { inject } from 'vue'
defineProps<{
urls: string[];
}>();
urls: string[]
}>()
const modal = inject(IK_MODAL)!;
const modal = inject(IK_MODAL)!
</script>
<style lang="postcss" scoped>

View File

@ -4,21 +4,21 @@
<template #title>
<i18n-t keypath="confirm-delete" scope="global" tag="div">
<span :class="textClass">
{{ $t("n-vms", { n: vmRefs.length }) }}
{{ $t('n-vms', { n: vmRefs.length }) }}
</span>
</i18n-t>
</template>
<template #subtitle>
{{ $t("please-confirm") }}
{{ $t('please-confirm') }}
</template>
<template #buttons>
<ModalDeclineButton>
{{ $t("go-back") }}
{{ $t('go-back') }}
</ModalDeclineButton>
<ModalApproveButton>
{{ $t("delete-vms", { n: vmRefs.length }) }}
{{ $t('delete-vms', { n: vmRefs.length }) }}
</ModalApproveButton>
</template>
</ConfirmModalLayout>
@ -26,28 +26,28 @@
</template>
<script lang="ts" setup>
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import ModalApproveButton from "@/components/ui/modals/ModalApproveButton.vue";
import ModalDeclineButton from "@/components/ui/modals/ModalDeclineButton.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import { useContext } from "@/composables/context.composable";
import { ColorContext } from "@/context";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useXenApiStore } from "@/stores/xen-api.store";
import { IK_MODAL } from "@/types/injection-keys";
import { faSatellite } from "@fortawesome/free-solid-svg-icons";
import { inject } from "vue";
import ConfirmModalLayout from '@/components/ui/modals/layouts/ConfirmModalLayout.vue'
import ModalApproveButton from '@/components/ui/modals/ModalApproveButton.vue'
import ModalDeclineButton from '@/components/ui/modals/ModalDeclineButton.vue'
import UiModal from '@/components/ui/modals/UiModal.vue'
import { useContext } from '@/composables/context.composable'
import { ColorContext } from '@/context'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types'
import { useXenApiStore } from '@/stores/xen-api.store'
import { IK_MODAL } from '@/types/injection-keys'
import { faSatellite } from '@fortawesome/free-solid-svg-icons'
import { inject } from 'vue'
const props = defineProps<{
vmRefs: XenApiVm["$ref"][];
}>();
vmRefs: XenApiVm['$ref'][]
}>()
const modal = inject(IK_MODAL)!;
const modal = inject(IK_MODAL)!
const { textClass } = useContext(ColorContext);
const { textClass } = useContext(ColorContext)
const handleSubmit = () => {
const xenApi = useXenApiStore().getXapi();
modal.approve(xenApi.vm.delete(props.vmRefs));
};
const xenApi = useXenApiStore().getXapi()
modal.approve(xenApi.vm.delete(props.vmRefs))
}
</script>

View File

@ -2,11 +2,11 @@
<UiModal>
<FormModalLayout :icon="faDisplay">
<template #title>
{{ $t("export-n-vms-manually", { n: labelWithUrl.length }) }}
{{ $t('export-n-vms-manually', { n: labelWithUrl.length }) }}
</template>
<p>
{{ $t("export-vms-manually-information") }}
{{ $t('export-vms-manually-information') }}
</p>
<ul class="list">
<li v-for="({ url, label }, index) in labelWithUrl" :key="index">
@ -24,29 +24,29 @@
</template>
<script lang="ts" setup>
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
import ModalDeclineButton from "@/components/ui/modals/ModalDeclineButton.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
import FormModalLayout from '@/components/ui/modals/layouts/FormModalLayout.vue'
import ModalDeclineButton from '@/components/ui/modals/ModalDeclineButton.vue'
import UiModal from '@/components/ui/modals/UiModal.vue'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types'
import { useVmCollection } from '@/stores/xen-api/vm.store'
import { faDisplay } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue'
const props = defineProps<{
blockedUrls: URL[];
}>();
blockedUrls: URL[]
}>()
const { getByOpaqueRef } = useVmCollection();
const { getByOpaqueRef } = useVmCollection()
const labelWithUrl = computed(() =>
props.blockedUrls.map((url) => {
const ref = url.searchParams.get("ref") as XenApiVm["$ref"];
props.blockedUrls.map(url => {
const ref = url.searchParams.get('ref') as XenApiVm['$ref']
return {
url: url,
url,
label: getByOpaqueRef(ref)?.name_label ?? ref,
};
}
})
);
)
</script>
<style lang="postcss" scoped>

View File

@ -2,7 +2,7 @@
<UiModal @submit.prevent="handleSubmit">
<FormModalLayout :icon="faDisplay">
<template #title>
{{ $t("export-n-vms", { n: vmRefs.length }) }}
{{ $t('export-n-vms', { n: vmRefs.length }) }}
</template>
<FormInputWrapper
@ -14,9 +14,7 @@
<option
v-for="key in Object.keys(VM_COMPRESSION_TYPE)"
:key="key"
:value="
VM_COMPRESSION_TYPE[key as keyof typeof VM_COMPRESSION_TYPE]
"
:value="VM_COMPRESSION_TYPE[key as keyof typeof VM_COMPRESSION_TYPE]"
>
{{ $t(key.toLowerCase()) }}
</option>
@ -26,7 +24,7 @@
<template #buttons>
<ModalDeclineButton />
<ModalApproveButton>
{{ $t("export-n-vms", { n: vmRefs.length }) }}
{{ $t('export-n-vms', { n: vmRefs.length }) }}
</ModalApproveButton>
</template>
</FormModalLayout>
@ -34,32 +32,32 @@
</template>
<script lang="ts" setup>
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
import { inject, ref } from "vue";
import { faDisplay } from '@fortawesome/free-solid-svg-icons'
import { inject, ref } from 'vue'
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
import FormSelect from "@/components/form/FormSelect.vue";
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
import ModalApproveButton from "@/components/ui/modals/ModalApproveButton.vue";
import ModalDeclineButton from "@/components/ui/modals/ModalDeclineButton.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import { IK_MODAL } from "@/types/injection-keys";
import { useXenApiStore } from "@/stores/xen-api.store";
import { VM_COMPRESSION_TYPE } from "@/libs/xen-api/xen-api.enums";
import FormInputWrapper from '@/components/form/FormInputWrapper.vue'
import FormSelect from '@/components/form/FormSelect.vue'
import FormModalLayout from '@/components/ui/modals/layouts/FormModalLayout.vue'
import ModalApproveButton from '@/components/ui/modals/ModalApproveButton.vue'
import ModalDeclineButton from '@/components/ui/modals/ModalDeclineButton.vue'
import UiModal from '@/components/ui/modals/UiModal.vue'
import { IK_MODAL } from '@/types/injection-keys'
import { useXenApiStore } from '@/stores/xen-api.store'
import { VM_COMPRESSION_TYPE } from '@/libs/xen-api/xen-api.enums'
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import type { XenApiVm } from '@/libs/xen-api/xen-api.types'
const props = defineProps<{
vmRefs: XenApiVm["$ref"][];
}>();
vmRefs: XenApiVm['$ref'][]
}>()
const modal = inject(IK_MODAL)!;
const modal = inject(IK_MODAL)!
const compressionType = ref(VM_COMPRESSION_TYPE.DISABLED);
const compressionType = ref(VM_COMPRESSION_TYPE.DISABLED)
const handleSubmit = () => {
const xenApi = useXenApiStore().getXapi();
xenApi.vm.export(props.vmRefs, compressionType.value);
modal.approve();
};
const xenApi = useXenApiStore().getXapi()
xenApi.vm.export(props.vmRefs, compressionType.value)
modal.approve()
}
</script>

View File

@ -2,20 +2,16 @@
<UiModal @submit.prevent="handleSubmit()">
<FormModalLayout>
<template #title>
{{ $t("migrate-n-vms", { n: vmRefs.length }) }}
{{ $t('migrate-n-vms', { n: vmRefs.length }) }}
</template>
<div>
<FormInputWrapper :label="$t('select-destination-host')" light>
<FormSelect v-model="selectedHost">
<option :value="undefined">
{{ $t("select-destination-host") }}
{{ $t('select-destination-host') }}
</option>
<option
v-for="host in availableHosts"
:key="host.$ref"
:value="host"
>
<option v-for="host in availableHosts" :key="host.$ref" :value="host">
{{ host.name_label }}
</option>
</FormSelect>
@ -25,7 +21,7 @@
<template #buttons>
<ModalDeclineButton />
<ModalApproveButton>
{{ $t("migrate-n-vms", { n: vmRefs.length }) }}
{{ $t('migrate-n-vms', { n: vmRefs.length }) }}
</ModalApproveButton>
</template>
</FormModalLayout>
@ -33,32 +29,30 @@
</template>
<script lang="ts" setup>
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
import FormSelect from "@/components/form/FormSelect.vue";
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
import ModalApproveButton from "@/components/ui/modals/ModalApproveButton.vue";
import ModalDeclineButton from "@/components/ui/modals/ModalDeclineButton.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import { useVmMigration } from "@/composables/vm-migration.composable";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { IK_MODAL } from "@/types/injection-keys";
import { inject } from "vue";
import FormInputWrapper from '@/components/form/FormInputWrapper.vue'
import FormSelect from '@/components/form/FormSelect.vue'
import FormModalLayout from '@/components/ui/modals/layouts/FormModalLayout.vue'
import ModalApproveButton from '@/components/ui/modals/ModalApproveButton.vue'
import ModalDeclineButton from '@/components/ui/modals/ModalDeclineButton.vue'
import UiModal from '@/components/ui/modals/UiModal.vue'
import { useVmMigration } from '@/composables/vm-migration.composable'
import type { XenApiVm } from '@/libs/xen-api/xen-api.types'
import { IK_MODAL } from '@/types/injection-keys'
import { inject } from 'vue'
const props = defineProps<{
vmRefs: XenApiVm["$ref"][];
}>();
vmRefs: XenApiVm['$ref'][]
}>()
const modal = inject(IK_MODAL)!;
const modal = inject(IK_MODAL)!
const { selectedHost, availableHosts, isValid, migrate } = useVmMigration(
() => props.vmRefs
);
const { selectedHost, availableHosts, isValid, migrate } = useVmMigration(() => props.vmRefs)
const handleSubmit = () => {
if (!isValid.value) {
return;
return
}
modal.approve(migrate());
};
modal.approve(migrate())
}
</script>

View File

@ -5,12 +5,12 @@
</template>
<script lang="ts" setup>
import { usePoolCollection } from "@/stores/xen-api/pool.store";
import { computed } from "vue";
import { faBuilding } from "@fortawesome/free-regular-svg-icons";
import TitleBar from "@/components/TitleBar.vue";
import { usePoolCollection } from '@/stores/xen-api/pool.store'
import { computed } from 'vue'
import { faBuilding } from '@fortawesome/free-regular-svg-icons'
import TitleBar from '@/components/TitleBar.vue'
const { pool } = usePoolCollection();
const { pool } = usePoolCollection()
const name = computed(() => pool.value?.name_label ?? "...");
const name = computed(() => pool.value?.name_label ?? '...')
</script>

View File

@ -1,39 +1,39 @@
<template>
<UiTabBar :disabled="!isReady">
<RouterTab :to="{ name: 'pool.dashboard', params: { uuid: pool?.uuid } }">
{{ $t("dashboard") }}
{{ $t('dashboard') }}
</RouterTab>
<RouterTab :to="{ name: 'pool.alarms', params: { uuid: pool?.uuid } }">
{{ $t("alarms") }}
{{ $t('alarms') }}
</RouterTab>
<RouterTab :to="{ name: 'pool.stats', params: { uuid: pool?.uuid } }">
{{ $t("stats") }}
{{ $t('stats') }}
</RouterTab>
<RouterTab :to="{ name: 'pool.system', params: { uuid: pool?.uuid } }">
{{ $t("system") }}
{{ $t('system') }}
</RouterTab>
<RouterTab :to="{ name: 'pool.network', params: { uuid: pool?.uuid } }">
{{ $t("network") }}
{{ $t('network') }}
</RouterTab>
<RouterTab :to="{ name: 'pool.storage', params: { uuid: pool?.uuid } }">
{{ $t("storage") }}
{{ $t('storage') }}
</RouterTab>
<RouterTab :to="{ name: 'pool.tasks', params: { uuid: pool?.uuid } }">
{{ $t("tasks") }}
{{ $t('tasks') }}
</RouterTab>
<RouterTab :to="{ name: 'pool.hosts', params: { uuid: pool?.uuid } }">
{{ $t("hosts") }}
{{ $t('hosts') }}
</RouterTab>
<RouterTab :to="{ name: 'pool.vms', params: { uuid: pool?.uuid } }">
{{ $t("vms") }}
{{ $t('vms') }}
</RouterTab>
</UiTabBar>
</template>
<script lang="ts" setup>
import RouterTab from "@/components/RouterTab.vue";
import UiTabBar from "@/components/ui/UiTabBar.vue";
import { usePoolCollection } from "@/stores/xen-api/pool.store";
import RouterTab from '@/components/RouterTab.vue'
import UiTabBar from '@/components/ui/UiTabBar.vue'
import { usePoolCollection } from '@/stores/xen-api/pool.store'
const { pool, isReady } = usePoolCollection();
const { pool, isReady } = usePoolCollection()
</script>

View File

@ -1,7 +1,7 @@
<template>
<UiCard class="pool-dashboard-alarms">
<UiCardTitle>
{{ $t("alarms") }}
{{ $t('alarms') }}
<template v-if="isReady && alarms.length > 0" #right>
<UiCounter :value="alarms.length" color="error" />
</template>
@ -9,9 +9,9 @@
<div v-if="!isStarted" class="pre-start">
<div>
<p class="text">
{{ $t("click-to-display-alarms") }}
{{ $t('click-to-display-alarms') }}
</p>
<UiButton @click="start">{{ $t("load-now") }}</UiButton>
<UiButton @click="start">{{ $t('load-now') }}</UiButton>
</div>
<div>
<img alt="" src="@/assets/server-status.svg" />
@ -25,9 +25,7 @@
<div>
<img alt="" src="@/assets/server-status.svg" />
</div>
<p class="text">
{{ $t("all-good") }}<br />{{ $t("no-alarm-triggered") }}
</p>
<p class="text">{{ $t('all-good') }}<br />{{ $t('no-alarm-triggered') }}</p>
</div>
<div v-else class="table-container">
<UiTable>
@ -40,23 +38,17 @@
</template>
<script lang="ts" setup>
import NoDataError from "@/components/NoDataError.vue";
import AlarmRow from "@/components/pool/dashboard/alarm/AlarmRow.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiCounter from "@/components/ui/UiCounter.vue";
import UiTable from "@/components/ui/UiTable.vue";
import { useAlarmCollection } from "@/stores/xen-api/alarm.store";
import NoDataError from '@/components/NoDataError.vue'
import AlarmRow from '@/components/pool/dashboard/alarm/AlarmRow.vue'
import UiButton from '@/components/ui/UiButton.vue'
import UiCard from '@/components/ui/UiCard.vue'
import UiCardSpinner from '@/components/ui/UiCardSpinner.vue'
import UiCardTitle from '@/components/ui/UiCardTitle.vue'
import UiCounter from '@/components/ui/UiCounter.vue'
import UiTable from '@/components/ui/UiTable.vue'
import { useAlarmCollection } from '@/stores/xen-api/alarm.store'
const {
records: alarms,
start,
isStarted,
isReady,
hasError,
} = useAlarmCollection({ defer: true });
const { records: alarms, start, isStarted, isReady, hasError } = useAlarmCollection({ defer: true })
</script>
<style lang="postcss" scoped>

View File

@ -1,7 +1,7 @@
<template>
<UiCard :color="hasError ? 'error' : undefined">
<UiCardTitle>
{{ $t("cpu-provisioning") }}
{{ $t('cpu-provisioning') }}
<template v-if="!hasError" #right>
<UiStatusIcon
v-if="state !== 'success'"
@ -20,11 +20,11 @@
<UiProgressLegend :label="$t('vcpus')" :value="`${value}%`" />
<UiCardFooter class="ui-card-footer">
<template #left>
<p>{{ $t("vcpus-used") }}</p>
<p>{{ $t('vcpus-used') }}</p>
<p class="footer-value">{{ nVCpuInUse }}</p>
</template>
<template #right>
<p>{{ $t("total-cpus") }}</p>
<p>{{ $t('total-cpus') }}</p>
<p class="footer-value">{{ nPCpu }}</p>
</template>
</UiCardFooter>
@ -34,74 +34,49 @@
</template>
<script lang="ts" setup>
import NoDataError from "@/components/NoDataError.vue";
import UiStatusIcon from "@/components/ui/icon/UiStatusIcon.vue";
import UiProgressBar from "@/components/ui/progress/UiProgressBar.vue";
import UiProgressLegend from "@/components/ui/progress/UiProgressLegend.vue";
import UiProgressScale from "@/components/ui/progress/UiProgressScale.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardFooter from "@/components/ui/UiCardFooter.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { vTooltip } from "@/directives/tooltip.directive";
import { percent } from "@/libs/utils";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmMetricsCollection } from "@/stores/xen-api/vm-metrics.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { logicAnd } from "@vueuse/math";
import { computed } from "vue";
import NoDataError from '@/components/NoDataError.vue'
import UiStatusIcon from '@/components/ui/icon/UiStatusIcon.vue'
import UiProgressBar from '@/components/ui/progress/UiProgressBar.vue'
import UiProgressLegend from '@/components/ui/progress/UiProgressLegend.vue'
import UiProgressScale from '@/components/ui/progress/UiProgressScale.vue'
import UiCard from '@/components/ui/UiCard.vue'
import UiCardFooter from '@/components/ui/UiCardFooter.vue'
import UiCardSpinner from '@/components/ui/UiCardSpinner.vue'
import UiCardTitle from '@/components/ui/UiCardTitle.vue'
import { vTooltip } from '@/directives/tooltip.directive'
import { percent } from '@/libs/utils'
import { VM_POWER_STATE } from '@/libs/xen-api/xen-api.enums'
import { useHostCollection } from '@/stores/xen-api/host.store'
import { useVmMetricsCollection } from '@/stores/xen-api/vm-metrics.store'
import { useVmCollection } from '@/stores/xen-api/vm.store'
import { logicAnd } from '@vueuse/math'
import { computed } from 'vue'
const ACTIVE_STATES = new Set([VM_POWER_STATE.RUNNING, VM_POWER_STATE.PAUSED]);
const ACTIVE_STATES = new Set([VM_POWER_STATE.RUNNING, VM_POWER_STATE.PAUSED])
const {
hasError: hostStoreHasError,
isReady: isHostStoreReady,
runningHosts,
} = useHostCollection();
const { hasError: hostStoreHasError, isReady: isHostStoreReady, runningHosts } = useHostCollection()
const {
hasError: vmStoreHasError,
isReady: isVmStoreReady,
records: vms,
} = useVmCollection();
const { hasError: vmStoreHasError, isReady: isVmStoreReady, records: vms } = useVmCollection()
const { getByOpaqueRef: getVmMetrics, isReady: isVmMetricsStoreReady } =
useVmMetricsCollection();
const { getByOpaqueRef: getVmMetrics, isReady: isVmMetricsStoreReady } = useVmMetricsCollection()
const nPCpu = computed(() =>
runningHosts.value.reduce(
(total, host) => total + Number(host.cpu_info.cpu_count),
0
)
);
const nPCpu = computed(() => runningHosts.value.reduce((total, host) => total + Number(host.cpu_info.cpu_count), 0))
const nVCpuInUse = computed(() => {
if (!isReady.value) {
return 0;
return 0
}
return vms.value.reduce(
(total, vm) =>
ACTIVE_STATES.has(vm.power_state)
? total + getVmMetrics(vm.metrics)!.VCPUs_number
: total,
(total, vm) => (ACTIVE_STATES.has(vm.power_state) ? total + getVmMetrics(vm.metrics)!.VCPUs_number : total),
0
);
});
const value = computed(() =>
Math.round(percent(nVCpuInUse.value, nPCpu.value))
);
const maxValue = computed(() => Math.ceil(value.value / 100) * 100);
const state = computed(() => (value.value > 100 ? "warning" : "success"));
const isReady = logicAnd(
isVmStoreReady,
isHostStoreReady,
isVmMetricsStoreReady
);
const hasError = computed(
() => hostStoreHasError.value || vmStoreHasError.value
);
)
})
const value = computed(() => Math.round(percent(nVCpuInUse.value, nPCpu.value)))
const maxValue = computed(() => Math.ceil(value.value / 100) * 100)
const state = computed(() => (value.value > 100 ? 'warning' : 'success'))
const isReady = logicAnd(isVmStoreReady, isHostStoreReady, isVmMetricsStoreReady)
const hasError = computed(() => hostStoreHasError.value || vmStoreHasError.value)
</script>
<style lang="postcss" scoped>

View File

@ -1,7 +1,7 @@
<template>
<UiCard :color="hasError ? 'error' : undefined">
<UiCardTitle>
{{ $t("cpu-usage") }}
{{ $t('cpu-usage') }}
<template v-if="vmStatsCanBeExpired || hostStatsCanBeExpired" #right>
<UiSpinner v-tooltip="$t('fetching-fresh-data')" />
</template>
@ -11,38 +11,34 @@
</UiCard>
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { vTooltip } from "@/directives/tooltip.directive";
import HostsCpuUsage from "@/components/pool/dashboard/cpuUsage/HostsCpuUsage.vue";
import VmsCpuUsage from "@/components/pool/dashboard/cpuUsage/VmsCpuUsage.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { computed, inject, type ComputedRef } from "vue";
import type { Stat } from "@/composables/fetch-stats.composable";
import type { HostStats, VmStats } from "@/libs/xapi-stats";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import { useHostCollection } from '@/stores/xen-api/host.store'
import { useVmCollection } from '@/stores/xen-api/vm.store'
import { vTooltip } from '@/directives/tooltip.directive'
import HostsCpuUsage from '@/components/pool/dashboard/cpuUsage/HostsCpuUsage.vue'
import VmsCpuUsage from '@/components/pool/dashboard/cpuUsage/VmsCpuUsage.vue'
import UiCard from '@/components/ui/UiCard.vue'
import UiCardTitle from '@/components/ui/UiCardTitle.vue'
import { computed, inject, type ComputedRef } from 'vue'
import type { Stat } from '@/composables/fetch-stats.composable'
import type { HostStats, VmStats } from '@/libs/xapi-stats'
import UiSpinner from '@/components/ui/UiSpinner.vue'
const { hasError: hasVmError } = useVmCollection();
const { hasError: hasHostError } = useHostCollection();
const { hasError: hasVmError } = useVmCollection()
const { hasError: hasHostError } = useHostCollection()
const vmStats = inject<ComputedRef<Stat<VmStats>[]>>(
"vmStats",
'vmStats',
computed(() => [])
);
)
const hostStats = inject<ComputedRef<Stat<HostStats>[]>>(
"hostStats",
'hostStats',
computed(() => [])
);
)
const vmStatsCanBeExpired = computed(() =>
vmStats.value.some((stat) => stat.canBeExpired)
);
const vmStatsCanBeExpired = computed(() => vmStats.value.some(stat => stat.canBeExpired))
const hostStatsCanBeExpired = computed(() =>
hostStats.value.some((stat) => stat.canBeExpired)
);
const hostStatsCanBeExpired = computed(() => hostStats.value.some(stat => stat.canBeExpired))
const hasError = computed(() => hasVmError.value || hasHostError.value);
const hasError = computed(() => hasVmError.value || hasHostError.value)
</script>

View File

@ -1,9 +1,9 @@
<template>
<UiCard>
<UiCardTitle class="patches-title">
{{ $t("patches") }}
{{ $t('patches') }}
<template v-if="areAllLoaded && count > 0" #right>
{{ $t("n-missing", { n: count }) }}
{{ $t('n-missing', { n: count }) }}
</template>
</UiCardTitle>
<div class="table-container">
@ -18,15 +18,15 @@
</template>
<script lang="ts" setup>
import HostPatches from "@/components/HostPatchesTable.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostPatches } from "@/composables/host-patches.composable";
import { useHostCollection } from "@/stores/xen-api/host.store";
import HostPatches from '@/components/HostPatchesTable.vue'
import UiCard from '@/components/ui/UiCard.vue'
import UiCardTitle from '@/components/ui/UiCardTitle.vue'
import { useHostPatches } from '@/composables/host-patches.composable'
import { useHostCollection } from '@/stores/xen-api/host.store'
const { records: hosts } = useHostCollection();
const { records: hosts } = useHostCollection()
const { count, patches, areSomeLoaded, areAllLoaded } = useHostPatches(hosts);
const { count, patches, areSomeLoaded, areAllLoaded } = useHostPatches(hosts)
</script>
<style lang="postcss" scoped>

View File

@ -1,126 +1,110 @@
<template>
<UiCard class="linear-chart" :color="hasError ? 'error' : undefined">
<UiCardTitle>{{ $t("network-throughput") }}</UiCardTitle>
<UiCardTitle>{{ $t('network-throughput') }}</UiCardTitle>
<UiCardTitle :level="UiCardTitleLevel.Subtitle">
{{ $t("last-week") }}
{{ $t('last-week') }}
</UiCardTitle>
<NoDataError v-if="hasError" />
<UiCardSpinner v-else-if="isLoading" />
<LinearChart
v-else
:data="data"
:max-value="customMaxValue"
:value-formatter="customValueFormatter"
/>
<LinearChart v-else :data="data" :max-value="customMaxValue" :value-formatter="customValueFormatter" />
</UiCard>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, inject } from "vue";
import { formatSize } from "@/libs/utils";
import type { HostStats } from "@/libs/xapi-stats";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import type { LinearChartData } from "@/types/chart";
import { map } from "lodash-es";
import NoDataError from "@/components/NoDataError.vue";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import { UiCardTitleLevel } from "@/types/enums";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useI18n } from "vue-i18n";
import { computed, defineAsyncComponent, inject } from 'vue'
import { formatSize } from '@/libs/utils'
import type { HostStats } from '@/libs/xapi-stats'
import { IK_HOST_LAST_WEEK_STATS } from '@/types/injection-keys'
import type { LinearChartData } from '@/types/chart'
import { map } from 'lodash-es'
import NoDataError from '@/components/NoDataError.vue'
import { RRD_STEP_FROM_STRING } from '@/libs/xapi-stats'
import UiCard from '@/components/ui/UiCard.vue'
import UiCardTitle from '@/components/ui/UiCardTitle.vue'
import UiCardSpinner from '@/components/ui/UiCardSpinner.vue'
import { UiCardTitleLevel } from '@/types/enums'
import { useHostCollection } from '@/stores/xen-api/host.store'
import { useI18n } from 'vue-i18n'
const { t } = useI18n();
const { t } = useI18n()
const LinearChart = defineAsyncComponent(
() => import("@/components/charts/LinearChart.vue")
);
const LinearChart = defineAsyncComponent(() => import('@/components/charts/LinearChart.vue'))
const hostLastWeekStats = inject(IK_HOST_LAST_WEEK_STATS);
const { hasError, isFetching } = useHostCollection();
const hostLastWeekStats = inject(IK_HOST_LAST_WEEK_STATS)
const { hasError, isFetching } = useHostCollection()
const data = computed<LinearChartData>(() => {
const stats = hostLastWeekStats?.stats?.value;
const timestampStart = hostLastWeekStats?.timestampStart?.value;
const stats = hostLastWeekStats?.stats?.value
const timestampStart = hostLastWeekStats?.timestampStart?.value
if (timestampStart === undefined || stats == null) {
return [];
return []
}
const results = {
tx: new Map<number, { timestamp: number; value: number }>(),
rx: new Map<number, { timestamp: number; value: number }>(),
};
}
const addResult = (stats: HostStats, type: "tx" | "rx") => {
const networkStats = Object.values(stats.pifs[type]);
const addResult = (stats: HostStats, type: 'tx' | 'rx') => {
const networkStats = Object.values(stats.pifs[type])
for (let hourIndex = 0; hourIndex < networkStats[0].length; hourIndex++) {
const timestamp =
(timestampStart + hourIndex * RRD_STEP_FROM_STRING.hours) * 1000;
const timestamp = (timestampStart + hourIndex * RRD_STEP_FROM_STRING.hours) * 1000
const networkThroughput = networkStats.reduce(
(total, throughput) => total + throughput[hourIndex],
0
);
const networkThroughput = networkStats.reduce((total, throughput) => total + throughput[hourIndex], 0)
results[type].set(timestamp, {
timestamp,
value: (results[type].get(timestamp)?.value ?? 0) + networkThroughput,
});
})
}
};
}
stats.forEach((host) => {
stats.forEach(host => {
if (!host.stats) {
return;
return
}
addResult(host.stats, "rx");
addResult(host.stats, "tx");
});
addResult(host.stats, 'rx')
addResult(host.stats, 'tx')
})
return [
{
label: t("network-upload"),
data: Array.from(results["tx"].values()),
label: t('network-upload'),
data: Array.from(results.tx.values()),
},
{
label: t("network-download"),
data: Array.from(results["rx"].values()),
label: t('network-download'),
data: Array.from(results.rx.values()),
},
];
});
]
})
const isStatFetched = computed(() => {
const stats = hostLastWeekStats?.stats?.value;
const stats = hostLastWeekStats?.stats?.value
if (stats === undefined) {
return false;
return false
}
return stats.every((host) => {
const hostStats = host.stats;
return stats.every(host => {
const hostStats = host.stats
return (
hostStats != null &&
Object.values(hostStats.pifs["rx"])[0].length +
Object.values(hostStats.pifs["tx"])[0].length ===
Object.values(hostStats.pifs.rx)[0].length + Object.values(hostStats.pifs.tx)[0].length ===
data.value[0].data.length + data.value[1].data.length
);
});
});
)
})
})
const isLoading = computed(() => isFetching.value || !isStatFetched.value);
const isLoading = computed(() => isFetching.value || !isStatFetched.value)
// TODO: improve the way to get the max value of graph
// See: https://github.com/vatesfr/xen-orchestra/pull/6610/files#r1072237279
const customMaxValue = computed(
() =>
Math.max(
...map(data.value[0].data, "value"),
...map(data.value[1].data, "value")
) * 1.5
);
() => Math.max(...map(data.value[0].data, 'value'), ...map(data.value[1].data, 'value')) * 1.5
)
const customValueFormatter = (value: number) => String(formatSize(value));
const customValueFormatter = (value: number) => String(formatSize(value))
</script>

View File

@ -1,7 +1,7 @@
<template>
<UiCard :color="hasError ? 'error' : undefined">
<UiCardTitle>
{{ $t("ram-usage") }}
{{ $t('ram-usage') }}
<template v-if="vmStatsCanBeExpired || hostStatsCanBeExpired" #right>
<UiSpinner v-tooltip="$t('fetching-fresh-data')" />
</template>
@ -12,39 +12,35 @@
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { vTooltip } from "@/directives/tooltip.directive";
import HostsRamUsage from "@/components/pool/dashboard/ramUsage/HostsRamUsage.vue";
import VmsRamUsage from "@/components/pool/dashboard/ramUsage/VmsRamUsage.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { computed, inject } from "vue";
import type { ComputedRef } from "vue";
import type { HostStats, VmStats } from "@/libs/xapi-stats";
import type { Stat } from "@/composables/fetch-stats.composable";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import { useHostCollection } from '@/stores/xen-api/host.store'
import { useVmCollection } from '@/stores/xen-api/vm.store'
import { vTooltip } from '@/directives/tooltip.directive'
import HostsRamUsage from '@/components/pool/dashboard/ramUsage/HostsRamUsage.vue'
import VmsRamUsage from '@/components/pool/dashboard/ramUsage/VmsRamUsage.vue'
import UiCard from '@/components/ui/UiCard.vue'
import UiCardTitle from '@/components/ui/UiCardTitle.vue'
import { computed, inject } from 'vue'
import type { ComputedRef } from 'vue'
import type { HostStats, VmStats } from '@/libs/xapi-stats'
import type { Stat } from '@/composables/fetch-stats.composable'
import UiSpinner from '@/components/ui/UiSpinner.vue'
const { hasError: hasVmError } = useVmCollection();
const { hasError: hasHostError } = useHostCollection();
const { hasError: hasVmError } = useVmCollection()
const { hasError: hasHostError } = useHostCollection()
const vmStats = inject<ComputedRef<Stat<VmStats>[]>>(
"vmStats",
'vmStats',
computed(() => [])
);
)
const hostStats = inject<ComputedRef<Stat<HostStats>[]>>(
"hostStats",
'hostStats',
computed(() => [])
);
)
const vmStatsCanBeExpired = computed(() =>
vmStats.value.some((stat) => stat.canBeExpired)
);
const vmStatsCanBeExpired = computed(() => vmStats.value.some(stat => stat.canBeExpired))
const hostStatsCanBeExpired = computed(() =>
hostStats.value.some((stat) => stat.canBeExpired)
);
const hostStatsCanBeExpired = computed(() => hostStats.value.some(stat => stat.canBeExpired))
const hasError = computed(() => hasVmError.value || hasHostError.value);
const hasError = computed(() => hasVmError.value || hasHostError.value)
</script>

View File

@ -1,60 +1,40 @@
<template>
<UiCard :color="hasError ? 'error' : undefined">
<UiCardTitle>{{ $t("status") }}</UiCardTitle>
<UiCardTitle>{{ $t('status') }}</UiCardTitle>
<NoDataError v-if="hasError" />
<UiCardSpinner v-else-if="!isReady" />
<template v-else>
<PoolDashboardStatusItem
:active="activeHostsCount"
:label="$t('hosts')"
:total="totalHostsCount"
/>
<PoolDashboardStatusItem :active="activeHostsCount" :label="$t('hosts')" :total="totalHostsCount" />
<UiSeparator />
<PoolDashboardStatusItem
:active="activeVmsCount"
:label="$t('vms')"
:total="totalVmsCount"
/>
<PoolDashboardStatusItem :active="activeVmsCount" :label="$t('vms')" :total="totalVmsCount" />
</template>
</UiCard>
</template>
<script lang="ts" setup>
import NoDataError from "@/components/NoDataError.vue";
import PoolDashboardStatusItem from "@/components/pool/dashboard/PoolDashboardStatusItem.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiSeparator from "@/components/ui/UiSeparator.vue";
import { useHostMetricsCollection } from "@/stores/xen-api/host-metrics.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { computed } from "vue";
import NoDataError from '@/components/NoDataError.vue'
import PoolDashboardStatusItem from '@/components/pool/dashboard/PoolDashboardStatusItem.vue'
import UiCard from '@/components/ui/UiCard.vue'
import UiCardSpinner from '@/components/ui/UiCardSpinner.vue'
import UiCardTitle from '@/components/ui/UiCardTitle.vue'
import UiSeparator from '@/components/ui/UiSeparator.vue'
import { useHostMetricsCollection } from '@/stores/xen-api/host-metrics.store'
import { useVmCollection } from '@/stores/xen-api/vm.store'
import { computed } from 'vue'
const {
isReady: isVmReady,
records: vms,
hasError: hasVmError,
} = useVmCollection();
const { isReady: isVmReady, records: vms, hasError: hasVmError } = useVmCollection()
const {
isReady: isHostMetricsReady,
records: hostMetrics,
hasError: hasHostMetricsError,
} = useHostMetricsCollection();
const { isReady: isHostMetricsReady, records: hostMetrics, hasError: hasHostMetricsError } = useHostMetricsCollection()
const hasError = computed(() => hasVmError.value || hasHostMetricsError.value);
const hasError = computed(() => hasVmError.value || hasHostMetricsError.value)
const isReady = computed(() => isVmReady.value && isHostMetricsReady.value);
const isReady = computed(() => isVmReady.value && isHostMetricsReady.value)
const totalHostsCount = computed(() => hostMetrics.value.length);
const totalHostsCount = computed(() => hostMetrics.value.length)
const activeHostsCount = computed(
() => hostMetrics.value.filter((hostMetrics) => hostMetrics.live).length
);
const activeHostsCount = computed(() => hostMetrics.value.filter(hostMetrics => hostMetrics.live).length)
const totalVmsCount = computed(() => vms.value.length);
const totalVmsCount = computed(() => vms.value.length)
const activeVmsCount = computed(
() => vms.value.filter((vm) => vm.power_state === "Running").length
);
const activeVmsCount = computed(() => vms.value.filter(vm => vm.power_state === 'Running').length)
</script>

View File

@ -21,17 +21,17 @@
</template>
<script lang="ts" setup>
import { computed } from "vue";
import ProgressCircle from "@/components/ProgressCircle.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import { computed } from 'vue'
import ProgressCircle from '@/components/ProgressCircle.vue'
import UiTitle from '@/components/ui/UiTitle.vue'
const props = defineProps<{
label: string;
active: number;
total: number;
}>();
label: string
active: number
total: number
}>()
const inactive = computed(() => props.total - props.active);
const inactive = computed(() => props.total - props.active)
</script>
<style lang="postcss" scoped>

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