Compare commits
100 Commits
api-utils
...
fix-emptyB
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
838576c8be | ||
|
|
e8bc723f98 | ||
|
|
8e65ef7dbc | ||
|
|
0c0251082d | ||
|
|
c250cd9b89 | ||
|
|
d6abdb246b | ||
|
|
5769da3ebc | ||
|
|
4f383635ef | ||
|
|
8a7abc2e54 | ||
|
|
af1650bd14 | ||
|
|
c6fdef33c4 | ||
|
|
5f73f09f59 | ||
|
|
1b0fc62e2e | ||
|
|
aa2dc9206d | ||
|
|
2b1562da81 | ||
|
|
25e270edb4 | ||
|
|
51c11c15a8 | ||
|
|
47922dee56 | ||
|
|
3dda4dbaad | ||
|
|
901f7b3fe2 | ||
|
|
774d66512e | ||
|
|
ec1669a32e | ||
|
|
bbcd4184b0 | ||
|
|
85ec26194b | ||
|
|
f0242380ca | ||
|
|
a624330818 | ||
|
|
3892efcca2 | ||
|
|
c1c122d92c | ||
|
|
b7a66e9f73 | ||
|
|
d92d2efc78 | ||
|
|
c2cb51a470 | ||
|
|
5242affdc1 | ||
|
|
71f3be288b | ||
|
|
58769815b0 | ||
|
|
c81c23c0d0 | ||
|
|
f06f89b5b4 | ||
|
|
fa748ed9de | ||
|
|
cd753acff7 | ||
|
|
8ff861e2be | ||
|
|
95ccb2e0ae | ||
|
|
b0e5846ad1 | ||
|
|
19fd456ccf | ||
|
|
7946a7db68 | ||
|
|
6127e30574 | ||
|
|
4aad9d8e32 | ||
|
|
78d15ddf96 | ||
|
|
302f7fb85e | ||
|
|
ea19b0851f | ||
|
|
b0c37df8d7 | ||
|
|
beba6f7e8d | ||
|
|
9388b5500c | ||
|
|
bae8ad25e9 | ||
|
|
c96b29fe96 | ||
|
|
9888013aff | ||
|
|
0bbb0c289d | ||
|
|
80097ea777 | ||
|
|
be452a5d63 | ||
|
|
bcc0452646 | ||
|
|
9d9691c5a3 | ||
|
|
e56edc70d5 | ||
|
|
d7f4d0f5e0 | ||
|
|
8c24dd1732 | ||
|
|
575a423edf | ||
|
|
e311860bb5 | ||
|
|
e6289ebc16 | ||
|
|
013e20aa0f | ||
|
|
45a0a83fa4 | ||
|
|
ae518399fa | ||
|
|
d949112921 | ||
|
|
bb19afc45c | ||
|
|
7780cb176a | ||
|
|
74ff64dfb4 | ||
|
|
9be3c40ead | ||
|
|
0f00c7e393 | ||
|
|
95492f6f89 | ||
|
|
046fa7282b | ||
|
|
6cd99c39f4 | ||
|
|
48c3a65cc6 | ||
|
|
8b0b2d7c31 | ||
|
|
d8280087a4 | ||
|
|
c14261a0bc | ||
|
|
3d6defca37 | ||
|
|
d062a5175a | ||
|
|
f218874c4b | ||
|
|
b1e879ca2f | ||
|
|
c5010c2caa | ||
|
|
2c40b99d8b | ||
|
|
0d127f2b92 | ||
|
|
0464886e80 | ||
|
|
d655a3e222 | ||
|
|
579f0b91d5 | ||
|
|
72b1878254 | ||
|
|
74dd4c8db7 | ||
|
|
ef4ecce572 | ||
|
|
1becccffbc | ||
|
|
b95b1622b1 | ||
|
|
36d6e3779d | ||
|
|
b0e000328d | ||
|
|
cc080ec681 | ||
|
|
0d4cf48410 |
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/{web-core,lite,web}/**/*.{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: {
|
||||
|
||||
48
.github/ISSUE_TEMPLATE/bug_report.md
vendored
48
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,48 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'status: triaging :triangular_flag_on_post:, type: bug :bug:'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
1. ⚠️ **If you don't follow this template, the issue will be closed**.
|
||||
2. ⚠️ **If your issue can't be easily reproduced, please report it [on the forum first](https://xcp-ng.org/forum/category/12/xen-orchestra)**.
|
||||
|
||||
Are you using XOA or XO from the sources?
|
||||
|
||||
If XOA:
|
||||
|
||||
- which release channel? (`stable` vs `latest`)
|
||||
- please consider creating a support ticket in [your dedicated support area](https://xen-orchestra.com/#!/member/support)
|
||||
|
||||
If XO from the sources:
|
||||
|
||||
- Provide **your commit number**. If it's older than a week, we won't investigate
|
||||
- Don't forget to [read this first](https://xen-orchestra.com/docs/community.html)
|
||||
- As well as follow [this guide](https://xen-orchestra.com/docs/community.html#report-a-bug)
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment (please provide the following information):**
|
||||
|
||||
- Node: [e.g. 16.12.1]
|
||||
- hypervisor: [e.g. XCP-ng 8.2.0]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
119
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
119
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
name: Bug Report
|
||||
description: Create a report to help us improve
|
||||
labels: ['type: bug :bug:', 'status: triaging :triangular_flag_on_post:']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
1. ⚠️ **If you don't follow this template, the issue will be closed**.
|
||||
2. ⚠️ **If your issue can't be easily reproduced, please report it [on the forum first](https://xcp-ng.org/forum/category/12/xen-orchestra)**.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '## Are you using XOA or XO from the sources?'
|
||||
- type: dropdown
|
||||
id: xo-origin
|
||||
attributes:
|
||||
label: Are you using XOA or XO from the sources?
|
||||
options:
|
||||
- XOA
|
||||
- XO from the sources
|
||||
- both
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '### If XOA:'
|
||||
- type: dropdown
|
||||
id: xoa-channel
|
||||
attributes:
|
||||
label: Which release channel?
|
||||
description: please consider creating a support ticket in [your dedicated support area](https://xen-orchestra.com/#!/member/support)
|
||||
options:
|
||||
- stable
|
||||
- latest
|
||||
- both
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '### If XO from the sources:'
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
- Don't forget to [read this first](https://xen-orchestra.com/docs/community.html)
|
||||
- As well as follow [this guide](https://xen-orchestra.com/docs/community.html#report-a-bug)
|
||||
- type: input
|
||||
id: xo-sources-commit-number
|
||||
attributes:
|
||||
label: Provide your commit number
|
||||
description: If it's older than a week, we won't investigate
|
||||
placeholder: e.g. 579f0
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '## Bug description:'
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: error-message
|
||||
attributes:
|
||||
label: Error message
|
||||
render: Markdown
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: To reproduce
|
||||
description: 'Steps to reproduce the behavior:'
|
||||
value: |
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. Scroll down to '...'
|
||||
4. See error
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what you expected to happen
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: '## Environment (please provide the following information):'
|
||||
- type: input
|
||||
id: node-version
|
||||
attributes:
|
||||
label: Node
|
||||
placeholder: e.g. 16.12.1
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: hypervisor-version
|
||||
attributes:
|
||||
label: Hypervisor
|
||||
placeholder: e.g. XCP-ng 8.2.0
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context about the problem here
|
||||
validations:
|
||||
required: false
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -30,8 +30,12 @@ pnpm-debug.log.*
|
||||
yarn-error.log
|
||||
yarn-error.log.*
|
||||
.env
|
||||
*.tsbuildinfo
|
||||
|
||||
# code coverage
|
||||
.nyc_output/
|
||||
coverage/
|
||||
.turbo/
|
||||
|
||||
# https://node-tap.org/dot-tap-folder/
|
||||
.tap/
|
||||
|
||||
@@ -62,6 +62,42 @@ decorateClass(Foo, {
|
||||
})
|
||||
```
|
||||
|
||||
### `decorateObject(object, map)`
|
||||
|
||||
Decorates an object the same way `decorateClass()` decorates a class:
|
||||
|
||||
```js
|
||||
import { decorateObject } from '@vates/decorate-with'
|
||||
|
||||
const object = {
|
||||
get bar() {
|
||||
// body
|
||||
},
|
||||
|
||||
set bar(value) {
|
||||
// body
|
||||
},
|
||||
|
||||
baz() {
|
||||
// body
|
||||
},
|
||||
}
|
||||
|
||||
decorateObject(object, {
|
||||
// getter and/or setter
|
||||
bar: {
|
||||
// without arguments
|
||||
get: lodash.memoize,
|
||||
|
||||
// with arguments
|
||||
set: [lodash.debounce, 150],
|
||||
},
|
||||
|
||||
// method (with or without arguments)
|
||||
baz: lodash.curry,
|
||||
})
|
||||
```
|
||||
|
||||
### `perInstance(fn, ...args)`
|
||||
|
||||
Helper to decorate the method by instance instead of for the whole class.
|
||||
|
||||
@@ -80,6 +80,42 @@ decorateClass(Foo, {
|
||||
})
|
||||
```
|
||||
|
||||
### `decorateObject(object, map)`
|
||||
|
||||
Decorates an object the same way `decorateClass()` decorates a class:
|
||||
|
||||
```js
|
||||
import { decorateObject } from '@vates/decorate-with'
|
||||
|
||||
const object = {
|
||||
get bar() {
|
||||
// body
|
||||
},
|
||||
|
||||
set bar(value) {
|
||||
// body
|
||||
},
|
||||
|
||||
baz() {
|
||||
// body
|
||||
},
|
||||
}
|
||||
|
||||
decorateObject(object, {
|
||||
// getter and/or setter
|
||||
bar: {
|
||||
// without arguments
|
||||
get: lodash.memoize,
|
||||
|
||||
// with arguments
|
||||
set: [lodash.debounce, 150],
|
||||
},
|
||||
|
||||
// method (with or without arguments)
|
||||
baz: lodash.curry,
|
||||
})
|
||||
```
|
||||
|
||||
### `perInstance(fn, ...args)`
|
||||
|
||||
Helper to decorate the method by instance instead of for the whole class.
|
||||
|
||||
@@ -14,10 +14,13 @@ function applyDecorator(decorator, value) {
|
||||
}
|
||||
|
||||
exports.decorateClass = exports.decorateMethodsWith = function decorateClass(klass, map) {
|
||||
const { prototype } = klass
|
||||
return decorateObject(klass.prototype, map)
|
||||
}
|
||||
|
||||
function decorateObject(object, map) {
|
||||
for (const name of Object.keys(map)) {
|
||||
const decorator = map[name]
|
||||
const descriptor = getOwnPropertyDescriptor(prototype, name)
|
||||
const descriptor = getOwnPropertyDescriptor(object, name)
|
||||
if (typeof decorator === 'function' || Array.isArray(decorator)) {
|
||||
descriptor.value = applyDecorator(decorator, descriptor.value)
|
||||
} else {
|
||||
@@ -30,10 +33,11 @@ exports.decorateClass = exports.decorateMethodsWith = function decorateClass(kla
|
||||
}
|
||||
}
|
||||
|
||||
defineProperty(prototype, name, descriptor)
|
||||
defineProperty(object, name, descriptor)
|
||||
}
|
||||
return klass
|
||||
return object
|
||||
}
|
||||
exports.decorateObject = decorateObject
|
||||
|
||||
exports.perInstance = function perInstance(fn, decorator, ...args) {
|
||||
const map = new WeakMap()
|
||||
|
||||
28
@vates/fuse-vhd/.USAGE.md
Normal file
28
@vates/fuse-vhd/.USAGE.md
Normal file
@@ -0,0 +1,28 @@
|
||||
Mount a vhd generated by xen-orchestra to filesystem
|
||||
|
||||
### Library
|
||||
|
||||
```js
|
||||
import { mount } from 'fuse-vhd'
|
||||
|
||||
// return a disposable, see promise-toolbox/Disposable
|
||||
// unmount automatically when disposable is disposed
|
||||
// in case of differencing VHD, it mounts the full chain
|
||||
await mount(handler, diskId, mountPoint)
|
||||
```
|
||||
|
||||
### cli
|
||||
|
||||
From the install folder :
|
||||
|
||||
```
|
||||
cli.mjs <remoteUrl> <vhdPathInRemote> <mountPoint>
|
||||
```
|
||||
|
||||
After installing the package
|
||||
|
||||
```
|
||||
xo-fuse-vhd <remoteUrl> <vhdPathInRemote> <mountPoint>
|
||||
```
|
||||
|
||||
remoteUrl can be found by using cli in `@xen-orchestra/fs` , for example a local remote will have a url like `file:///path/to/remote/root`
|
||||
59
@vates/fuse-vhd/README.md
Normal file
59
@vates/fuse-vhd/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/fuse-vhd
|
||||
|
||||
[](https://npmjs.org/package/@vates/fuse-vhd)  [](https://bundlephobia.com/result?p=@vates/fuse-vhd) [](https://npmjs.org/package/@vates/fuse-vhd)
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/fuse-vhd):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/fuse-vhd
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Mount a vhd generated by xen-orchestra to filesystem
|
||||
|
||||
### Library
|
||||
|
||||
```js
|
||||
import { mount } from 'fuse-vhd'
|
||||
|
||||
// return a disposable, see promise-toolbox/Disposable
|
||||
// unmount automatically when disposable is disposed
|
||||
// in case of differencing VHD, it mounts the full chain
|
||||
await mount(handler, diskId, mountPoint)
|
||||
```
|
||||
|
||||
### cli
|
||||
|
||||
From the install folder :
|
||||
|
||||
```
|
||||
cli.mjs <remoteUrl> <vhdPathInRemote> <mountPoint>
|
||||
```
|
||||
|
||||
After installing the package
|
||||
|
||||
```
|
||||
xo-fuse-vhd <remoteUrl> <vhdPathInRemote> <mountPoint>
|
||||
```
|
||||
|
||||
remoteUrl can be found by using cli in `@xen-orchestra/fs` , for example a local remote will have a url like `file:///path/to/remote/root`
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
26
@vates/fuse-vhd/cli.mjs
Executable file
26
@vates/fuse-vhd/cli.mjs
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import Disposable from 'promise-toolbox/Disposable'
|
||||
import { getSyncedHandler } from '@xen-orchestra/fs'
|
||||
|
||||
import { mount } from './index.mjs'
|
||||
|
||||
async function* main([remoteUrl, vhdPathInRemote, mountPoint]) {
|
||||
if (mountPoint === undefined) {
|
||||
throw new TypeError('missing arg: cli <remoteUrl> <vhdPathInRemote> <mountPoint>')
|
||||
}
|
||||
const handler = yield getSyncedHandler({ url: remoteUrl })
|
||||
const mounted = await mount(handler, vhdPathInRemote, mountPoint)
|
||||
|
||||
let disposePromise
|
||||
process.on('SIGINT', async () => {
|
||||
// ensure single dispose
|
||||
if (!disposePromise) {
|
||||
disposePromise = mounted.dispose()
|
||||
}
|
||||
await disposePromise
|
||||
process.exit()
|
||||
})
|
||||
}
|
||||
|
||||
Disposable.wrap(main)(process.argv.slice(2))
|
||||
@@ -58,7 +58,7 @@ export const mount = Disposable.factory(async function* mount(handler, diskPath,
|
||||
},
|
||||
})
|
||||
return new Disposable(
|
||||
() => fromCallback(() => fuse.unmount()),
|
||||
fromCallback(() => fuse.mount())
|
||||
() => fromCallback(cb => fuse.unmount(cb)),
|
||||
fromCallback(cb => fuse.mount(cb))
|
||||
)
|
||||
})
|
||||
|
||||
@@ -19,10 +19,14 @@
|
||||
},
|
||||
"main": "./index.mjs",
|
||||
"dependencies": {
|
||||
"@xen-orchestra/fs": "^4.1.3",
|
||||
"fuse-native": "^2.2.6",
|
||||
"lru-cache": "^7.14.0",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"vhd-lib": "^4.8.0"
|
||||
"vhd-lib": "^4.9.0"
|
||||
},
|
||||
"bin": {
|
||||
"xo-fuse-vhd": "./cli.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
|
||||
@@ -41,9 +41,7 @@ export default class MultiNbdClient {
|
||||
}
|
||||
if (connectedClients.length < this.#clients.length) {
|
||||
warn(
|
||||
`incomplete connection by multi Nbd, only ${connectedClients.length} over ${
|
||||
this.#clients.length
|
||||
} expected clients`
|
||||
`incomplete connection by multi Nbd, only ${connectedClients.length} over ${this.#clients.length} expected clients`
|
||||
)
|
||||
this.#clients = connectedClients
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ export class ImportVmBackup {
|
||||
|
||||
async #decorateIncrementalVmMetadata() {
|
||||
const { additionnalVmTag, mapVdisSrs, useDifferentialRestore } = this._importIncrementalVmSettings
|
||||
|
||||
|
||||
const ignoredVdis = new Set(
|
||||
Object.entries(mapVdisSrs)
|
||||
.filter(([_, srUuid]) => srUuid === null)
|
||||
|
||||
@@ -21,7 +21,7 @@ export class RestoreMetadataBackup {
|
||||
})
|
||||
} else {
|
||||
const metadata = JSON.parse(await handler.readFile(join(backupId, 'metadata.json')))
|
||||
const dataFileName = resolve(backupId, metadata.data ?? 'data.json')
|
||||
const dataFileName = resolve('/', backupId, metadata.data ?? 'data.json').slice(1)
|
||||
const data = await handler.readFile(dataFileName)
|
||||
|
||||
// if data is JSON, sent it as a plain string, otherwise, consider the data as binary and encode it
|
||||
|
||||
@@ -86,7 +86,12 @@ export const VmsRemote = class RemoteVmsBackupRunner extends Abstract {
|
||||
throw new Error(`Job mode ${job.mode} not implemented for mirror backup`)
|
||||
}
|
||||
|
||||
return runTask(taskStart, () => vmBackup.run())
|
||||
return sourceRemoteAdapter
|
||||
.listVmBackups(vmUuid, ({ mode }) => mode === job.mode)
|
||||
.then(vmBackups => {
|
||||
// avoiding to create tasks for empty directories
|
||||
if (vmBackups.length > 0) return runTask(taskStart, () => vmBackup.run())
|
||||
})
|
||||
}
|
||||
const { concurrency } = settings
|
||||
await asyncMapSettled(vmsUuids, !concurrency ? handleVm : limitConcurrency(concurrency)(handleVm))
|
||||
|
||||
@@ -205,7 +205,7 @@ export class IncrementalRemoteWriter extends MixinRemoteWriter(AbstractIncrement
|
||||
// TODO remove when this has been done before the export
|
||||
await checkVhd(handler, parentPath)
|
||||
}
|
||||
|
||||
|
||||
// don't write it as transferSize += await async function
|
||||
// since i += await asyncFun lead to race condition
|
||||
// as explained : https://eslint.org/docs/latest/rules/require-atomic-updates
|
||||
|
||||
@@ -58,7 +58,7 @@ export const MixinXapiWriter = (BaseClass = Object) =>
|
||||
)
|
||||
}
|
||||
const healthCheckVm = xapi.getObject(healthCheckVmRef) ?? (await xapi.waitObject(healthCheckVmRef))
|
||||
await healthCheckVm.add_tag('xo:no-bak=Health Check')
|
||||
await healthCheckVm.add_tags('xo:no-bak=Health Check')
|
||||
await new HealthCheckVmBackup({
|
||||
restoredVm: healthCheckVm,
|
||||
xapi,
|
||||
|
||||
@@ -2,8 +2,20 @@ import mapValues from 'lodash/mapValues.js'
|
||||
import { dirname } from 'node:path'
|
||||
|
||||
function formatVmBackup(backup) {
|
||||
const { isVhdDifferencing } = backup
|
||||
const { isVhdDifferencing, vmSnapshot } = backup
|
||||
|
||||
let differencingVhds
|
||||
let dynamicVhds
|
||||
const withMemory = vmSnapshot.suspend_VDI !== 'OpaqueRef:NULL'
|
||||
// isVhdDifferencing is either undefined or an object
|
||||
if (isVhdDifferencing !== undefined) {
|
||||
differencingVhds = Object.values(isVhdDifferencing).filter(t => t).length
|
||||
dynamicVhds = Object.values(isVhdDifferencing).filter(t => !t).length
|
||||
if (withMemory) {
|
||||
// the suspend VDI (memory) is always a dynamic
|
||||
dynamicVhds -= 1
|
||||
}
|
||||
}
|
||||
return {
|
||||
disks:
|
||||
backup.vhds === undefined
|
||||
@@ -28,9 +40,9 @@ function formatVmBackup(backup) {
|
||||
name_label: backup.vm.name_label,
|
||||
},
|
||||
|
||||
// isVhdDifferencing is either undefined or an object
|
||||
differencingVhds: isVhdDifferencing && Object.values(isVhdDifferencing).filter(t => t).length,
|
||||
dynamicVhds: isVhdDifferencing && Object.values(isVhdDifferencing).filter(t => !t).length,
|
||||
differencingVhds,
|
||||
dynamicVhds,
|
||||
withMemory,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"tar": "^6.1.15",
|
||||
"uuid": "^9.0.0",
|
||||
"vhd-lib": "^4.8.0",
|
||||
"vhd-lib": "^4.9.0",
|
||||
"xen-api": "^2.0.0",
|
||||
"yazl": "^2.5.1"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
@@ -2,12 +2,19 @@
|
||||
|
||||
## **next**
|
||||
|
||||
- Fix Typescript typings errors when running `yarn type-check` command (PR [#7278](https://github.com/vatesfr/xen-orchestra/pull/7278))
|
||||
- Introduce PWA Json Manifest (PR [#7291](https://github.com/vatesfr/xen-orchestra/pull/7291))
|
||||
|
||||
## **0.1.7** (2023-12-28)
|
||||
|
||||
- [VM/Action] Ability to migrate a VM from its view (PR [#7164](https://github.com/vatesfr/xen-orchestra/pull/7164))
|
||||
- Ability to override host address with `master` URL query param (PR [#7187](https://github.com/vatesfr/xen-orchestra/pull/7187))
|
||||
- Added tooltip on CPU provisioning warning icon (PR [#7223](https://github.com/vatesfr/xen-orchestra/pull/7223))
|
||||
- Add indeterminate state on FormToggle component (PR [#7230](https://github.com/vatesfr/xen-orchestra/pull/7230))
|
||||
- Add new UiStatusPanel component (PR [#7227](https://github.com/vatesfr/xen-orchestra/pull/7227))
|
||||
- XOA quick deploy (PR [#7245](https://github.com/vatesfr/xen-orchestra/pull/7245))
|
||||
- Fix infinite loader when no stats on pool dashboard (PR [#7236](https://github.com/vatesfr/xen-orchestra/pull/7236))
|
||||
- [Tree view] Display VMs count (PR [#7185](https://github.com/vatesfr/xen-orchestra/pull/7185))
|
||||
|
||||
## **0.1.6** (2023-11-30)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>XO Lite</title>
|
||||
</head>
|
||||
|
||||
@@ -1,43 +1,42 @@
|
||||
{
|
||||
"name": "@xen-orchestra/lite",
|
||||
"version": "0.1.6",
|
||||
"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": {
|
||||
"@csstools/postcss-global-data": "^2.1.1",
|
||||
"@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",
|
||||
"@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.7",
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"@vueuse/core": "^10.7.1",
|
||||
"@vueuse/math": "^10.7.1",
|
||||
"@vueuse/shared": "^10.7.1",
|
||||
"@xen-orchestra/web-core": "*",
|
||||
"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,19 +47,21 @@
|
||||
"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-color-function": "^4.1.0",
|
||||
"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.13",
|
||||
"vue-echarts": "^6.6.8",
|
||||
"vue-i18n": "^9.9.0",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue-tsc": "^1.8.22",
|
||||
"vue-tsc": "^1.8.27",
|
||||
"zx": "^7.2.3"
|
||||
},
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"postcss-nested": {},
|
||||
"postcss-custom-media": {},
|
||||
},
|
||||
};
|
||||
10
@xen-orchestra/lite/postcss.config.mjs
Normal file
10
@xen-orchestra/lite/postcss.config.mjs
Normal file
@@ -0,0 +1,10 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@csstools/postcss-global-data': {
|
||||
files: ['../web-core/lib/assets/css/.globals.pcss'],
|
||||
},
|
||||
'postcss-nested': {},
|
||||
'postcss-custom-media': {},
|
||||
'postcss-color-function': {},
|
||||
},
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
14
@xen-orchestra/lite/public/manifest.webmanifest
Normal file
14
@xen-orchestra/lite/public/manifest.webmanifest
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "XO Lite",
|
||||
"short_name": "XOLite",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"description": "Integrated local and lightweight solution to manage a local XCP-ng pool.",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml"
|
||||
}
|
||||
]
|
||||
}
|
||||
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,77 +16,56 @@
|
||||
</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 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;
|
||||
if (link == null) {
|
||||
link = document.createElement("link");
|
||||
link.rel = "icon";
|
||||
document.getElementsByTagName("head")[0].appendChild(link);
|
||||
}
|
||||
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";
|
||||
</style>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
.main {
|
||||
overflow: auto;
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
@custom-media --mobile (max-width: 1023px);
|
||||
@custom-media --desktop (min-width: 1024px);
|
||||
@@ -1,100 +0,0 @@
|
||||
@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;
|
||||
font-size: 1.3rem;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: var(--color-blue-scale-100);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-extra-blue-base);
|
||||
}
|
||||
|
||||
code,
|
||||
code * {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
}
|
||||
|
||||
.card-view {
|
||||
padding: 1.2rem;
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: underline;
|
||||
color: var(--color-extra-blue-base);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: var(--color-extra-blue-d20);
|
||||
}
|
||||
|
||||
.link:active,
|
||||
.link.router-link-active {
|
||||
color: var(--color-extra-blue-d40);
|
||||
}
|
||||
|
||||
.link.router-link-active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.context-color-success {
|
||||
color: var(--color-green-infra-base);
|
||||
}
|
||||
|
||||
.context-color-error {
|
||||
color: var(--color-red-vates-base);
|
||||
}
|
||||
|
||||
.context-color-warning {
|
||||
color: var(--color-orange-world-base);
|
||||
}
|
||||
|
||||
.context-color-info {
|
||||
color: var(--color-extra-blue-base);
|
||||
}
|
||||
|
||||
.context-background-color-success {
|
||||
background-color: var(--background-color-green-infra);
|
||||
}
|
||||
|
||||
.context-background-color-error {
|
||||
background-color: var(--background-color-red-vates);
|
||||
}
|
||||
|
||||
.context-background-color-warning {
|
||||
background-color: var(--background-color-orange-world);
|
||||
}
|
||||
|
||||
.context-background-color-info {
|
||||
background-color: var(--background-color-extra-blue);
|
||||
}
|
||||
|
||||
.context-border-color-success {
|
||||
border-color: var(--color-green-infra-base);
|
||||
}
|
||||
|
||||
.context-border-color-error {
|
||||
border-color: var(--color-red-vates-base);
|
||||
}
|
||||
|
||||
.context-border-color-warning {
|
||||
border-color: var(--color-orange-world-base);
|
||||
}
|
||||
|
||||
.context-border-color-info {
|
||||
border-color: var(--color-extra-blue-base);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
:root {
|
||||
--color-logo: #282467;
|
||||
|
||||
--color-blue-scale-000: #000000;
|
||||
--color-blue-scale-100: #1a1b38;
|
||||
--color-blue-scale-200: #595a6f;
|
||||
--color-blue-scale-300: #9899a5;
|
||||
--color-blue-scale-400: #e5e5e7;
|
||||
--color-blue-scale-500: #ffffff;
|
||||
|
||||
--color-extra-blue-l60: #d1cefb;
|
||||
--color-extra-blue-l40: #bbb5f9;
|
||||
--color-extra-blue-l20: #a39df8;
|
||||
--color-extra-blue-base: #8f84ff;
|
||||
--color-extra-blue-d20: #716ac6;
|
||||
--color-extra-blue-d40: #554f94;
|
||||
--color-extra-blue-d60: #383563;
|
||||
|
||||
--color-green-infra-l60: #b5dbca;
|
||||
--color-green-infra-l40: #91c9b0;
|
||||
--color-green-infra-l20: #70b795;
|
||||
--color-green-infra-base: #55a57b;
|
||||
--color-green-infra-d20: #438463;
|
||||
--color-green-infra-d40: #32634a;
|
||||
--color-green-infra-d60: #214231;
|
||||
|
||||
--color-orange-world-l60: #f2cda8;
|
||||
--color-orange-world-l40: #ebb57d;
|
||||
--color-orange-world-l20: #e59d56;
|
||||
--color-orange-world-base: #ef7f18;
|
||||
--color-orange-world-d20: #bf6612;
|
||||
--color-orange-world-d40: #864f1f;
|
||||
--color-orange-world-d60: #5a3514;
|
||||
|
||||
--color-red-vates-l60: #dda5a7;
|
||||
--color-red-vates-l40: #ce787c;
|
||||
--color-red-vates-l20: #bf4f51;
|
||||
--color-red-vates-base: #be1621;
|
||||
--color-red-vates-d20: #8e2221;
|
||||
--color-red-vates-d40: #6a1919;
|
||||
--color-red-vates-d60: #471010;
|
||||
|
||||
--color-grayscale-200: #585757;
|
||||
|
||||
--background-color-primary: #ffffff;
|
||||
--background-color-secondary: #f6f6f7;
|
||||
--background-color-extra-blue: #f4f3fe;
|
||||
--background-color-green-infra: #ecf5f2;
|
||||
--background-color-orange-world: #fbf2e9;
|
||||
--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),
|
||||
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),
|
||||
0 2.4rem 3.8rem rgba(20, 20, 30, 0.04);
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
color-scheme: dark;
|
||||
|
||||
--color-logo: #e5e5e7;
|
||||
|
||||
--color-blue-scale-000: #ffffff;
|
||||
--color-blue-scale-100: #e5e5e7;
|
||||
--color-blue-scale-200: #9899a5;
|
||||
--color-blue-scale-300: #595a6f;
|
||||
--color-blue-scale-400: #1a1b38;
|
||||
--color-blue-scale-500: #000000;
|
||||
|
||||
--background-color-primary: #14141d;
|
||||
--background-color-secondary: #17182a;
|
||||
--background-color-extra-blue: #35335d;
|
||||
--background-color-green-infra: #243b3d;
|
||||
--background-color-orange-world: #493328;
|
||||
--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),
|
||||
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),
|
||||
0 2.4rem 3.8rem rgba(20, 20, 30, 0.08);
|
||||
}
|
||||
1
@xen-orchestra/lite/src/assets/xo.svg
Normal file
1
@xen-orchestra/lite/src/assets/xo.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 43 KiB |
@@ -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>
|
||||
@@ -61,14 +51,14 @@ const openSettings = () => router.push({ name: "settings" });
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
color: var(--color-blue-scale-100);
|
||||
color: var(--color-grey-100);
|
||||
border: none;
|
||||
border-radius: 0.8rem;
|
||||
background-color: var(--background-color-secondary);
|
||||
gap: 0.8rem;
|
||||
|
||||
&:disabled {
|
||||
color: var(--color-blue-scale-400);
|
||||
color: var(--color-grey-500);
|
||||
}
|
||||
|
||||
&:not(:disabled) {
|
||||
@@ -82,7 +72,7 @@ const openSettings = () => router.push({ name: "settings" });
|
||||
|
||||
&:active,
|
||||
&.active {
|
||||
color: var(--color-extra-blue-base);
|
||||
color: var(--color-purple-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,6 +86,6 @@ const openSettings = () => router.push({ name: "settings" });
|
||||
}
|
||||
|
||||
.menu-item-logout {
|
||||
color: var(--color-red-vates-base);
|
||||
color: var(--color-red-base);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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 />
|
||||
@@ -13,26 +8,35 @@
|
||||
<slot />
|
||||
<div class="right">
|
||||
<PoolOverrideWarning as-tooltip />
|
||||
<UiButton v-if="isDesktop" :icon="faDownload" @click="openXoaDeploy">
|
||||
{{ $t('deploy-xoa') }}
|
||||
</UiButton>
|
||||
<AccountButton />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import AccountButton from "@/components/AccountButton.vue";
|
||||
import PoolOverrideWarning from "@/components/PoolOverrideWarning.vue";
|
||||
import TextLogo from "@/components/TextLogo.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import { useNavigationStore } from "@/stores/navigation.store";
|
||||
import { useUiStore } from "@/stores/ui.store";
|
||||
import { faBars } 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 uiStore = useUiStore();
|
||||
const { isMobile } = storeToRefs(uiStore);
|
||||
const router = useRouter()
|
||||
|
||||
const navigationStore = useNavigationStore();
|
||||
const { trigger: navigationTrigger } = storeToRefs(navigationStore);
|
||||
const openXoaDeploy = () => router.push({ name: 'xoa.deploy' })
|
||||
|
||||
const uiStore = useUiStore()
|
||||
const { isMobile, isDesktop } = storeToRefs(uiStore)
|
||||
|
||||
const navigationStore = useNavigationStore()
|
||||
const { trigger: navigationTrigger } = storeToRefs(navigationStore)
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
@@ -42,7 +46,7 @@ const { trigger: navigationTrigger } = storeToRefs(navigationStore);
|
||||
justify-content: space-between;
|
||||
height: 5.5rem;
|
||||
padding: 1rem;
|
||||
border-bottom: 0.1rem solid var(--color-blue-scale-400);
|
||||
border-bottom: 0.1rem solid var(--color-grey-500);
|
||||
background-color: var(--background-color-secondary);
|
||||
|
||||
img {
|
||||
@@ -62,5 +66,6 @@ const { trigger: navigationTrigger } = storeToRefs(navigationStore);
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -136,7 +135,7 @@ form {
|
||||
background-color: var(--background-color-secondary);
|
||||
|
||||
.error {
|
||||
color: var(--color-red-vates-base);
|
||||
color: var(--color-red-base);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +156,7 @@ input {
|
||||
max-width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem 1.5rem;
|
||||
border: 1px solid var(--color-blue-scale-400);
|
||||
border: 1px solid var(--color-grey-500);
|
||||
border-radius: 0.8rem;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -59,7 +60,7 @@ useEventListener(
|
||||
}
|
||||
|
||||
code:not(.hljs-code) {
|
||||
background-color: var(--background-color-extra-blue);
|
||||
background-color: var(--background-color-purple-10);
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
@@ -80,12 +81,12 @@ useEventListener(
|
||||
}
|
||||
|
||||
thead th {
|
||||
border-bottom: 2px solid var(--color-blue-scale-400);
|
||||
border-bottom: 2px solid var(--color-grey-500);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
tbody td {
|
||||
border-bottom: 1px solid var(--color-blue-scale-400);
|
||||
border-bottom: 1px solid var(--color-grey-500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,11 +103,11 @@ useEventListener(
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-extra-blue-base);
|
||||
color: var(--color-purple-base);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--color-extra-blue-d20);
|
||||
color: var(--color-purple-d20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -50,7 +45,7 @@ whenever(isOpen, () => {
|
||||
max-width: 37rem;
|
||||
height: calc(100vh - 5.5rem);
|
||||
padding: 0.5rem;
|
||||
border-right: 1px solid var(--color-blue-scale-400);
|
||||
border-right: 1px solid var(--color-grey-500);
|
||||
background-color: var(--background-color-primary);
|
||||
|
||||
&.collapsible {
|
||||
|
||||
@@ -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>
|
||||
@@ -43,9 +41,9 @@ watchEffect(() => {
|
||||
display: inline-flex;
|
||||
padding: 0.3125em 0.5em;
|
||||
pointer-events: none;
|
||||
color: var(--color-blue-scale-500);
|
||||
color: var(--color-grey-600);
|
||||
border-radius: 0.5em;
|
||||
background-color: var(--color-blue-scale-100);
|
||||
background-color: var(--color-grey-100);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@@ -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,9 +142,9 @@ 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);
|
||||
background-color: var(--color-grey-100);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -55,14 +54,14 @@ const isIcon = (maybeIcon: any): maybeIcon is IconDefinition =>
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
padding: 0 0.7rem;
|
||||
border: 1px solid var(--color-blue-scale-400);
|
||||
border: 1px solid var(--color-grey-500);
|
||||
border-radius: 0.8rem;
|
||||
background-color: var(--color-blue-scale-500);
|
||||
background-color: var(--color-grey-600);
|
||||
box-shadow: var(--shadow-100);
|
||||
gap: 0.1rem;
|
||||
|
||||
&:focus-within {
|
||||
outline: 1px solid var(--color-extra-blue-l40);
|
||||
outline: 1px solid var(--color-purple-l40);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +71,7 @@ const isIcon = (maybeIcon: any): maybeIcon is IconDefinition =>
|
||||
}
|
||||
|
||||
.form-widget:hover .widget {
|
||||
border-color: var(--color-extra-blue-l60);
|
||||
border-color: var(--color-purple-l60);
|
||||
}
|
||||
|
||||
.element {
|
||||
@@ -94,8 +93,8 @@ const isIcon = (maybeIcon: any): maybeIcon is IconDefinition =>
|
||||
font-size: inherit;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--color-blue-scale-100);
|
||||
background-color: var(--color-blue-scale-500);
|
||||
color: var(--color-grey-100);
|
||||
background-color: var(--color-grey-600);
|
||||
flex: 1;
|
||||
|
||||
&:disabled {
|
||||
@@ -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;
|
||||
@@ -135,7 +134,7 @@ const isIcon = (maybeIcon: any): maybeIcon is IconDefinition =>
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--color-blue-scale-200);
|
||||
color: var(--color-grey-200);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -25,7 +25,7 @@ defineProps<{
|
||||
font-size: 1.3rem;
|
||||
line-height: 150%;
|
||||
margin: 0.5rem 0;
|
||||
color: var(--color-red-vates-base);
|
||||
color: var(--color-red-base);
|
||||
|
||||
& svg {
|
||||
margin-right: 0.5rem;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -25,6 +25,6 @@
|
||||
font-weight: 500;
|
||||
font-size: 1.25em;
|
||||
line-height: 150%;
|
||||
color: var(--color-red-vates-base);
|
||||
color: var(--color-red-base);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -27,6 +27,6 @@
|
||||
font-weight: 500;
|
||||
font-size: 2rem;
|
||||
line-height: 150%;
|
||||
color: var(--color-extra-blue-base);
|
||||
color: var(--color-purple-base);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,85 +12,80 @@
|
||||
</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>
|
||||
.unknown {
|
||||
color: var(--color-blue-scale-300);
|
||||
color: var(--color-grey-300);
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -39,7 +33,7 @@ const isRecordNotFound = computed(
|
||||
}
|
||||
|
||||
.spinner {
|
||||
color: var(--color-extra-blue-base);
|
||||
color: var(--color-purple-base);
|
||||
display: flex;
|
||||
margin: auto;
|
||||
width: 10rem;
|
||||
|
||||
@@ -5,28 +5,28 @@
|
||||
: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>
|
||||
.contact {
|
||||
font-weight: 400;
|
||||
font-size: 20px;
|
||||
color: var(--color-blue-scale-100);
|
||||
color: var(--color-grey-100);
|
||||
|
||||
& a {
|
||||
text-transform: lowercase;
|
||||
|
||||
@@ -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,31 +20,31 @@
|
||||
<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>
|
||||
.warning-not-current-pool {
|
||||
color: var(--color-orange-world-base);
|
||||
color: var(--color-orange-base);
|
||||
cursor: pointer;
|
||||
|
||||
.wrapper {
|
||||
|
||||
@@ -3,47 +3,41 @@
|
||||
</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>
|
||||
.power-state-icon {
|
||||
color: var(--color-extra-blue-d60);
|
||||
color: var(--color-purple-d60);
|
||||
|
||||
&.state-running {
|
||||
color: var(--color-green-infra-base);
|
||||
color: var(--color-green-base);
|
||||
}
|
||||
|
||||
&.state-paused {
|
||||
color: var(--color-blue-scale-300);
|
||||
color: var(--color-grey-300);
|
||||
}
|
||||
|
||||
&.state-suspended {
|
||||
color: var(--color-extra-blue-d20);
|
||||
color: var(--color-purple-d20);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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,38 +8,36 @@
|
||||
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>
|
||||
.progress-circle-fill {
|
||||
animation: progress 1s ease-out forwards;
|
||||
fill: none;
|
||||
stroke: var(--color-green-infra-base);
|
||||
stroke: var(--color-green-base);
|
||||
stroke-width: 1.2;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: v-bind(progress), 100;
|
||||
@@ -52,13 +46,13 @@ const progress = computed(() => {
|
||||
.progress-circle-background {
|
||||
fill: none;
|
||||
stroke-width: 1.2;
|
||||
stroke: var(--color-blue-scale-400);
|
||||
stroke: var(--color-grey-500);
|
||||
}
|
||||
|
||||
.progress-circle-text {
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
fill: var(--color-green-infra-base);
|
||||
fill: var(--color-green-base);
|
||||
text-anchor: middle;
|
||||
alignment-baseline: middle;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -29,18 +29,18 @@ defineProps<{
|
||||
align-items: center;
|
||||
height: 6rem;
|
||||
padding: 0 1.5rem;
|
||||
border-bottom: 1px solid var(--color-blue-scale-400);
|
||||
border-bottom: 1px solid var(--color-grey-500);
|
||||
background-color: var(--background-color-primary);
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 2.5rem;
|
||||
color: var(--color-extra-blue-base);
|
||||
color: var(--color-purple-base);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2.5rem;
|
||||
color: var(--color-blue-scale-100);
|
||||
color: var(--color-grey-100);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -63,28 +60,28 @@ const computedData = computed(() => {
|
||||
}
|
||||
|
||||
.progress-item:nth-child(1) {
|
||||
--progress-bar-color: var(--color-extra-blue-d60);
|
||||
--progress-bar-color: var(--color-purple-d60);
|
||||
}
|
||||
|
||||
.progress-item:nth-child(2) {
|
||||
--progress-bar-color: var(--color-extra-blue-d40);
|
||||
--progress-bar-color: var(--color-purple-d40);
|
||||
}
|
||||
|
||||
.progress-item:nth-child(3) {
|
||||
--progress-bar-color: var(--color-extra-blue-d20);
|
||||
--progress-bar-color: var(--color-purple-d20);
|
||||
}
|
||||
|
||||
.progress-item {
|
||||
--progress-bar-height: 1.2rem;
|
||||
--progress-bar-color: var(--color-extra-blue-l20);
|
||||
--progress-bar-background-color: var(--color-blue-scale-400);
|
||||
--progress-bar-color: var(--color-purple-l20);
|
||||
--progress-bar-background-color: var(--color-grey-500);
|
||||
|
||||
&.warning {
|
||||
--progress-bar-color: var(--color-orange-world-base);
|
||||
--progress-bar-color: var(--color-orange-base);
|
||||
}
|
||||
|
||||
&.error {
|
||||
--progress-bar-color: var(--color-red-vates-base);
|
||||
--progress-bar-color: var(--color-red-base);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -25,12 +25,12 @@
|
||||
th,
|
||||
td {
|
||||
padding: 0.3rem 0.6rem;
|
||||
border-bottom: 0.1rem solid var(--color-blue-scale-400);
|
||||
border-bottom: 0.1rem solid var(--color-grey-500);
|
||||
vertical-align: center;
|
||||
}
|
||||
|
||||
&:nth-child(odd) {
|
||||
background-color: var(--background-color-extra-blue);
|
||||
background-color: var(--background-color-purple-10);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -140,14 +127,14 @@ const openRawValueModal = (code: string) =>
|
||||
align-items: center;
|
||||
padding: 0.4rem 0.6rem;
|
||||
cursor: pointer;
|
||||
color: var(--color-blue-scale-300);
|
||||
color: var(--color-grey-300);
|
||||
border-radius: 0.4rem;
|
||||
gap: 0.6rem;
|
||||
|
||||
&.active {
|
||||
font-weight: 600;
|
||||
cursor: default;
|
||||
color: var(--color-green-infra-l20);
|
||||
color: var(--color-green-l20);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,7 +157,7 @@ const openRawValueModal = (code: string) =>
|
||||
|
||||
.help {
|
||||
font-style: italic;
|
||||
color: var(--color-blue-scale-200);
|
||||
color: var(--color-grey-200);
|
||||
}
|
||||
|
||||
.default-value {
|
||||
@@ -181,12 +168,12 @@ const openRawValueModal = (code: string) =>
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
opacity: 1;
|
||||
color: var(--color-green-infra-base);
|
||||
color: var(--color-green-base);
|
||||
}
|
||||
}
|
||||
|
||||
.v-model-indicator,
|
||||
.context-indicator {
|
||||
color: var(--color-green-infra-base);
|
||||
color: var(--color-green-base);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -87,7 +81,7 @@ const isIndeterminate = computed(
|
||||
|
||||
.input.indeterminate + .fake-checkbox > .icon {
|
||||
opacity: 1;
|
||||
color: var(--color-blue-scale-300);
|
||||
color: var(--color-grey-300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +114,7 @@ const isIndeterminate = computed(
|
||||
|
||||
.fake-checkbox {
|
||||
width: 2.5em;
|
||||
--background-color: var(--color-blue-scale-400);
|
||||
--background-color: var(--color-grey-500);
|
||||
}
|
||||
|
||||
.icon {
|
||||
@@ -134,7 +128,7 @@ const isIndeterminate = computed(
|
||||
|
||||
.input.indeterminate + .fake-checkbox > .icon {
|
||||
opacity: 1;
|
||||
color: var(--color-blue-scale-300);
|
||||
color: var(--color-grey-300);
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@@ -149,10 +143,9 @@ const isIndeterminate = computed(
|
||||
.icon {
|
||||
font-size: var(--checkbox-icon-size);
|
||||
position: absolute;
|
||||
color: var(--color-blue-scale-500);
|
||||
color: var(--color-grey-600);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -169,44 +162,44 @@ const isIndeterminate = computed(
|
||||
background-color: var(--background-color);
|
||||
box-shadow: var(--shadow-100);
|
||||
|
||||
--border-color: var(--color-blue-scale-400);
|
||||
--border-color: var(--color-grey-500);
|
||||
}
|
||||
|
||||
.input:disabled {
|
||||
& + .fake-checkbox {
|
||||
cursor: not-allowed;
|
||||
--background-color: var(--background-color-secondary);
|
||||
--border-color: var(--color-blue-scale-400);
|
||||
--border-color: var(--color-grey-500);
|
||||
}
|
||||
|
||||
&:checked + .fake-checkbox {
|
||||
--border-color: transparent;
|
||||
--background-color: var(--color-extra-blue-l60);
|
||||
--background-color: var(--color-purple-l60);
|
||||
}
|
||||
}
|
||||
|
||||
.input:not(:disabled) {
|
||||
&:hover + .fake-checkbox,
|
||||
&:focus + .fake-checkbox {
|
||||
--border-color: var(--color-extra-blue-l40);
|
||||
--border-color: var(--color-purple-l40);
|
||||
}
|
||||
|
||||
&:active + .fake-checkbox {
|
||||
--border-color: var(--color-extra-blue-l20);
|
||||
--border-color: var(--color-purple-l20);
|
||||
}
|
||||
|
||||
&:checked + .fake-checkbox {
|
||||
--border-color: transparent;
|
||||
--background-color: var(--color-extra-blue-base);
|
||||
--background-color: var(--color-purple-base);
|
||||
}
|
||||
|
||||
&:checked:hover + .fake-checkbox,
|
||||
&:checked:focus + .fake-checkbox {
|
||||
--background-color: var(--color-extra-blue-d20);
|
||||
--background-color: var(--color-purple-d20);
|
||||
}
|
||||
|
||||
&:checked:active + .fake-checkbox {
|
||||
--background-color: var(--color-extra-blue-d40);
|
||||
--background-color: var(--color-purple-d40);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -153,14 +144,14 @@ defineExpose({
|
||||
--after-width: v-bind('afterWidth || "1.625em"');
|
||||
--caret-width: 1.5em;
|
||||
|
||||
--text-color: var(--color-blue-scale-100);
|
||||
--text-color: var(--color-grey-100);
|
||||
|
||||
&.empty {
|
||||
--text-color: var(--color-blue-scale-300);
|
||||
--text-color: var(--color-grey-300);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
--text-color: var(--color-blue-scale-400);
|
||||
--text-color: var(--color-grey-500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +189,7 @@ defineExpose({
|
||||
}
|
||||
|
||||
--background-color: var(--background-color-primary);
|
||||
--border-color: var(--color-blue-scale-400);
|
||||
--border-color: var(--color-grey-500);
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
@@ -208,63 +199,63 @@ defineExpose({
|
||||
&:not(:disabled) {
|
||||
&.info {
|
||||
&:hover {
|
||||
--border-color: var(--color-extra-blue-l60);
|
||||
--border-color: var(--color-purple-l60);
|
||||
}
|
||||
|
||||
&:active {
|
||||
--border-color: var(--color-extra-blue-l40);
|
||||
--border-color: var(--color-purple-l40);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
--border-color: var(--color-extra-blue-base);
|
||||
--border-color: var(--color-purple-base);
|
||||
}
|
||||
}
|
||||
|
||||
&.success {
|
||||
--border-color: var(--color-green-infra-base);
|
||||
--border-color: var(--color-green-base);
|
||||
|
||||
&:hover {
|
||||
--border-color: var(--color-green-infra-l60);
|
||||
--border-color: var(--color-green-l60);
|
||||
}
|
||||
|
||||
&:active {
|
||||
--border-color: var(--color-green-infra-l40);
|
||||
--border-color: var(--color-green-l40);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
--border-color: var(--color-green-infra-base);
|
||||
--border-color: var(--color-green-base);
|
||||
}
|
||||
}
|
||||
|
||||
&.warning {
|
||||
--border-color: var(--color-orange-world-base);
|
||||
--border-color: var(--color-orange-base);
|
||||
|
||||
&:hover {
|
||||
--border-color: var(--color-orange-world-l60);
|
||||
--border-color: var(--color-orange-l60);
|
||||
}
|
||||
|
||||
&:active {
|
||||
--border-color: var(--color-orange-world-l40);
|
||||
--border-color: var(--color-orange-l40);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
--border-color: var(--color-orange-world-base);
|
||||
--border-color: var(--color-orange-base);
|
||||
}
|
||||
}
|
||||
|
||||
&.error {
|
||||
--border-color: var(--color-red-vates-base);
|
||||
--border-color: var(--color-red-base);
|
||||
|
||||
&:hover {
|
||||
--border-color: var(--color-red-vates-l60);
|
||||
--border-color: var(--color-red-l60);
|
||||
}
|
||||
|
||||
&:active {
|
||||
--border-color: var(--color-red-vates-l40);
|
||||
--border-color: var(--color-red-l40);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
--border-color: var(--color-red-vates-base);
|
||||
--border-color: var(--color-red-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -104,7 +96,7 @@ useContext(DisabledContext, () => props.disabled);
|
||||
|
||||
&.light {
|
||||
font-size: 1.6rem;
|
||||
color: var(--color-blue-scale-300);
|
||||
color: var(--color-grey-300);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@@ -112,7 +104,7 @@ useContext(DisabledContext, () => props.disabled);
|
||||
font-size: 1.4rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: var(--color-blue-scale-100);
|
||||
color: var(--color-grey-100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +126,7 @@ useContext(DisabledContext, () => props.disabled);
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
text-decoration: none;
|
||||
color: var(--color-extra-blue-base);
|
||||
color: var(--color-purple-base);
|
||||
|
||||
& > span {
|
||||
text-decoration: underline;
|
||||
@@ -142,14 +134,14 @@ useContext(DisabledContext, () => props.disabled);
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--color-orange-world-base);
|
||||
color: var(--color-orange-base);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--color-red-vates-base);
|
||||
color: var(--color-red-base);
|
||||
}
|
||||
|
||||
.help {
|
||||
color: var(--color-blue-scale-300);
|
||||
color: var(--color-grey-300);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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,47 +13,47 @@
|
||||
</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>
|
||||
.collapsible {
|
||||
padding: 1rem 1.5rem;
|
||||
background-color: var(--background-color-extra-blue);
|
||||
background-color: var(--background-color-purple-10);
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
|
||||
@@ -67,16 +67,16 @@ whenever(
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: var(--color-extra-blue-base);
|
||||
color: var(--color-purple-base);
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--color-extra-blue-base);
|
||||
border-bottom: 1px solid var(--color-purple-base);
|
||||
width: 100%;
|
||||
font-size: 2rem;
|
||||
font-weight: 500;
|
||||
padding-bottom: 1rem;
|
||||
|
||||
.collapsible & {
|
||||
color: var(--color-blue-scale-100);
|
||||
color: var(--color-grey-100);
|
||||
padding-bottom: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -87,6 +87,6 @@ whenever(
|
||||
}
|
||||
|
||||
.collapse-icon {
|
||||
color: var(--color-extra-blue-base);
|
||||
color: var(--color-purple-base);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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,18 +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"
|
||||
/>
|
||||
<InfraAction
|
||||
:icon="isExpanded ? faAngleDown : faAngleUp"
|
||||
@click="toggle()"
|
||||
/>
|
||||
<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()" />
|
||||
</template>
|
||||
</InfraItemLabel>
|
||||
|
||||
@@ -25,39 +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 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 vmCount = computed(() => recordsByHostRef.value.get(props.hostOpaqueRef)?.length ?? 0)
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
@@ -72,6 +65,20 @@ const [isExpanded, toggle] = useToggle(true);
|
||||
}
|
||||
|
||||
.master-icon {
|
||||
color: var(--color-orange-world-base);
|
||||
color: var(--color-orange-base);
|
||||
}
|
||||
|
||||
.vm-count {
|
||||
font-size: smaller;
|
||||
font-weight: bold;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
color: var(--color-grey-600);
|
||||
border-radius: calc(var(--size) / 2);
|
||||
background-color: var(--color-purple-base);
|
||||
--size: 2.3rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
@@ -27,6 +23,6 @@ const { records: hosts, isReady, hasError } = useHostCollection();
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
line-height: 150%;
|
||||
color: var(--color-red-vates-base);
|
||||
color: var(--color-red-base);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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,48 +19,48 @@
|
||||
</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>
|
||||
.infra-item-label {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
color: var(--color-blue-scale-100);
|
||||
color: var(--color-grey-100);
|
||||
border-radius: 0.8rem;
|
||||
background-color: var(--background-color-primary);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-blue-scale-100);
|
||||
color: var(--color-grey-100);
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.active {
|
||||
color: var(--color-extra-blue-base);
|
||||
color: var(--color-purple-base);
|
||||
background-color: var(--background-color-primary);
|
||||
}
|
||||
|
||||
&.exact-active {
|
||||
color: var(--color-blue-scale-100);
|
||||
background-color: var(--background-color-extra-blue);
|
||||
color: var(--color-grey-100);
|
||||
background-color: var(--background-color-purple-10);
|
||||
|
||||
.icon {
|
||||
color: var(--color-extra-blue-base);
|
||||
color: var(--color-purple-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -27,7 +27,7 @@ defineProps<{
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--color-blue-scale-100);
|
||||
color: var(--color-grey-100);
|
||||
}
|
||||
|
||||
.link-placeholder {
|
||||
@@ -41,7 +41,7 @@ defineProps<{
|
||||
.loader {
|
||||
flex: 1;
|
||||
animation: pulse alternate 1s infinite;
|
||||
background-color: var(--background-color-extra-blue);
|
||||
background-color: var(--background-color-purple-10);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
@@ -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>
|
||||
@@ -50,6 +43,6 @@ const { isReady, hasError, pool } = usePoolCollection();
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
line-height: 150%;
|
||||
color: var(--color-red-vates-base);
|
||||
color: var(--color-red-base);
|
||||
}
|
||||
</style>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user