feat(lite): upgrade deps + root eslint config (#7292)
This commit is contained in:
parent
b0c37df8d7
commit
ea19b0851f
35
.eslintrc.js
35
.eslintrc.js
@ -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
1
.gitignore
vendored
@ -30,6 +30,7 @@ pnpm-debug.log.*
|
||||
yarn-error.log
|
||||
yarn-error.log.*
|
||||
.env
|
||||
*.tsbuildinfo
|
||||
|
||||
# code coverage
|
||||
.nyc_output/
|
||||
|
@ -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") },
|
||||
],
|
||||
},
|
||||
};
|
@ -1,4 +0,0 @@
|
||||
// Keeping this file to prevent applying the global monorepo config for now
|
||||
module.exports = {
|
||||
trailingComma: "es5",
|
||||
};
|
@ -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
|
||||
|
@ -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>
|
||||
```
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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))
|
||||
),
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
4
@xen-orchestra/lite/env.d.ts
vendored
4
@xen-orchestra/lite/env.d.ts
vendored
@ -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
|
||||
|
@ -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"
|
||||
|
@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"postcss-nested": {},
|
||||
"postcss-custom-media": {},
|
||||
},
|
||||
};
|
6
@xen-orchestra/lite/postcss.config.mjs
Normal file
6
@xen-orchestra/lite/postcss.config.mjs
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'postcss-nested': {},
|
||||
'postcss-custom-media': {},
|
||||
},
|
||||
}
|
281
@xen-orchestra/lite/scripts/release.mjs
Normal file → Executable file
281
@xen-orchestra/lite/scripts/release.mjs
Normal file → Executable 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
|
||||
`);
|
||||
`)
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
```
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user