Compare commits

...

170 Commits

Author SHA1 Message Date
Florent BEAUCHAMP
4d10e261f8 feat(s3): compute sensible chunk size for s3 upload 2023-10-10 14:45:38 +02:00
Florent BEAUCHAMP
84252c3abe feat(fs/s3): compute md5 only when needed 2023-10-10 11:44:55 +02:00
Florent BEAUCHAMP
4fb48e01fa fix(@xen-orchestra/fs: compute md5 only when needed 2023-10-10 11:33:56 +02:00
Florent BEAUCHAMP
516fc3f6ff feat: object lock mode need content md5
also Put object part with a prevalculated md5 and size doesn'c consume additionnal memory against presigned + raw upload
2023-10-04 14:26:59 +02:00
Florent BEAUCHAMP
676851ea82 feat(s3): test upload without sdk 2023-10-02 14:49:21 +02:00
Florent BEAUCHAMP
a7a64f4281 fix(fs/s3): throw an error if upload >50GB 2023-10-02 14:31:01 +02:00
Gabriel Gunullu
2e1abad255 feat(xapi/VDI_importContent): add SR name_label to task name_label (#6979) 2023-09-28 16:10:29 +02:00
Julien Fontanet
c7d5b4b063 fix(xo-web/messages): clarify *forget tokens* description
Introduced by c7df11cc6
2023-09-28 15:41:10 +02:00
Julien Fontanet
cc5f4b0996 fix(xo-web/messages): connection token → authentication token
Uniformize naming.
2023-09-28 15:41:06 +02:00
Julien Fontanet
55f627ed83 chore: fix formatting
Introduced by 869f7ffab
2023-09-28 15:37:45 +02:00
Florent BEAUCHAMP
988179a3f0 fix(xo-server): add mbr for cloud-init only for windows VM (#7050)
Fixes zammad#16808
2023-09-28 09:09:13 +02:00
Julien Fontanet
ce617e0732 fix(xo-server/host.restart): make force defaults to false
Introduced by 5ee11c7b6
2023-09-27 17:39:10 +02:00
Florent BEAUCHAMP
f0f429a473 fix(xo-server-backup-report): send report for Mirror Backup (#7049) 2023-09-27 16:39:27 +02:00
Thierry Goettelmann
bb6e158301 feat(lite): host patches (#6709) 2023-09-27 11:44:03 +02:00
Pierre Donias
7ff304a042 feat: technical release (#7058) 2023-09-27 11:30:16 +02:00
Julien Fontanet
7df1994d7f fix(xo-server/sr.getAllUnhealthyVdiChainsLength): require admin permission
Introduced by 0975863d9
2023-09-27 10:37:30 +02:00
Mathieu
a3a2fda157 feat(lite/pool/VMs): ability to snapshot selected VMs (#7021) 2023-09-26 17:28:15 +02:00
Thierry Goettelmann
d8530f9518 chore(lite): update changelog (#7057)
Fixes [#7040](https://github.com/vatesfr/xen-orchestra/pull/7040)
2023-09-26 17:13:29 +02:00
Thierry Goettelmann
d3062ac35c feat(lite/pool/VMs): ability to migrate selected VMs (#7040) 2023-09-26 17:06:00 +02:00
Thierry Goettelmann
b11f11f4db feat(lite): rework modal system (#6994) 2023-09-26 16:25:23 +02:00
Thierry Goettelmann
79d48f3b56 feat(lite/xapi): update XenApi types and enums (#7018) 2023-09-26 15:19:33 +02:00
Pierre Donias
869f7ffab0 feat(xo-web/XOA/Support): button to restart xo-server service (#7056) 2023-09-26 14:35:17 +02:00
Julien Fontanet
6665d6a8e6 chore: format with Prettier 2023-09-26 14:34:47 +02:00
Pierre Donias
8eb0bdbda7 feat(xo-server,xo-web/SR): reclaim space (#7054)
Fixes #1204
2023-09-26 14:21:43 +02:00
Mathieu
710689db0b feat(xo-web/home/host,pool): display product brand and version (#7027) 2023-09-26 11:16:08 +02:00
mathieuRA
801eea7e75 feat(xo-web/host/advanced): confirmation modal for download system logs 2023-09-26 11:10:22 +02:00
Julien Fontanet
7885e1e6e7 feat(xo-web/host/advanced): button do download system logs
Fixes #3968
2023-09-26 11:10:22 +02:00
Julien Fontanet
d384c746ca feat(xo-server/rest-api): export host audit and system logs
See #3968
2023-09-26 11:10:22 +02:00
Pierre Donias
a30d962b1d feat(xo-server,xo-web/patching): support new XS Updates system (#7044)
See Zammad#13416

Support for new XenServer Updates system with authentication:
- User downloads Client ID JSON file from XenServer account
- User uploads it to XO in their user preferences
- XO uses `username` and `apikey` from that file to authenticate and download updates
2023-09-26 10:29:07 +02:00
Pierre Donias
b6e078716b docs(users/auth): update GitHub plugin screenshots (#7035) 2023-09-25 16:10:00 +02:00
Julien Fontanet
34b69c7ee8 chore: refresh yarn.lock
Introduced by 90e0f2684
2023-09-25 09:10:54 +02:00
Julien Fontanet
70bf8d9620 fix(xo-web/kubernetes): handle empty searches domain field
Do not send `['']` if empty.
2023-09-25 09:08:33 +02:00
Florent BEAUCHAMP
c8bfda9cf5 fix(xo-vmdk-to-vhd): handle ova with disk position collision (#7051)
Some OVA have multiple disks with the same position, which prevent the VM from being created (error while creating VBD). Renumeroting the problematic disk works around the issue.

This may lead to unbootable VM in case the renumeroted disk was the bootable one (VMware-VirtualSAN-Witness-7.0.0-15843807.ova for example).

Fixes #7046
2023-09-22 11:44:12 +02:00
Gabriel Gunullu
1eb4c20844 fix(xo-web/kubernetes): remove required property from search domain (#7028)
Make this field optional for the cluster creation.
2023-09-22 09:46:13 +02:00
Florent BEAUCHAMP
e5c5f19219 fix(backups): mirror must not replicate themselves (#7043)
Fixes zammad#16871
2023-09-21 14:45:29 +02:00
Florent BEAUCHAMP
db92f0e365 fix(vhd-lib): VhdFile implementation is not compatible with encrypted remote (#7045) 2023-09-21 11:18:44 +02:00
Adocentyn
570de7c0fe feat: add licenses (#7042) 2023-09-21 10:28:31 +02:00
Florent BEAUCHAMP
90e0f26845 fix(xo-server): ova export with files bigger than 8.2GB (#7047)
Following 15f69a1

Updating tar-stream to latest version fixes support of files bigger than 8.2GB
2023-09-20 17:17:45 +02:00
Julien Fontanet
c714bc3518 fix(stream-reader): requires Node >=12.3 2023-09-18 09:51:14 +02:00
Julien Fontanet
48e0acda32 chore: update dev deps 2023-09-18 09:43:13 +02:00
Thierry Goettelmann
013cdbcd96 fix(lite/composable): useSubscriber is disabled by default (#7041) 2023-09-15 12:01:20 +02:00
Julien Fontanet
fdd886f213 chore(xo-web/jobs): use set for user ids 2023-09-15 11:05:23 +02:00
Julien Fontanet
de70ef3064 chore(xo-web/jobs): use addSubscriptions for all subs 2023-09-15 11:05:23 +02:00
Julien Fontanet
9142a95f79 feat(xo-web/addSubscriptions): support initial values 2023-09-15 11:05:23 +02:00
Julien Fontanet
1c6aebf997 fix(xo-web/jobs): make schedules a computed
Fixes #6968

The schedules did not appear if the jobs subscription triggered after the schedules one.

The logic has been moved to a computed depending on both subscriptions.
2023-09-15 11:05:23 +02:00
Julien Fontanet
7b9ec4b7a7 chore(xo-web/_getScheduleJob): remove unnecessary sort 2023-09-15 11:05:23 +02:00
Julien Fontanet
decb87f0c9 chore(xo-web/_getScheduleJob): explicit comparison 2023-09-15 11:05:23 +02:00
Julien Fontanet
e17470f56c chore(xo-web/_getScheduleJob): fix comment 2023-09-15 11:05:23 +02:00
Julien Fontanet
99ddbcdc67 fix(xo-web/_getScheduleJob): jobs can be undefined
Related to #6968
2023-09-15 11:05:23 +02:00
Pierre Donias
6953e2fe7b fix(xo-web/backup/mirror): submit button: "Edit" → "Save" (#7036) 2023-09-13 10:06:30 +02:00
Pierre Donias
beb1063ba1 fix(xo-server-auth-github): bad argument passed to registerUser2 (#7032)
Introduced by 562401ebe4
2023-09-12 11:39:55 +02:00
Pierre Donias
7773edd590 fix(xo-server-auth-google): bad argument passed to registerUser2 (#7031)
Introduced by 91b19d9bc4
See https://xcp-ng.org/forum/topic/7729
2023-09-12 11:21:28 +02:00
Julien Fontanet
0104649b84 fix(xo-server/importVmBackupNg): set result when restoring via XO Proxy (#7026) 2023-09-10 18:32:34 +02:00
Pierre Donias
1c9d1049e0 fix(xo-web/render-xo-item/PIF): hide parenthesis if no info inside (#7022)
See Zammad#17381
2023-09-08 10:45:28 +02:00
Pierre Donias
d992a4cb87 feat(netbox): don't delete VMs and interfaces that don't have a UUID (#7008)
See https://xcp-ng.org/forum/topic/7639

In an effort of not deleting or overwriting useful data that has been added
manually by the user, this reverts the feature of deleting VMs and interfaces
that are not bound to an XO object via their custom field UUID. Such objects:
- shouldn't exist in normal use cases anyway
- aren't an issue for the Netbox sync
- are easy to clean manually
2023-09-08 10:36:16 +02:00
Julien Fontanet
52114ad4b0 docs(backup_troubleshooting): unexpected key/full (#7023)
Co-authored-by: Jon Sands <fohdeesha@gmail.com>
2023-09-08 09:56:21 +02:00
Julien Fontanet
bcc62cfcaf feat: release 5.86.1 2023-09-07 16:45:50 +02:00
Julien Fontanet
60434b136a feat(xo-web): 5.124.1 2023-09-06 16:55:52 +02:00
Julien Fontanet
13f3c8851d feat(xo-server): 5.122.0 2023-09-06 16:55:33 +02:00
Julien Fontanet
f386f94dc2 feat(@xen-orchestra/proxy): 0.26.33 2023-09-06 16:52:02 +02:00
Julien Fontanet
fda1fd1a04 feat(xen-api): 1.3.6 2023-09-06 16:51:34 +02:00
Thierry
0b17bdd9bc fix(lite/types): type of ObjectLink component is broken 2023-09-06 09:03:32 +00:00
Thierry
2c5706a89b fix(lite/types): issue with createUseCollection typing in VSCode 2023-09-06 09:03:32 +00:00
rbarhtaoui
5448452b71 fix(xo-web): fix naming conflict for duplicate variables (#7019)
Introduced by c9244b2
2023-09-05 12:28:25 +02:00
Julien Fontanet
22e7c126e6 fix(xen-api): set hostnameRaw before creating transport
Fixes zammad#17423

Introduced by 158a8e14a

Fix XML-RPC transport.
2023-09-05 10:52:11 +02:00
Julien Fontanet
750fefe957 fix(xo-web): don't delete other user's auth tokens
Fixes zammad#17276
2023-09-05 10:39:33 +02:00
Julien Fontanet
025e671989 feat(xo-server/api): split token.delete to token.deleteOwn
So that the behavior is more consistent.
2023-09-05 10:39:33 +02:00
Thierry Goettelmann
df0ed5e794 feat(lite): implement useContext composable (#6991) 2023-09-05 10:05:42 +02:00
Manon Mercier
da45ace7c1 docs(manage_infrastructure): info about Dashboard/Health (#7003)
Related to #5678 

Co-authored-by: Jon Sands <fohdeesha@gmail.com>
2023-09-04 15:48:18 +02:00
Manon Mercier
2a623b8ae7 docs(manage_infrastructure): manage dom0 memory (#6916) 2023-09-04 15:26:55 +02:00
Pierre Donias
f034ec45f3 feat(lite): 0.1.3 (#7011) 2023-09-01 13:42:38 +02:00
Pierre Donias
970bc0ac5d fix(lite/stories): bad import path for POWER_STATE (#7012)
Introduced by 5e8539865f
2023-09-01 11:07:49 +02:00
Mathieu
3abbc8d57e feat: release 5.86.0 (#7010) 2023-08-31 14:53:04 +02:00
Mathieu
06570d78a0 feat: technical release (#7009) 2023-08-31 10:24:11 +02:00
Florent BEAUCHAMP
6a0df7aec2 feat(fs/s3): retry on failures (#6966) 2023-08-31 09:51:28 +02:00
Julien Fontanet
30aeb95f3a fix(xo-server-audit): ignore more side-effects free methods 2023-08-31 09:26:23 +02:00
Julien Fontanet
36d6d53a26 fix(xo-web/jobs/schedules): order jobs by name
Fixes https://xcp-ng.org/forum/post/64825
2023-08-30 22:27:33 +02:00
Thierry Goettelmann
895773b6c6 feat(lite): add alarms to pool dashboard (#6976)
* feat(lite): new iteration for XenApi, stores and subscriptions

* feat(lite/xen-api): enhance XenApi typings and utils

* feat(lite): add subscription dependencies for xen-api records stores

* feat(lite/xen-api): use generics for XenApiEvent

* feat(lite/xen-api): add load error handling

* feat(lite/store): simplify alarm store onRemove

* feat(lite): add alarms to pool dashboard

* feat(lite/alarms): rename type

* feat(lite/object-link): merge useStore and routeName configs + better naming

* feat(lite/object-link): typing enhancement + loader

* feat(lite): feedback on pool dashboard alarms

* feedback
2023-08-30 17:22:41 +02:00
Pierre Donias
8ebc0dba4f feat(netbox): primary IP: fallback to next IPs in the list of addresses
Fixes #6978
2023-08-30 17:02:46 +02:00
Pierre Donias
006f12f17f feat(netbox): do not throw when IP cannot be parsed
See https://xcp-ng.org/forum/topic/7625
2023-08-30 17:02:46 +02:00
Pierre Donias
b22239804a fix(netbox): properly remove deleted Netbox IPs from local collection 2023-08-30 17:02:46 +02:00
Pierre Donias
afd174ca21 feat(netbox): handle empty collections in request() method 2023-08-30 17:02:46 +02:00
Pierre Donias
27c6c1b896 chore(netbox): use lodash find/filter where relevant 2023-08-30 17:02:46 +02:00
Florent BEAUCHAMP
311b420b74 feat(backups): expose preferNbd setting per backup job (#6995) 2023-08-30 16:59:36 +02:00
Julien Fontanet
e403298140 fix(CHANGELOG.unreleased): add missing xo-server
Introduced by 9c7fd94a9
2023-08-30 16:13:50 +02:00
Julien Fontanet
9c7fd94a9b fix(xo-server/rest-api): limit applies at the end for {backups,restore}/logs
Fixes https://xcp-ng.org/forum/post/64880
2023-08-30 16:02:42 +02:00
Mathieu
8cdae83150 feat: technical release (#7007) 2023-08-30 10:37:46 +02:00
Pierre Donias
5b1cc7415e fix(xo-server/normalizeVmNetworks): always assume multiple space-delimited IPs (#6990)
See https://xcp-ng.org/forum/topic/7625
2023-08-30 09:56:08 +02:00
Florent BEAUCHAMP
f5d3bc1f2d feat(backups): merge worker concurrency (#6965) 2023-08-30 09:38:37 +02:00
Julien Fontanet
ba81d0e08a feat(xo-server-transport-email): add local hostname to config (#6988)
Fixes https://xcp-ng.org/forum/topic/7579/
2023-08-29 23:22:31 +02:00
Pierre Donias
3b3f927e4b docs(netbox): steps and labels better match Netbox UI (#6986)
See https://xcp-ng.org/forum/topic/7625
2023-08-29 15:42:17 +02:00
Thierry Goettelmann
5e8539865f feat(lite): new iteration for XenApi, stores and subscriptions (#6998) 2023-08-29 15:23:18 +02:00
Florent BEAUCHAMP
3a3fa2882c fix(backup/healthcheck): mirror backup appeared detached (#7000) 2023-08-29 14:52:44 +02:00
Thierry Goettelmann
3baa37846e feat(lite/stories): allow to organize stories in subdirectories (#6992) 2023-08-21 11:12:16 +02:00
Mathieu
999fba2030 feat(xo-web/pool/advanced): ability to set a crash dump SR (#6973)
Fixes #5060
2023-08-18 15:34:05 +02:00
Mathieu
785a5857ef fix(xapi/host_smartReboot): resume VMs after enabling host (#6980)
Found when investigating https://xcp-ng.org/forum/post/60372
2023-08-17 16:22:35 +02:00
Thierry Goettelmann
067f4ac882 feat(lite): new XenApi records collection system (#6975) 2023-08-17 15:22:33 +02:00
Julien Fontanet
8a26e08102 feat(xo-server/rest-api): filter/limit support for {backups/restore}/logs
Fixes https://xcp-ng.org/forum/post/64789
2023-08-17 13:59:32 +02:00
Julien Fontanet
42aa202f7a fix(xo-server/job.set): accept userId
Fixes https://xcp-ng.org/forum/post/64668
2023-08-16 15:13:00 +02:00
Julien Fontanet
403d2c8e7b fix(mixins/Tasks): behave when no user connected to API
Introduced by 1ddbe87d0
2023-08-11 11:27:08 +02:00
Julien Fontanet
ad46bde302 feat(backups/XO metadata): transfer binary config in base64 2023-08-10 15:39:34 +02:00
Julien Fontanet
1b6ec2c545 fix(xo-web/home): don't search in linked objects (#6881)
Introduced by 5928984069

For instance, searching the UUID of a running VM was showing all other VMs on the same host due to the UUID being present in their `container.residentVms`.
2023-08-10 14:42:07 +02:00
Julien Fontanet
56388557cb fix(xo-server): increase timeout when file restore via XO Proxy
Related to zammad#13396
2023-08-10 11:37:08 +02:00
Julien Fontanet
1ddbe87d0f feat(mixins/Tasks): inject userId in tasks 2023-08-09 16:18:29 +02:00
Pierre Donias
3081810450 feat(xo-server-netbox): synchronize VM tags
Fixes #5899
See Zammad#12478
See https://xcp-ng.org/forum/topic/6902
2023-08-08 15:23:57 +02:00
Pierre Donias
155be7fd95 fix(netbox): add missing trailing / in URL 2023-08-08 15:23:57 +02:00
Pierre Donias
ef960e94d3 chore(netbox): namespace all XO objects as xo* 2023-08-08 15:23:57 +02:00
Pierre Donias
bfd99a48fe chore(netbox): namespace all Netbox objects as nb* 2023-08-08 15:23:57 +02:00
Florent BEAUCHAMP
a13fda5fe9 fix(backups/_MixinXapiWriter): typo _heathCheckSr → _healthCheckSr (#6969)
Fix `TypeError: Cannot read properties of undefined (reading 'uuid') at #isAlreadyOnHealthCheckSr`
2023-08-08 09:48:53 +02:00
Florent BEAUCHAMP
66bee59774 fix(xen-api/getResource): don't fail silently when HTTP request fails without response (#6970)
Seen while investigating zammad#16309
2023-08-08 09:39:18 +02:00
Julien Fontanet
685400bbf8 fix(xo-server): fix get-stream@3 usage
Fixes #6971

Introduced by 3dca7f2a7
2023-08-08 08:05:38 +02:00
Julien Fontanet
5bef8fc411 fix(lite): disable linting because it's broken
Introduced by 3dca7f2a7
2023-08-05 17:05:02 +02:00
Julien Fontanet
aa7ff1449a fix(lite): adapt ESLint config to prettier@3
Introduced by 3dca7f2a7
2023-08-04 22:09:55 +02:00
Julien Fontanet
3dca7f2a71 chore: update deps 2023-08-03 17:56:24 +02:00
Julien Fontanet
3dc2f649f6 chore: format with Prettier 2023-08-03 17:56:24 +02:00
Julien Fontanet
9eb537c2f9 chore: update dev deps 2023-08-03 17:56:24 +02:00
Thierry Goettelmann
dfd5f6882f feat(lite): enhance typings for improved type safety (#6949) 2023-08-03 11:33:29 +02:00
Julien Fontanet
7214016338 fix(xo-server/_authenticateUser): don't use registerUser()
Introduced by 99605bf18
2023-08-03 10:25:15 +02:00
Julien Fontanet
606e3c4ce5 docs(xo-server-test-plugin): explain configurationPresets 2023-08-03 10:21:15 +02:00
Julien Fontanet
fb04d3d25d docs(xo-server-test-plugin): show title/description for settings 2023-08-03 10:20:51 +02:00
Julien Fontanet
db8c042131 fix(xo-web/plugins): merge preset with existing config
Instead of replacing it.
2023-08-03 10:14:07 +02:00
Julien Fontanet
fd9005fba8 fix(xo-web/plugins): don't disable presets when config not edited 2023-08-03 10:12:52 +02:00
Julien Fontanet
2d25413b8d fix(xo-server-auth-ldap): mark userIdAttribute as required
It can no longer be ommited since 99605bf18
2023-08-03 09:56:33 +02:00
Julien Fontanet
035679800a chore(xo-server-auth-ldap): defaults are merged automatically by xo-server
Related to 8c7d25424
2023-08-03 09:53:01 +02:00
Thierry Goettelmann
abd0a3035a feat(lite/component): created UiResources + UiResource (#6932) 2023-08-01 11:18:10 +02:00
Julien Fontanet
d307730c68 feat: release 5.85.0 2023-07-31 17:06:25 +02:00
Julien Fontanet
1b44de4958 feat(xo-server): 5.120.2 2023-07-31 16:52:03 +02:00
Julien Fontanet
ec78a1ce8b feat(xo-web): 5.122.2 2023-07-31 16:32:42 +02:00
Julien Fontanet
19c82ab30d feat(xo-server): 5.120.1 2023-07-31 16:32:41 +02:00
Julien Fontanet
9986f3fb18 fix(xo-web/removeUserAuthProvider): notify on error
Introduced by 52cf2d151
2023-07-31 16:29:09 +02:00
Julien Fontanet
d24e9c093d fix(xo-server/updaterUser): fix current user auth protection
Introduced by 2d52aee95
2023-07-31 16:28:16 +02:00
Julien Fontanet
70c8b24fac feat(xo-web): 5.122.1 2023-07-31 15:58:15 +02:00
Julien Fontanet
9c9c11104b feat(xo-server-auth-google): 0.3.0 2023-07-31 15:58:05 +02:00
Julien Fontanet
cba90b27f4 feat(xo-server-auth-github): 0.3.0 2023-07-31 15:57:44 +02:00
Julien Fontanet
46cbced570 feat(xo-server): 5.120.0 2023-07-31 15:56:48 +02:00
Julien Fontanet
52cf2d1514 feat(xo-web/settings/users): auth providers can be removed 2023-07-31 15:48:49 +02:00
Julien Fontanet
e51351be8d feat(xo-server/api): user.removeAuthProvider 2023-07-31 15:48:49 +02:00
Julien Fontanet
2a42e0ff94 feat(xo-web/users): display users auth providers
Related to zammad#16318
2023-07-31 15:48:49 +02:00
Julien Fontanet
3a824a2bfc fix(xo-server/updateUser): check password xor auth providers 2023-07-31 15:48:49 +02:00
Julien Fontanet
fc1c809a18 fix(xo-server): remove password when sign in with provider
Password can no longer be used/edited when an auth provider is registered.

For security concerns, this useless password should be removed from the database.
2023-07-31 15:48:49 +02:00
Julien Fontanet
221cd40199 fix(xo-server/updateUser): can remove password 2023-07-31 15:48:49 +02:00
Julien Fontanet
aca19d9a81 fix(xo-server): user pass disabled when associated auth providers 2023-07-31 15:48:49 +02:00
Julien Fontanet
0601bbe18d fix(xo-server/recover-account): remove all auth providers 2023-07-31 15:48:49 +02:00
Julien Fontanet
2d52aee952 fix(xo-server/updateUser): can remove all auth providers with null 2023-07-31 15:48:49 +02:00
Julien Fontanet
99605bf185 feat(xo-server/registerUser): completely disable 2023-07-31 15:48:49 +02:00
Julien Fontanet
91b19d9bc4 feat(xo-server-auth-google): use registerUser2 2023-07-31 15:48:49 +02:00
Julien Fontanet
562401ebe4 feat(xo-server-auth-github): use registerUser2 2023-07-31 15:48:49 +02:00
Julien Fontanet
6fd2f2610d fix(xo-web/new-vm): don't send device in VIFs
Introduced by 6ae19b064

Fixes #6960
2023-07-31 09:34:30 +02:00
Gabriel Gunullu
6ae19b0640 fix(xo-web/new-vm): list VIFs ordered by device (#6944)
Fixes zammad#15920
2023-07-28 18:51:48 +02:00
Pierre Donias
6b936d8a8c feat(lite): 0.1.2 (#6958) 2023-07-28 17:37:07 +02:00
Thierry Goettelmann
8f2cfaae00 feat(lite): open console in new window (#6868)
Add a link to open the console in a new window.
2023-07-28 14:04:06 +02:00
Thierry Goettelmann
5c215e1a8a feat(lite/console): rework VM console page (#6863)
Rework the VM Console page to be better aligned with Figma mockup.
- Spinner while loading the console
- Added the "monitor" image with correct message when VM is powered off
- Better screen space usage
2023-07-28 11:39:33 +02:00
Pierre Donias
e3cb98124f feat: technical release (#6956) 2023-07-28 10:05:26 +02:00
Julien Fontanet
90c3319880 feat(xo-web/backup/file-restore): add export format selection 2023-07-27 17:22:58 +02:00
Julien Fontanet
348db876d2 feat(xo-server/backupNg.fetchFiles): add format param 2023-07-27 17:22:58 +02:00
Julien Fontanet
408fd7ec03 feat(proxy/backup.fetchPartitionFiles): add format param 2023-07-27 17:22:58 +02:00
Julien Fontanet
1fd84836b1 feat(backups/fetchPartitionFiles): add tgz (tar+gzip) support
Around 6 times faster than ZIP export.
2023-07-27 17:22:58 +02:00
Julien Fontanet
522204795f fix(backups/fetchPartitionFiles): rewrite ZIP creation
It's now sequential which leads to better performance and less memory consumption.

Empty directories are now included and all entries have correct mode and modification time.
2023-07-27 17:22:58 +02:00
Julien Fontanet
e29c422ac9 fix(xo-server/_handleHttpRequest): use pipeline between result and response
Properly closes one stream if the other is destroyed.
2023-07-27 17:22:58 +02:00
Florent BEAUCHAMP
152cf09b7e feat(vmware-explorer): handle sesparse files (#6909) 2023-07-27 17:15:29 +02:00
Pierre Donias
ff728099dc docs(netbox): update screenshot (#6955) 2023-07-27 17:13:57 +02:00
Mathieu
706d94221d feat(xo-server/pool/rpu): avoid unnecessary VMs migration (#6943) 2023-07-27 17:12:31 +02:00
Gabriel Gunullu
340e9af7f4 fix(backups): handle incremental replication to multiple SRs (#6811)
Fix matching previous replications when multiple SRs.

Fixes #6582
2023-07-27 17:09:15 +02:00
Pierre Donias
40e536ba61 feat(xo-server-netbox): synchronize VM platform (#6954)
See Zammad#12478
See https://xcp-ng.org/forum/topic/6902
2023-07-27 16:59:50 +02:00
Thierry Goettelmann
fd4c56c8c2 feat(lite/pool): add tasks to Pool Dashboard (#6713)
Other updates:
- Move pending/finished tasks logic to store subscription
- Add `count` prop to `UiCardTitle`
- Add "No tasks" message on Task table if empty
- Make the `finishedTasks` prop optional
- Add ability to have full width dashboard cards
2023-07-27 16:23:52 +02:00
Thierry Goettelmann
20d04ba956 feat(lite): dynamic page title (#6853)
See #6793

ℹ️ This PR adds a `pageTitleStore` which allows defining the current page title
according to 3 parts: an object, a string, and a count. Each part is optional.

 The page title is **reactive** when function argument is a `Ref`, a `Computed`
or a getter. For example, when updating a VM name, the page title will be
updated in every tabs.

🪄 Each title part is automatically unset when the component that set it is
unmounted.
2023-07-27 11:41:33 +02:00
Pierre Donias
3b1bcc67ae feat(xo-server-netbox): rewrite (#6950)
Fixes #6038, Fixes #6135, Fixes #6024, Fixes #6036
See https://xcp-ng.org/forum/topic/6070
See zammad#5695
See https://xcp-ng.org/forum/topic/6149
See https://xcp-ng.org/forum/topic/6332

Complete rewrite of the plugin. Main functional changes:
- Synchronize VM description
- Fix duplicated VMs in Netbox after disconnecting one pool
- Migrating a VM from one pool to another keeps VM data added manually
- Fix largest IP prefix being picked instead of smallest
- Fix synchronization not working if some pools are unavailable
- Better error messages
2023-07-27 10:07:26 +02:00
Julien Fontanet
1add3fbf9d fix(yarn.lock): refresh
Introduced by 1c23bd5ff
2023-07-26 13:36:28 +02:00
Julien Fontanet
97f0759de0 feat(mixins/Hooks): warning every 5s if listener still running
This helps diagnosticate issues when a hook is stuck.'
2023-07-25 16:41:29 +02:00
Julien Fontanet
005ab47d9b fix(xo-web): clear token on authentication failure (#6937)
This prevents infinite refreshes when the token is deemed valid by the server
but the authentication failed for any reasons.
2023-07-25 09:49:11 +02:00
354 changed files with 12549 additions and 6159 deletions

View File

@@ -1,8 +1,11 @@
'use strict'
module.exports = {
arrowParens: 'avoid',
jsxSingleQuote: true,
semi: false,
singleQuote: true,
trailingComma: 'es5',
// 2020-11-24: Requested by nraynaud and approved by the rest of the team
//

View File

@@ -33,7 +33,7 @@
"test": "node--test"
},
"devDependencies": {
"sinon": "^15.0.1",
"sinon": "^16.0.0",
"tap": "^16.3.0",
"test": "^3.2.1"
}

View File

@@ -13,12 +13,15 @@ describe('decorateWith', () => {
const expectedFn = Function.prototype
const newFn = () => {}
const decorator = decorateWith(function wrapper(fn, ...args) {
assert.deepStrictEqual(fn, expectedFn)
assert.deepStrictEqual(args, expectedArgs)
const decorator = decorateWith(
function wrapper(fn, ...args) {
assert.deepStrictEqual(fn, expectedFn)
assert.deepStrictEqual(args, expectedArgs)
return newFn
}, ...expectedArgs)
return newFn
},
...expectedArgs
)
const descriptor = {
configurable: true,

View File

@@ -29,7 +29,7 @@
"ensure-array": "^1.0.0"
},
"devDependencies": {
"sinon": "^15.0.1",
"sinon": "^16.0.0",
"test": "^3.2.1"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@vates/fuse-vhd",
"version": "1.0.0",
"version": "2.0.0",
"license": "ISC",
"private": false,
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/fuse-vhd",
@@ -22,7 +22,7 @@
"fuse-native": "^2.2.6",
"lru-cache": "^7.14.0",
"promise-toolbox": "^0.21.0",
"vhd-lib": "^4.5.0"
"vhd-lib": "^4.6.0"
},
"scripts": {
"postversion": "npm publish --access public"

View File

@@ -13,18 +13,18 @@
"url": "https://vates.fr"
},
"license": "ISC",
"version": "1.2.1",
"version": "2.0.0",
"engines": {
"node": ">=14.0"
},
"main": "./index.mjs",
"dependencies": {
"@vates/async-each": "^1.0.0",
"@vates/read-chunk": "^1.1.1",
"@vates/read-chunk": "^1.2.0",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/log": "^0.6.0",
"promise-toolbox": "^0.21.0",
"xen-api": "^1.3.3"
"xen-api": "^1.3.6"
},
"devDependencies": {
"tap": "^16.3.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@vates/node-vsphere-soap",
"version": "1.0.0",
"version": "2.0.0",
"description": "interface to vSphere SOAP/WSDL from node for interfacing with vCenter or ESXi, forked from node-vsphere-soap",
"main": "lib/client.mjs",
"author": "reedog117",

View File

@@ -19,7 +19,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "1.1.1",
"version": "1.2.0",
"engines": {
"node": ">=8.10"
},

View File

@@ -27,7 +27,7 @@
"license": "ISC",
"version": "0.1.0",
"engines": {
"node": ">=10"
"node": ">=12.3"
},
"scripts": {
"postversion": "npm publish --access public",

View File

@@ -35,7 +35,7 @@
"test": "node--test"
},
"devDependencies": {
"sinon": "^15.0.1",
"sinon": "^16.0.0",
"test": "^3.2.1"
}
}

View File

@@ -7,9 +7,9 @@
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
"dependencies": {
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/backups": "^0.39.0",
"@xen-orchestra/fs": "^4.0.1",
"filenamify": "^4.1.0",
"@xen-orchestra/backups": "^0.42.1",
"@xen-orchestra/fs": "^4.1.0",
"filenamify": "^6.0.0",
"getopts": "^2.2.5",
"lodash": "^4.17.15",
"promise-toolbox": "^0.21.0"
@@ -27,7 +27,7 @@
"scripts": {
"postversion": "npm publish --access public"
},
"version": "1.0.9",
"version": "1.0.12",
"license": "AGPL-3.0-or-later",
"author": {
"name": "Vates SAS",

View File

@@ -5,7 +5,7 @@ import { createLogger } from '@xen-orchestra/log'
import { createVhdDirectoryFromStream, openVhd, VhdAbstract, VhdDirectory, VhdSynthetic } from 'vhd-lib'
import { decorateMethodsWith } from '@vates/decorate-with'
import { deduped } from '@vates/disposable/deduped.js'
import { dirname, join, normalize, resolve } from 'node:path'
import { dirname, join, resolve } from 'node:path'
import { execFile } from 'child_process'
import { mount } from '@vates/fuse-vhd'
import { readdir, lstat } from 'node:fs/promises'
@@ -18,6 +18,7 @@ import fromEvent from 'promise-toolbox/fromEvent'
import groupBy from 'lodash/groupBy.js'
import pDefer from 'promise-toolbox/defer'
import pickBy from 'lodash/pickBy.js'
import tar from 'tar'
import zlib from 'zlib'
import { BACKUP_DIR } from './_getVmBackupDir.mjs'
@@ -41,20 +42,23 @@ const compareTimestamp = (a, b) => a.timestamp - b.timestamp
const noop = Function.prototype
const resolveRelativeFromFile = (file, path) => resolve('/', dirname(file), path).slice(1)
const makeRelative = path => resolve('/', path).slice(1)
const resolveSubpath = (root, path) => resolve(root, makeRelative(path))
const resolveSubpath = (root, path) => resolve(root, `.${resolve('/', path)}`)
async function addZipEntries(zip, realBasePath, virtualBasePath, relativePaths) {
for (const relativePath of relativePaths) {
const realPath = join(realBasePath, relativePath)
const virtualPath = join(virtualBasePath, relativePath)
async function addDirectory(files, realPath, metadataPath) {
const stats = await lstat(realPath)
if (stats.isDirectory()) {
await asyncMap(await readdir(realPath), file =>
addDirectory(files, realPath + '/' + file, metadataPath + '/' + file)
)
} else if (stats.isFile()) {
files.push({
realPath,
metadataPath,
})
const stats = await lstat(realPath)
const { mode, mtime } = stats
const opts = { mode, mtime }
if (stats.isDirectory()) {
zip.addEmptyDirectory(virtualPath, opts)
await addZipEntries(zip, realPath, virtualPath, await readdir(realPath))
} else if (stats.isFile()) {
zip.addFile(realPath, virtualPath, opts)
}
}
}
@@ -182,17 +186,6 @@ export class RemoteAdapter {
})
}
async *_usePartitionFiles(diskId, partitionId, paths) {
const path = yield this.getPartition(diskId, partitionId)
const files = []
await asyncMap(paths, file =>
addDirectory(files, resolveSubpath(path, file), normalize('./' + file).replace(/\/+$/, ''))
)
return files
}
// check if we will be allowed to merge a a vhd created in this adapter
// with the vhd at path `path`
async isMergeableParent(packedParentUid, path) {
@@ -209,15 +202,24 @@ export class RemoteAdapter {
})
}
fetchPartitionFiles(diskId, partitionId, paths) {
fetchPartitionFiles(diskId, partitionId, paths, format) {
const { promise, reject, resolve } = pDefer()
Disposable.use(
async function* () {
const files = yield this._usePartitionFiles(diskId, partitionId, paths)
const zip = new ZipFile()
files.forEach(({ realPath, metadataPath }) => zip.addFile(realPath, metadataPath))
zip.end()
const { outputStream } = zip
const path = yield this.getPartition(diskId, partitionId)
let outputStream
if (format === 'tgz') {
outputStream = tar.c({ cwd: path, gzip: true }, paths.map(makeRelative))
} else if (format === 'zip') {
const zip = new ZipFile()
await addZipEntries(zip, path, '', paths.map(makeRelative))
zip.end()
;({ outputStream } = zip)
} else {
throw new Error('unsupported format ' + format)
}
resolve(outputStream)
await fromEvent(outputStream, 'end')
}.bind(this)
@@ -679,11 +681,13 @@ export class RemoteAdapter {
}
}
async outputStream(path, input, { checksum = true, validator = noop } = {}) {
async outputStream(path, input, { checksum = true, maxStreamLength, streamLength, validator = noop } = {}) {
const container = watchStreamSize(input)
await this._handler.outputStream(path, input, {
checksum,
dirMode: this._dirMode,
maxStreamLength,
streamLength,
async validator() {
await input.task
return validator.apply(this, arguments)
@@ -740,8 +744,15 @@ export class RemoteAdapter {
}
}
readFullVmBackup(metadata) {
return this._handler.createReadStream(resolve('/', dirname(metadata._filename), metadata.xva))
async readFullVmBackup(metadata) {
const xvaPath = resolve('/', dirname(metadata._filename), metadata.xva)
const stream = await this._handler.createReadStream(xvaPath)
try {
stream.length = await this._handler.getSize(xvaPath)
} catch (error) {
warn(`Can't compute length of xva file`, { xvaPath, error })
}
return stream
}
async readVmBackupMetadata(path) {
@@ -824,8 +835,6 @@ decorateMethodsWith(RemoteAdapter, {
debounceResourceFactory,
]),
_usePartitionFiles: Disposable.factory,
getDisk: compose([Disposable.factory, [deduped, diskId => [diskId]], debounceResourceFactory]),
getPartition: Disposable.factory,

View File

@@ -21,7 +21,12 @@ export class RestoreMetadataBackup {
})
} else {
const metadata = JSON.parse(await handler.readFile(join(backupId, 'metadata.json')))
return String(await handler.readFile(resolve(backupId, metadata.data ?? 'data.json')))
const dataFileName = resolve(backupId, metadata.data ?? 'data.json')
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
const isJson = dataFileName.endsWith('.json')
return isJson ? data.toString() : { encoding: 'base64', data: data.toString('base64') }
}
}
}

View File

@@ -16,6 +16,8 @@ export const TAG_BASE_DELTA = 'xo:base_delta'
export const TAG_COPY_SRC = 'xo:copy_of'
const TAG_BACKUP_SR = 'xo:backup:sr'
const ensureArray = value => (value === undefined ? [] : Array.isArray(value) ? value : [value])
const resolveUuid = async (xapi, cache, uuid, type) => {
if (uuid == null) {
@@ -39,6 +41,7 @@ export async function exportIncrementalVm(
fullVdisRequired = new Set(),
disableBaseTags = false,
preferNbd,
} = {}
) {
// refs of VM's VDIs → base's VDIs.
@@ -86,6 +89,7 @@ export async function exportIncrementalVm(
baseRef: baseVdi?.$ref,
cancelToken,
format: 'vhd',
preferNbd,
})
})
@@ -157,7 +161,10 @@ export const importIncrementalVm = defer(async function importIncrementalVm(
if (detectBase) {
const remoteBaseVmUuid = vmRecord.other_config[TAG_BASE_DELTA]
if (remoteBaseVmUuid) {
baseVm = find(xapi.objects.all, obj => (obj = obj.other_config) && obj[TAG_COPY_SRC] === remoteBaseVmUuid)
baseVm = find(
xapi.objects.all,
obj => (obj = obj.other_config) && obj[TAG_COPY_SRC] === remoteBaseVmUuid && obj[TAG_BACKUP_SR] === sr.$id
)
if (!baseVm) {
throw new Error(`could not find the base VM (copy of ${remoteBaseVmUuid})`)

View File

@@ -22,7 +22,13 @@ export class XoMetadataBackup {
const dir = `${scheduleDir}/${formatFilenameDate(timestamp)}`
const data = job.xoMetadata
const dataBaseName = './data.json'
let dataBaseName = './data'
// JSON data is sent as plain string, binary data is sent as an object with `data` and `encoding properties
const isJson = typeof data === 'string'
if (isJson) {
dataBaseName += '.json'
}
const metadata = JSON.stringify(
{
@@ -54,7 +60,7 @@ export class XoMetadataBackup {
async () => {
const handler = adapter.handler
const dirMode = this._config.dirMode
await handler.outputFile(dataFileName, data, { dirMode })
await handler.outputFile(dataFileName, isJson ? data : Buffer.from(data.data, data.encoding), { dirMode })
await handler.outputFile(metaDataFileName, metadata, {
dirMode,
})

View File

@@ -29,6 +29,8 @@ export const FullRemote = class FullRemoteVmBackupRunner extends AbstractRemote
writer =>
writer.run({
stream: forkStreamUnpipe(stream),
// stream is copied and transformed, it's not safe to attach additionnal properties to it
streamLength: stream.length,
timestamp: metadata.timestamp,
vm: metadata.vm,
vmSnapshot: metadata.vmSnapshot,

View File

@@ -35,13 +35,22 @@ export const FullXapi = class FullXapiVmBackupRunner extends AbstractXapi {
useSnapshot: false,
})
)
const vdis = await exportedVm.$getDisks()
let maxStreamLength = 1024 * 1024 // Ovf file and tar headers are a few KB, let's stay safe
vdis.forEach(vdiRef => {
const vdi = this._xapi.getObject(vdiRef)
maxStreamLength += vdi.physical_utilisation ?? 0 // at most the xva will take the physical usage of the disk
// it can be smaller due to the smaller block size for xva than vhd, and compression of xcp-ng
})
const sizeContainer = watchStreamSize(stream)
const timestamp = Date.now()
await this._callWriters(
writer =>
writer.run({
maxStreamLength,
sizeContainer,
stream: forkStreamUnpipe(stream),
timestamp,

View File

@@ -41,6 +41,7 @@ export const IncrementalXapi = class IncrementalXapiVmBackupRunner extends Abstr
const deltaExport = await exportIncrementalVm(exportedVm, baseVm, {
fullVdisRequired,
preferNbd: this._settings.preferNbd,
})
// since NBD is network based, if one disk use nbd , all the disk use them
// except the suspended VDI

View File

@@ -4,6 +4,7 @@ import { Disposable } from 'promise-toolbox'
import { getVmBackupDir } from '../../_getVmBackupDir.mjs'
import { Abstract } from './_Abstract.mjs'
import { extractIdsFromSimplePattern } from '../../extractIdsFromSimplePattern.mjs'
export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstract {
constructor({
@@ -34,7 +35,8 @@ export const AbstractRemote = class AbstractRemoteVmBackupRunner extends Abstrac
this._writers = writers
const RemoteWriter = this._getRemoteWriter()
Object.entries(remoteAdapters).forEach(([remoteId, adapter]) => {
extractIdsFromSimplePattern(job.remotes).forEach(remoteId => {
const adapter = remoteAdapters[remoteId]
const targetSettings = {
...settings,
...allSettings[remoteId],

View File

@@ -24,7 +24,7 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
)
}
async _run({ timestamp, sizeContainer, stream, vm, vmSnapshot }) {
async _run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot }) {
const settings = this._settings
const job = this._job
const scheduleId = this._scheduleId
@@ -65,6 +65,8 @@ export class FullRemoteWriter extends MixinRemoteWriter(AbstractFullWriter) {
await Task.run({ name: 'transfer' }, async () => {
await adapter.outputStream(dataFilename, stream, {
maxStreamLength,
streamLength,
validator: tmpPath => adapter.isValidXva(tmpPath),
})
return { size: sizeContainer.size }

View File

@@ -1,9 +1,9 @@
import { AbstractWriter } from './_AbstractWriter.mjs'
export class AbstractFullWriter extends AbstractWriter {
async run({ timestamp, sizeContainer, stream, vm, vmSnapshot }) {
async run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot }) {
try {
return await this._run({ timestamp, sizeContainer, stream, vm, vmSnapshot })
return await this._run({ maxStreamLength, timestamp, sizeContainer, stream, streamLength, vm, vmSnapshot })
} finally {
// ensure stream is properly closed
stream.destroy()

View File

@@ -18,7 +18,7 @@ export const MixinXapiWriter = (BaseClass = Object) =>
const vdiRefs = await xapi.VM_getDisks(baseVm.$ref)
for (const vdiRef of vdiRefs) {
const vdi = xapi.getObject(vdiRef)
if (vdi.$SR.uuid !== this._heathCheckSr.uuid) {
if (vdi.$SR.uuid !== this._healthCheckSr.uuid) {
return false
}
}

View File

@@ -2,18 +2,21 @@
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable n/shebang */
import { asyncEach } from '@vates/async-each'
import { catchGlobalErrors } from '@xen-orchestra/log/configure'
import { createLogger } from '@xen-orchestra/log'
import { getSyncedHandler } from '@xen-orchestra/fs'
import { join } from 'node:path'
import { load as loadConfig } from 'app-conf'
import Disposable from 'promise-toolbox/Disposable'
import min from 'lodash/min.js'
import { getVmBackupDir } from '../_getVmBackupDir.mjs'
import { RemoteAdapter } from '../RemoteAdapter.mjs'
import { CLEAN_VM_QUEUE } from './index.mjs'
const APP_NAME = 'xo-merge-worker'
const APP_DIR = new URL('.', import.meta.url).pathname
// -------------------------------------------------------------------
catchGlobalErrors(createLogger('xo:backups:mergeWorker'))
@@ -34,6 +37,7 @@ const main = Disposable.wrap(async function* main(args) {
for (let i = 0; i < 10; ++i) {
const entries = await handler.list(CLEAN_VM_QUEUE)
if (entries.length !== 0) {
entries.sort()
return entries
}
await new Promise(timeoutResolver)
@@ -42,38 +46,47 @@ const main = Disposable.wrap(async function* main(args) {
let taskFiles
while ((taskFiles = await listRetry()) !== undefined) {
const taskFileBasename = min(taskFiles)
const previousTaskFile = join(CLEAN_VM_QUEUE, taskFileBasename)
const taskFile = join(CLEAN_VM_QUEUE, '_' + taskFileBasename)
const { concurrency } = await loadConfig(APP_NAME, {
appDir: APP_DIR,
ignoreUnknownFormats: true,
})
await asyncEach(
taskFiles,
async taskFileBasename => {
const previousTaskFile = join(CLEAN_VM_QUEUE, taskFileBasename)
const taskFile = join(CLEAN_VM_QUEUE, '_' + taskFileBasename)
// move this task to the end
try {
await handler.rename(previousTaskFile, taskFile)
} catch (error) {
// this error occurs if the task failed too many times (i.e. too many `_` prefixes)
// there is nothing more that can be done
if (error.code === 'ENAMETOOLONG') {
await handler.unlink(previousTaskFile)
}
// move this task to the end
try {
await handler.rename(previousTaskFile, taskFile)
} catch (error) {
// this error occurs if the task failed too many times (i.e. too many `_` prefixes)
// there is nothing more that can be done
if (error.code === 'ENAMETOOLONG') {
await handler.unlink(previousTaskFile)
}
throw error
}
try {
const vmDir = getVmBackupDir(String(await handler.readFile(taskFile)))
try {
await adapter.cleanVm(vmDir, { merge: true, logInfo: info, logWarn: warn, remove: true })
} catch (error) {
// consider the clean successful if the VM dir is missing
if (error.code !== 'ENOENT') {
throw error
}
}
handler.unlink(taskFile).catch(error => warn('deleting task failure', { error }))
} catch (error) {
warn('failure handling task', { error })
}
try {
const vmDir = getVmBackupDir(String(await handler.readFile(taskFile)))
try {
await adapter.cleanVm(vmDir, { merge: true, logInfo: info, logWarn: warn, remove: true })
} catch (error) {
// consider the clean successful if the VM dir is missing
if (error.code !== 'ENOENT') {
throw error
}
}
handler.unlink(taskFile).catch(error => warn('deleting task failure', { error }))
} catch (error) {
warn('failure handling task', { error })
}
},
{ concurrency }
)
}
})

View File

@@ -0,0 +1 @@
concurrency = 1

View File

@@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/vatesfr/xen-orchestra.git"
},
"version": "0.39.0",
"version": "0.42.1",
"engines": {
"node": ">=14.18"
},
@@ -17,21 +17,23 @@
"test-integration": "node--test *.integ.mjs"
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"@kldzj/stream-throttle": "^1.1.1",
"@vates/async-each": "^1.0.0",
"@vates/cached-dns.lookup": "^1.0.0",
"@vates/compose": "^2.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.4",
"@vates/fuse-vhd": "^1.0.0",
"@vates/nbd-client": "^1.2.1",
"@vates/fuse-vhd": "^2.0.0",
"@vates/nbd-client": "^2.0.0",
"@vates/parse-duration": "^0.1.1",
"@xen-orchestra/async-map": "^0.1.2",
"@xen-orchestra/fs": "^4.0.1",
"@xen-orchestra/fs": "^4.1.0",
"@xen-orchestra/log": "^0.6.0",
"@xen-orchestra/template": "^0.1.0",
"compare-versions": "^5.0.1",
"d3-time-format": "^3.0.0",
"app-conf": "^2.3.0",
"compare-versions": "^6.0.0",
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",
"golike-defer": "^0.5.1",
"limit-concurrency-decorator": "^0.5.0",
@@ -40,20 +42,21 @@
"parse-pairs": "^2.0.0",
"promise-toolbox": "^0.21.0",
"proper-lockfile": "^4.1.2",
"tar": "^6.1.15",
"uuid": "^9.0.0",
"vhd-lib": "^4.5.0",
"xen-api": "^1.3.3",
"vhd-lib": "^4.6.0",
"xen-api": "^1.3.6",
"yazl": "^2.5.1"
},
"devDependencies": {
"fs-extra": "^11.1.0",
"rimraf": "^5.0.1",
"sinon": "^15.0.1",
"sinon": "^16.0.0",
"test": "^3.2.1",
"tmp": "^0.2.1"
},
"peerDependencies": {
"@xen-orchestra/xapi": "^2.2.1"
"@xen-orchestra/xapi": "^3.1.0"
},
"license": "AGPL-3.0-or-later",
"author": {

View File

@@ -18,7 +18,7 @@
"preferGlobal": true,
"dependencies": {
"golike-defer": "^0.5.1",
"xen-api": "^1.3.3"
"xen-api": "^1.3.6"
},
"scripts": {
"postversion": "npm publish"

View File

@@ -42,7 +42,7 @@
"test": "node--test"
},
"devDependencies": {
"sinon": "^15.0.1",
"sinon": "^16.0.0",
"test": "^3.2.1"
}
}

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "@xen-orchestra/fs",
"version": "4.0.1",
"version": "4.1.0",
"license": "AGPL-3.0-or-later",
"description": "The File System for Xen Orchestra backups.",
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/fs",
@@ -25,11 +25,12 @@
"@aws-sdk/lib-storage": "^3.54.0",
"@aws-sdk/middleware-apply-body-checksum": "^3.58.0",
"@aws-sdk/node-http-handler": "^3.54.0",
"@aws-sdk/s3-request-presigner": "^3.421.0",
"@sindresorhus/df": "^3.1.1",
"@vates/async-each": "^1.0.0",
"@vates/coalesce-calls": "^0.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/read-chunk": "^1.1.1",
"@vates/read-chunk": "^1.2.0",
"@xen-orchestra/log": "^0.6.0",
"bind-property-descriptor": "^2.0.0",
"decorator-synchronized": "^0.6.0",
@@ -53,7 +54,7 @@
"cross-env": "^7.0.2",
"dotenv": "^16.0.0",
"rimraf": "^5.0.1",
"sinon": "^15.0.4",
"sinon": "^16.0.0",
"test": "^3.3.0",
"tmp": "^0.2.1"
},

View File

@@ -189,7 +189,7 @@ export default class RemoteHandlerAbstract {
* @param {number} [options.dirMode]
* @param {(this: RemoteHandlerAbstract, path: string) => Promise<undefined>} [options.validator] Function that will be called before the data is commited to the remote, if it fails, file should not exist
*/
async outputStream(path, input, { checksum = true, dirMode, validator } = {}) {
async outputStream(path, input, { checksum = true, dirMode, maxStreamLength, streamLength, validator } = {}) {
path = normalizePath(path)
let checksumStream
@@ -201,6 +201,8 @@ export default class RemoteHandlerAbstract {
}
await this._outputStream(path, input, {
dirMode,
maxStreamLength,
streamLength,
validator,
})
if (checksum) {

View File

@@ -5,6 +5,7 @@ import {
CreateMultipartUploadCommand,
DeleteObjectCommand,
GetObjectCommand,
GetObjectLockConfigurationCommand,
HeadObjectCommand,
ListObjectsV2Command,
PutObjectCommand,
@@ -16,21 +17,22 @@ import { NodeHttpHandler } from '@aws-sdk/node-http-handler'
import { getApplyMd5BodyChecksumPlugin } from '@aws-sdk/middleware-apply-body-checksum'
import { Agent as HttpAgent } from 'http'
import { Agent as HttpsAgent } from 'https'
import pRetry from 'promise-toolbox/retry'
import { createLogger } from '@xen-orchestra/log'
import { decorateWith } from '@vates/decorate-with'
import { PassThrough, pipeline } from 'stream'
import { PassThrough, Transform, pipeline } from 'stream'
import { parse } from 'xo-remote-parser'
import copyStreamToBuffer from './_copyStreamToBuffer.js'
import guessAwsRegion from './_guessAwsRegion.js'
import RemoteHandlerAbstract from './abstract'
import { basename, join, split } from './path'
import { asyncEach } from '@vates/async-each'
import { pRetry } from 'promise-toolbox'
// endpoints https://docs.aws.amazon.com/general/latest/gr/s3.html
// limits: https://docs.aws.amazon.com/AmazonS3/latest/dev/qfacts.html
const MAX_PART_SIZE = 1024 * 1024 * 1024 * 5 // 5GB
const MAX_PART_NUMBER = 10000
const MIN_PART_SIZE = 5 * 1024 * 1024
const { warn } = createLogger('xo:fs:s3')
export default class S3Handler extends RemoteHandlerAbstract {
@@ -72,12 +74,47 @@ export default class S3Handler extends RemoteHandlerAbstract {
}),
})
// Workaround for https://github.com/aws/aws-sdk-js-v3/issues/2673
this.#s3.middlewareStack.use(getApplyMd5BodyChecksumPlugin(this.#s3.config))
const parts = split(path)
this.#bucket = parts.shift()
this.#dir = join(...parts)
const WITH_RETRY = [
'_closeFile',
'_copy',
'_getInfo',
'_getSize',
'_list',
'_mkdir',
'_openFile',
'_outputFile',
'_read',
'_readFile',
'_rename',
'_rmdir',
'_truncate',
'_unlink',
'_write',
'_writeFile',
]
WITH_RETRY.forEach(functionName => {
if (this[functionName] !== undefined) {
// adding the retry on the top level mtehod won't
// cover when _functionName are called internally
this[functionName] = pRetry.wrap(this[functionName], {
delays: [100, 200, 500, 1000, 2000],
// these errors should not change on retry
when: err => !['EEXIST', 'EISDIR', 'ENOTEMPTY', 'ENOENT', 'ENOTDIR', 'EISDIR'].includes(err?.code),
onRetry(error) {
warn('retrying method on fs ', {
method: functionName,
attemptNumber: this.attemptNumber,
delay: this.delay,
error,
file: this.arguments?.[0],
})
},
})
}
})
}
get type() {
@@ -186,18 +223,41 @@ export default class S3Handler extends RemoteHandlerAbstract {
}
}
async _outputStream(path, input, { validator }) {
async _outputStream(path, input, { maxStreamLength, streamLength, validator }) {
const maxInputLength = streamLength ?? maxStreamLength
let partSize
if (maxInputLength === undefined) {
warn(`Writing ${path} to a S3 remote without a max size set will cut it to 50GB`, { path })
partSize = MIN_PART_SIZE // min size for S3
} else {
partSize = Math.min(Math.max(Math.ceil(maxInputLength / MAX_PART_NUMBER), MIN_PART_SIZE), MAX_PART_SIZE)
}
// esnure we d'ont try to upload a stream to big for this part size
let readCounter = 0
const streamCutter = new Transform({
transform(chunk, encoding, callback) {
const MAX_SIZE = MAX_PART_NUMBER * partSize
readCounter += chunk.length
if (readCounter > MAX_SIZE) {
callback(new Error(`read ${readCounter} bytes, maximum size allowed is ${MAX_SIZE} `))
} else {
callback(null, chunk)
}
},
})
// Workaround for "ReferenceError: ReadableStream is not defined"
// https://github.com/aws/aws-sdk-js-v3/issues/2522
const Body = new PassThrough()
pipeline(input, Body, () => {})
pipeline(input, streamCutter, Body, () => {})
const upload = new Upload({
client: this.#s3,
params: {
...this.#createParams(path),
Body,
},
partSize,
leavePartsOnError: false,
})
await upload.done()
@@ -212,21 +272,6 @@ export default class S3Handler extends RemoteHandlerAbstract {
}
}
// some objectstorage provider like backblaze, can answer a 500/503 routinely
// in this case we should retry, and let their load balancing do its magic
// https://www.backblaze.com/b2/docs/calling.html#error_handling
@decorateWith(pRetry.wrap, {
delays: [100, 200, 500, 1000, 2000],
when: e => e.$metadata?.httpStatusCode === 500,
onRetry(error) {
warn('retrying writing file', {
attemptNumber: this.attemptNumber,
delay: this.delay,
error,
file: this.arguments[0],
})
},
})
async _writeFile(file, data, options) {
return this.#s3.send(
new PutObjectCommand({
@@ -396,6 +441,21 @@ export default class S3Handler extends RemoteHandlerAbstract {
async _closeFile(fd) {}
async _sync() {
await super._sync()
try {
const res = await this.#s3.send(new GetObjectLockConfigurationCommand({ Bucket: this.#bucket }))
if (res.ObjectLockConfiguration?.ObjectLockEnabled === 'Enabled') {
// Workaround for https://github.com/aws/aws-sdk-js-v3/issues/2673
// increase memory consumption in outputStream as if buffer the streams
this.#s3.middlewareStack.use(getApplyMd5BodyChecksumPlugin(this.#s3.config))
}
} catch (error) {
if (error.Code !== 'ObjectLockConfigurationNotFoundError') {
throw error
}
}
}
useVhdDirectory() {
return true
}

View File

@@ -0,0 +1,258 @@
import fs from 'fs/promises'
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
import { createHash } from "crypto";
import {
CompleteMultipartUploadCommand,
CreateMultipartUploadCommand,
GetObjectLockConfigurationCommand,
PutObjectCommand,
S3Client,
UploadPartCommand,
} from '@aws-sdk/client-s3'
import { NodeHttpHandler } from '@aws-sdk/node-http-handler'
import { Agent as HttpAgent } from 'http'
import { Agent as HttpsAgent } from 'https'
import { parse } from 'xo-remote-parser'
import { join, split } from './dist/path.js'
import guessAwsRegion from './dist/_guessAwsRegion.js'
import { PassThrough } from 'stream'
import { readChunk } from '@vates/read-chunk'
import { pFromCallback } from 'promise-toolbox'
async function v2(url, inputStream){
const {
allowUnauthorized,
host,
path,
username,
password,
protocol,
region = guessAwsRegion(host),
} = parse(url)
const client = new S3Client({
apiVersion: '2006-03-01',
endpoint: `${protocol}://s3.us-east-2.amazonaws.com`,
forcePathStyle: true,
credentials: {
accessKeyId: username,
secretAccessKey: password,
},
region,
requestHandler: new NodeHttpHandler({
socketTimeout: 600000,
httpAgent: new HttpAgent({
keepAlive: true,
}),
httpsAgent: new HttpsAgent({
rejectUnauthorized: !allowUnauthorized,
keepAlive: true,
}),
}),
})
const pathParts = split(path)
const bucket = pathParts.shift()
const dir = join(...pathParts)
const command = new CreateMultipartUploadCommand({
Bucket: bucket, Key: join(dir, 'flov2')
})
const multipart = await client.send(command)
console.log({multipart})
const parts = []
// monitor memory usage
const intervalMonitorMemoryUsage = setInterval(()=>console.log(Math.round(process.memoryUsage().rss/1024/1024)), 2000)
const CHUNK_SIZE = Math.ceil(5*1024*1024*1024*1024/10000) // smallest chunk allowing 5TB upload
async function read(inputStream, maxReadSize){
if(maxReadSize === 0){
return null
}
process.stdout.write('+')
const chunk = await readChunk(inputStream, maxReadSize)
process.stdout.write('@')
return chunk
}
async function write(data, chunkStream, remainingBytes){
const ready = chunkStream.write(data)
if(!ready){
process.stdout.write('.')
await pFromCallback(cb=> chunkStream.once('drain', cb))
process.stdout.write('@')
}
remainingBytes -= data.length
process.stdout.write(remainingBytes+' ')
return remainingBytes
}
async function uploadChunk(inputStream){
const PartNumber = parts.length +1
let done = false
let remainingBytes = CHUNK_SIZE
const maxChunkPartSize = Math.round(CHUNK_SIZE / 1000)
const chunkStream = new PassThrough()
console.log({maxChunkPartSize,CHUNK_SIZE})
let data
let chunkBuffer = []
const hash = createHash('md5');
try{
while((data = await read(inputStream, Math.min(remainingBytes, maxChunkPartSize))) !== null){
chunkBuffer.push(data)
hash.update(data)
remainingBytes -= data.length
//remainingBytes = await write(data, chunkStream, remainingBytes)
}
console.log('data put')
const fullBuffer = Buffer.alloc(maxChunkPartSize,0)
done = remainingBytes > 0
// add padding at the end of the file (not a problem for tar like : xva/ova)
// if not content length will not match and we'll have UND_ERR_REQ_CONTENT_LENGTH_MISMATCH error
console.log('full padding')
while(remainingBytes > maxChunkPartSize){
chunkBuffer.push(fullBuffer)
hash.update(fullBuffer)
remainingBytes -= maxChunkPartSize
//remainingBytes = await write(fullBuffer,chunkStream, remainingBytes)
}
console.log('full padding done ')
chunkBuffer.push(Buffer.alloc(remainingBytes,0))
hash.update(Buffer.alloc(remainingBytes,0))
console.log('md5 ok ')
//await write(Buffer.alloc(remainingBytes,0),chunkStream, remainingBytes)
// wait for the end of the upload
const command = new UploadPartCommand({
...multipart,
PartNumber,
ContentLength:CHUNK_SIZE,
Body: chunkStream,
ContentMD5 : hash.digest('base64')
})
const promise = client.send(command)
for (const buffer of chunkBuffer){
await write(buffer, chunkStream, remainingBytes)
}
chunkStream.on('error', err => console.error(err))
const res = await promise
console.log({res, headers : res.headers })
parts.push({ ETag:/*res.headers.get('etag') */res.ETag, PartNumber })
}catch(err){
console.error(err)
throw err
}
return done
}
while(!await uploadChunk(inputStream)){
console.log('uploaded one chunk', parts.length)
}
// mark the upload as complete and ask s3 to glue the chunk together
const completRes = await client.send(
new CompleteMultipartUploadCommand({
...multipart,
MultipartUpload: { Parts: parts },
})
)
console.log({completRes})
clearInterval(intervalMonitorMemoryUsage)
}
async function simplePut(url , inputStream){
const {
allowUnauthorized,
host,
path,
username,
password,
protocol,
region = guessAwsRegion(host),
} = parse(url)
const client = new S3Client({
apiVersion: '2006-03-01',
endpoint: `${protocol}://s3.us-east-2.amazonaws.com`,
forcePathStyle: true,
credentials: {
accessKeyId: username,
secretAccessKey: password,
},
region,
requestHandler: new NodeHttpHandler({
socketTimeout: 600000,
httpAgent: new HttpAgent({
keepAlive: true,
}),
httpsAgent: new HttpsAgent({
rejectUnauthorized: !allowUnauthorized,
keepAlive: true,
}),
}),
})
const pathParts = split(path)
const bucket = pathParts.shift()
const dir = join(...pathParts)
//const hasObjectLock = await client.send(new GetObjectLockConfigurationCommand({Bucket: bucket}))
//console.log(hasObjectLock.ObjectLockConfiguration?.ObjectLockEnabled === 'Enabled')
const md5 = await createMD5('/tmp/1g')
console.log({md5})
const command = new PutObjectCommand({
Bucket: bucket, Key: join(dir, 'simple'),
ContentMD5: md5,
ContentLength: 1024*1024*1024,
Body: inputStream
})
const intervalMonitorMemoryUsage = setInterval(()=>console.log(Math.round(process.memoryUsage().rss/1024/1024)), 2000)
const res = await client.send(command)
/*
const presignedUrl = await getSignedUrl(client, command,{ expiresIn: 3600 });
const res = await fetch(presignedUrl, {
method: 'PUT',
body:inputStream,
duplex: "half",
headers:{
"x-amz-decoded-content-length": 1024*1024*1024,
"content-md5" : md5
}
})*/
clearInterval(intervalMonitorMemoryUsage)
console.log(res)
}
async function createMD5(filePath) {
const input = await fs.open(filePath) // big ass file
return new Promise((res, rej) => {
const hash = createHash('md5');
const rStream = input.createReadStream(filePath);
rStream.on('data', (data) => {
hash.update(data);
});
rStream.on('end', () => {
res(hash.digest('base64'));
});
})
}
const input = await fs.open('/tmp/1g') // big ass file
const inputStream = input.createReadStream()
const remoteUrl = ""
v2(remoteUrl,inputStream)
//simplePut(remoteUrl,inputStream)

View File

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

View File

@@ -2,8 +2,19 @@
## **next**
- Ability to migrate selected VMs to another host (PR [#7040](https://github.com/vatesfr/xen-orchestra/pull/7040))
- Ability to snapshot selected VMs (PR [#7021](https://github.com/vatesfr/xen-orchestra/pull/7021))
- Add Patches to Pool Dashboard (PR [#6709](https://github.com/vatesfr/xen-orchestra/pull/6709))
## **0.1.3** (2023-09-01)
- Add Alarms to Pool Dashboard (PR [#6976](https://github.com/vatesfr/xen-orchestra/pull/6976))
## **0.1.2** (2023-07-28)
- Ability to export selected VMs as CSV file (PR [#6915](https://github.com/vatesfr/xen-orchestra/pull/6915))
- [Pool/VMs] Ability to export selected VMs as JSON file (PR [#6911](https://github.com/vatesfr/xen-orchestra/pull/6911))
- Add Tasks to Pool Dashboard (PR [#6713](https://github.com/vatesfr/xen-orchestra/pull/6713))
## **0.1.1** (2023-07-03)

View File

@@ -0,0 +1,144 @@
<!-- TOC -->
- [XenApiCollection](#xenapicollection)
- [Get the collection](#get-the-collection)
- [Defer the subscription](#defer-the-subscription)
- [Create a dedicated collection](#create-a-dedicated-collection)
- [Alter the collection](#alter-the-collection)
_ [Example 1: Adding props to records](#example-1-adding-props-to-records)
_ [Example 2: Adding props to the collection](#example-2-adding-props-to-the-collection) \* [Example 3, filtering and sorting the collection](#example-3-filtering-and-sorting-the-collection)
<!-- TOC -->
# XenApiCollection
## Get the collection
To retrieve a collection, invoke `useXenApiCollection("VM")`.
By doing this, the current component will be automatically subscribed to the collection and will be updated when the
collection changes.
When the component is unmounted, the subscription will be automatically stopped.
## Defer the subscription
If you don't want to fetch the data of the collection when the component is mounted, you can pass `{ immediate: false }`
as options: `const { start, isStarted } = useXenApiCollection("VM", { immediate: false })`.
Then you subscribe to the collection by calling `start()`.
## Create a dedicated collection
It is recommended to create a dedicated collection composable for each type of record you want to use.
They are stored in `src/composables/xen-api-collection/*-collection.composable.ts`.
```typescript
// src/composables/xen-api-collection/console-collection.composable.ts
export const useConsoleCollection = () => useXenApiCollection("console");
```
If you want to allow the user to defer the subscription, you can propagate the options to `useXenApiCollection`.
```typescript
// console-collection.composable.ts
export const useConsoleCollection = <
Immediate extends boolean = true,
>(options?: {
immediate?: Immediate;
}) => useXenApiCollection("console", options);
```
```typescript
// MyComponent.vue
const collection = useConsoleCollection({ immediate: false });
setTimeout(() => collection.start(), 10000);
```
## Alter the collection
You can alter the collection by overriding parts of it.
### Example 1: Adding props to records
```typescript
// xen-api.ts
export interface XenApiConsole extends XenApiRecord<"console"> {
// ... existing props
someProp: string;
someOtherProp: number;
}
```
```typescript
// console-collection.composable.ts
export const useConsoleCollection = () => {
const collection = useXenApiCollection("console");
const records = computed(() => {
return collection.records.value.map((console) => ({
...console,
someProp: "Some value",
someOtherProp: 42,
}));
});
return {
...collection,
records,
};
};
```
```typescript
const consoleCollection = useConsoleCollection();
consoleCollection.getByUuid("...").someProp; // "Some value"
```
### Example 2: Adding props to the collection
```typescript
// vm-collection.composable.ts
export const useVmCollection = () => {
const collection = useXenApiCollection("VM");
return {
...collection,
runningVms: computed(() =>
collection.records.value.filter(
(vm) => vm.power_state === POWER_STATE.RUNNING
)
),
};
};
```
### Example 3, filtering and sorting the collection
```typescript
// vm-collection.composable.ts
export const useVmCollection = () => {
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
)
.sort((vm1, vm2) => vm1.name_label.localeCompare(vm2.name_label))
),
};
};
```

View File

@@ -1,144 +0,0 @@
# Stores for XenApiRecord collections
All collections of `XenApiRecord` are stored inside the `xapiCollectionStore`.
To retrieve a collection, invoke `useXapiCollectionStore().get(type)`.
## Accessing a collection
In order to use a collection, you'll need to subscribe to it.
```typescript
const consoleStore = useXapiCollectionStore().get("console");
const { records, getByUuid /* ... */ } = consoleStore.subscribe();
```
## Deferred subscription
If you wish to initialize the subscription on demand, you can pass `{ immediate: false }` as options to `subscribe()`.
```typescript
const consoleStore = useXapiCollectionStore().get("console");
const { records, start, isStarted /* ... */ } = consoleStore.subscribe({
immediate: false,
});
// Later, you can then use start() to initialize the subscription.
```
## Create a dedicated store for a collection
To create a dedicated store for a specific `XenApiRecord`, simply return the collection from the XAPI Collection Store:
```typescript
export const useConsoleStore = defineStore("console", () =>
useXapiCollectionStore().get("console")
);
```
## Extending the base Subscription
To extend the base Subscription, you'll need to override the `subscribe` method.
For that, you can use the `createSubscribe<XenApiRecord, Extensions>((options) => { /* ... */})` helper.
### Define the extensions
Subscription extensions are defined as `(object | [object, RequiredOptions])[]`.
When using a tuple (`[object, RequiredOptions]`), the corresponding `object` type will be added to the subscription if
the `RequiredOptions` for that tuple are present in the options passed to `subscribe`.
```typescript
// Always present extension
type DefaultExtension = {
propA: string;
propB: ComputedRef<number>;
};
// Conditional extension 1
type FirstConditionalExtension = [
{ propC: ComputedRef<string> }, // <- This signature will be added
{ optC: string } // <- if this condition is met
];
// Conditional extension 2
type SecondConditionalExtension = [
{ propD: () => void }, // <- This signature will be added
{ optD: number } // <- if this condition is met
];
// Create the extensions array
type Extensions = [
DefaultExtension,
FirstConditionalExtension,
SecondConditionalExtension
];
```
### Define the subscription
```typescript
export const useConsoleStore = defineStore("console", () => {
const consoleCollection = useXapiCollectionStore().get("console");
const subscribe = createSubscribe<XenApiConsole, Extensions>((options) => {
const originalSubscription = consoleCollection.subscribe(options);
const extendedSubscription = {
propA: "Some string",
propB: computed(() => 42),
};
const propCSubscription = options?.optC !== undefined && {
propC: computed(() => "Some other string"),
};
const propDSubscription = options?.optD !== undefined && {
propD: () => console.log("Hello"),
};
return {
...originalSubscription,
...extendedSubscription,
...propCSubscription,
...propDSubscription,
};
});
return {
...consoleCollection,
subscribe,
};
});
```
The generated `subscribe` method will then automatically have the following `options` signature:
```typescript
type Options = {
immediate?: false;
optC?: string;
optD?: number;
};
```
### Use the subscription
In each case, all the default properties (`records`, `getByUuid`, etc.) will be present.
```typescript
const store = useConsoleStore();
// No options (propA and propB will be present)
const subscription = store.subscribe();
// optC option (propA, propB and propC will be present)
const subscription = store.subscribe({ optC: "Hello" });
// optD option (propA, propB and propD will be present)
const subscription = store.subscribe({ optD: 12 });
// optC and optD options (propA, propB, propC and propD will be present)
const subscription = store.subscribe({ optC: "Hello", optD: 12 });
```

View File

@@ -1,10 +1,10 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<title>XO Lite</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,6 +1,6 @@
{
"name": "@xen-orchestra/lite",
"version": "0.1.1",
"version": "0.1.3",
"scripts": {
"dev": "GIT_HEAD=$(git rev-parse HEAD) vite",
"build": "run-p type-check build-only",
@@ -22,7 +22,7 @@
"@types/marked": "^4.0.8",
"@vueuse/core": "^10.1.2",
"@vueuse/math": "^10.1.2",
"complex-matcher": "^0.7.0",
"complex-matcher": "^0.7.1",
"d3-time-format": "^4.1.0",
"decorator-synchronized": "^0.6.0",
"echarts": "^5.3.3",
@@ -49,7 +49,7 @@
"@rushstack/eslint-patch": "^1.1.0",
"@types/node": "^16.11.41",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.1.3",
"eslint-plugin-vue": "^9.0.0",

View File

@@ -4,10 +4,10 @@
<AppLogin />
</div>
<div v-else>
<AppHeader />
<AppHeader v-if="uiStore.hasUi" />
<div style="display: flex">
<AppNavigation />
<main class="main">
<AppNavigation v-if="uiStore.hasUi" />
<main class="main" :class="{ 'no-ui': !uiStore.hasUi }">
<RouterView />
</main>
</div>
@@ -23,8 +23,8 @@ import AppNavigation from "@/components/AppNavigation.vue";
import AppTooltips from "@/components/AppTooltips.vue";
import UnreachableHostsModal from "@/components/UnreachableHostsModal.vue";
import { useChartTheme } from "@/composables/chart-theme.composable";
import { usePoolStore } from "@/stores/pool.store";
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";
@@ -41,10 +41,10 @@ if (link == null) {
}
link.href = favicon;
document.title = "XO Lite";
const xenApiStore = useXenApiStore();
const { pool } = usePoolStore().subscribe();
const { pool } = usePoolCollection();
useChartTheme();
const uiStore = useUiStore();
@@ -74,10 +74,8 @@ if (import.meta.env.DEV) {
whenever(
() => pool.value?.$ref,
async (poolRef) => {
const xenApi = xenApiStore.getXapi();
await xenApi.injectWatchEvent(poolRef);
await xenApi.startWatch();
(poolRef) => {
xenApiStore.getXapi().startWatching(poolRef);
}
);
</script>
@@ -92,5 +90,9 @@ whenever(
flex: 1;
height: calc(100vh - 8rem);
background-color: var(--background-color-secondary);
&.no-ui {
height: 100vh;
}
}
</style>

View File

@@ -46,3 +46,51 @@ code * {
.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);
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -0,0 +1,346 @@
<svg width="345" height="254" viewBox="0 0 345 254" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2254_118640)">
<path opacity="0.1"
d="M112.532 161.16C123.943 156.653 141.854 155.734 158.158 156.095C209.369 157.221 255.596 168.175 295.58 180.566C310.056 185.053 324.307 189.888 333.704 196.065C352.831 208.64 347.157 226.06 320.561 236.419C311.511 239.945 300.541 242.656 289.278 245.039C269.249 249.3 247.65 252.717 224.921 253.701C208.715 254.394 192.307 253.829 176.197 252.861C131.179 250.146 86.9952 244.182 49.9595 233.931C33.7148 229.444 18.405 223.864 11.7176 216.6C5.03021 209.336 9.30546 200.139 25.8933 195.839C32.7572 194.063 40.9546 193.259 48.9854 192.507C60.8044 191.398 72.813 190.301 83.4783 188.046C94.4965 185.716 107.541 181.209 105.361 176.052C103.011 170.518 101.135 165.66 112.532 161.16Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M164.531 9.89007L163.286 82.4315C163.286 82.4315 208.365 82.4315 246.718 94.9375L247.467 21.6346C247.467 21.6346 185.95 6.88994 164.531 9.89007Z"
fill="black"/>
<path
d="M163.878 8.90472L162.632 81.4461C162.632 81.4461 207.712 81.4461 246.065 93.9521L246.813 20.6492C246.813 20.6492 185.296 5.90459 163.878 8.90472Z"
fill="#F5F5FB"/>
<path opacity="0.1"
d="M145.332 9.83393L146.577 82.3753C146.577 82.3753 101.498 82.3754 63.1447 94.8814L62.3962 21.5981C62.3962 21.5981 123.913 6.8338 145.332 9.83393Z"
fill="black"/>
<path
d="M145.986 8.84956L147.231 81.391C147.231 81.391 102.151 81.391 63.7985 93.897L63.05 20.6137C63.05 20.6137 124.567 5.84943 145.986 8.84956Z"
fill="#F5F5FB"/>
<path opacity="0.1"
d="M258.348 31.9775V103.761C258.348 103.761 273.056 102.011 303.924 140.278L306.667 68.4944C306.667 68.4944 274.537 33.7303 258.348 31.9775Z"
fill="black"/>
<path
d="M257.694 31.3213V103.104C257.694 103.104 272.403 101.355 303.271 139.621L306.013 67.8382C306.013 67.8382 273.883 33.0741 257.694 31.3213Z"
fill="#F5F5FB"/>
<path opacity="0.1"
d="M52.3162 34.1074V105.894C52.3162 105.894 37.6078 104.144 6.73976 142.411L4.00073 70.6276C4.00073 70.6276 36.1271 35.8635 52.3162 34.1074Z"
fill="black"/>
<path
d="M52.9697 33.123V104.91C52.9697 104.91 38.2613 103.16 7.39333 141.426L4.6543 69.6432C4.6543 69.6432 36.7807 34.8791 52.9697 33.123Z"
fill="#F5F5FB"/>
<path
d="M218.358 154.169C218.358 156.614 202.894 170.745 167.695 170.745C132.497 170.745 113.438 157.107 113.438 154.661C113.438 152.216 133.15 158.272 168.349 158.272C203.548 158.272 218.358 151.72 218.358 154.169Z"
fill="#F7F7FD"/>
<path opacity="0.1"
d="M103.322 120.488C105.39 120.488 107.067 120.257 107.067 119.972C107.067 119.688 105.39 119.457 103.322 119.457C101.253 119.457 99.5759 119.688 99.5759 119.972C99.5759 120.257 101.253 120.488 103.322 120.488Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M88.976 121.949C91.0447 121.949 92.7217 121.718 92.7217 121.433C92.7217 121.149 91.0447 120.918 88.976 120.918C86.9072 120.918 85.2302 121.149 85.2302 121.433C85.2302 121.718 86.9072 121.949 88.976 121.949Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M72.0841 119.972C74.1528 119.972 75.8299 119.741 75.8299 119.457C75.8299 119.172 74.1528 118.941 72.0841 118.941C70.0154 118.941 68.3384 119.172 68.3384 119.457C68.3384 119.741 70.0154 119.972 72.0841 119.972Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M94.7187 125.96C96.1881 125.96 97.3793 125.795 97.3793 125.592C97.3793 125.389 96.1881 125.225 94.7187 125.225C93.2493 125.225 92.0581 125.389 92.0581 125.592C92.0581 125.795 93.2493 125.96 94.7187 125.96Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M79.8305 125.592C81.2999 125.592 82.4911 125.427 82.4911 125.224C82.4911 125.021 81.2999 124.856 79.8305 124.856C78.3611 124.856 77.1699 125.021 77.1699 125.224C77.1699 125.427 78.3611 125.592 79.8305 125.592Z"
fill="#2CA878"/>
<path
d="M88.8974 121.318C88.8974 121.318 94.1467 104.03 88.0737 97.1072C83.5305 91.9276 78.3662 92.5348 76.0782 93.1913C75.4806 93.351 74.9247 93.6392 74.449 94.0359C73.9732 94.4327 73.589 94.9284 73.3229 95.489C72.5155 97.2385 72.6691 100.084 78.3629 103.393C87.9038 108.94 88.4954 116.552 88.4954 116.552L88.8974 121.318Z"
fill="#2CA878"/>
<path d="M76.8889 95.6396C76.8889 95.6396 92.3687 100.501 88.8975 121.318" stroke="#565987" stroke-miterlimit="10"/>
<path d="M85.9297 97.3828C85.9297 97.3828 83.7986 98.8238 84.4752 100.993" stroke="#565987" stroke-miterlimit="10"/>
<path d="M78.4414 98.6663C78.4414 98.6663 79.7488 97.3337 81.3275 98.0689" stroke="#565987" stroke-miterlimit="10"/>
<path d="M84.5764 105.359C84.5764 105.359 86.8644 104.194 87.456 105.655" stroke="#565987" stroke-miterlimit="10"/>
<path d="M90.2213 107.791C90.2213 107.791 88.5021 107.653 88.4727 108.477" stroke="#565987" stroke-miterlimit="10"/>
<path
d="M89.11 121.456C89.11 121.456 82.9587 108.891 81.2656 109.167C80.4648 109.298 80.0954 110.089 79.9255 110.887C79.7152 111.916 79.8295 112.985 80.2523 113.946C81.1381 115.955 83.4816 119.779 89.11 121.456Z"
fill="#2CA878"/>
<path d="M81.3079 111.55C81.3079 111.55 87.7632 120.951 89.0118 121.364" stroke="#565987" stroke-miterlimit="10"/>
<path d="M80.7229 113.017H82.2297" stroke="#565987" stroke-miterlimit="10"/>
<path d="M82.5271 116.457L84.887 116.559" stroke="#565987" stroke-miterlimit="10"/>
<path d="M83.9652 113.706L83.707 114.94" stroke="#565987" stroke-miterlimit="10"/>
<path d="M86.4754 117.465L86.4297 118.587" stroke="#565987" stroke-miterlimit="10"/>
<path
d="M88.927 121.456C88.927 121.456 95.0751 108.891 96.7715 109.167C97.5755 109.298 97.9449 110.089 98.1116 110.887C98.3203 111.916 98.2061 112.985 97.7847 113.946C96.899 115.955 94.5554 119.779 88.927 121.456Z"
fill="#2CA878"/>
<path d="M96.719 111.55C96.719 111.55 90.2636 120.951 89.0183 121.364" stroke="#565987" stroke-miterlimit="10"/>
<path d="M97.3139 113.017H95.8071" stroke="#565987" stroke-miterlimit="10"/>
<path d="M95.5096 116.457L93.1465 116.559" stroke="#565987" stroke-miterlimit="10"/>
<path d="M94.0715 113.706L94.3298 114.94" stroke="#565987" stroke-miterlimit="10"/>
<path d="M91.5613 117.465L91.607 118.587" stroke="#565987" stroke-miterlimit="10"/>
<path
d="M78.3662 124.39C78.1986 124.253 78.0766 124.067 78.0165 123.858C77.9889 123.754 77.9983 123.643 78.043 123.545C78.0877 123.447 78.165 123.367 78.2616 123.32C78.4839 123.231 78.7257 123.389 78.9153 123.546C78.993 123.638 79.089 123.713 79.197 123.766C79.3051 123.818 79.4229 123.847 79.5429 123.852C79.4205 123.74 79.3288 123.599 79.2766 123.441C79.2245 123.284 79.2138 123.115 79.2454 122.952C79.2581 122.885 79.2874 122.822 79.3304 122.768C79.4546 122.634 79.6802 122.693 79.8305 122.798C80.3044 123.126 80.4352 123.783 80.4385 124.364C80.464 124.146 80.464 123.925 80.4385 123.707C80.4287 123.599 80.4457 123.491 80.4879 123.391C80.5301 123.292 80.5962 123.204 80.6803 123.136C80.7927 123.075 80.9188 123.045 81.0464 123.047C81.1524 123.032 81.2605 123.04 81.3635 123.069C81.4666 123.098 81.5623 123.149 81.6445 123.218C81.7227 123.326 81.7628 123.458 81.7587 123.591C81.7545 123.725 81.7063 123.854 81.6217 123.957C81.4453 124.156 81.2364 124.324 81.0039 124.452C80.8261 124.557 80.6751 124.702 80.5627 124.876C80.5485 124.899 80.5375 124.925 80.53 124.951H79.1899C78.8925 124.8 78.6157 124.612 78.3662 124.39Z"
fill="#2CA878"/>
<path
d="M101.802 119.329C101.634 119.193 101.512 119.009 101.452 118.801C101.424 118.696 101.433 118.585 101.478 118.486C101.523 118.387 101.6 118.307 101.697 118.259C101.817 118.236 101.94 118.245 102.055 118.285C102.17 118.326 102.272 118.396 102.351 118.489C102.428 118.581 102.524 118.656 102.632 118.708C102.74 118.76 102.858 118.788 102.978 118.791C102.856 118.68 102.764 118.538 102.712 118.381C102.66 118.223 102.649 118.055 102.681 117.892C102.692 117.825 102.722 117.762 102.766 117.711C102.89 117.577 103.116 117.632 103.266 117.737C103.74 118.066 103.871 118.722 103.874 119.306C103.899 119.088 103.899 118.868 103.874 118.65C103.864 118.542 103.881 118.433 103.923 118.334C103.966 118.234 104.032 118.147 104.116 118.079C104.228 118.017 104.354 117.988 104.482 117.993C104.588 117.977 104.696 117.984 104.799 118.014C104.903 118.043 104.998 118.094 105.08 118.164C105.158 118.272 105.199 118.402 105.195 118.536C105.19 118.669 105.142 118.797 105.057 118.899C104.881 119.099 104.672 119.266 104.439 119.395C104.262 119.501 104.111 119.646 103.998 119.818C103.984 119.842 103.973 119.867 103.965 119.894H102.625C102.327 119.743 102.05 119.553 101.802 119.329Z"
fill="#2CA878"/>
<path
d="M93.9899 124.39C93.8222 124.253 93.7003 124.067 93.6402 123.858C93.6133 123.754 93.6229 123.643 93.6676 123.545C93.7122 123.447 93.7891 123.368 93.8853 123.32C94.1108 123.231 94.3494 123.389 94.539 123.546C94.6168 123.638 94.7128 123.713 94.8208 123.765C94.9289 123.818 95.0466 123.847 95.1666 123.852C95.0445 123.74 94.9532 123.598 94.9017 123.441C94.8501 123.283 94.84 123.115 94.8724 122.952C94.8824 122.885 94.9107 122.821 94.9541 122.768C95.0783 122.634 95.3038 122.693 95.4542 122.798C95.9281 123.126 96.0621 123.783 96.0654 124.364C96.0892 124.146 96.0892 123.925 96.0654 123.707C96.0557 123.599 96.0727 123.491 96.1149 123.391C96.1571 123.292 96.2232 123.204 96.3073 123.136C96.4613 123.065 96.6312 123.035 96.8002 123.049C96.9692 123.064 97.1316 123.122 97.2715 123.218C97.3507 123.326 97.3915 123.457 97.3874 123.591C97.3832 123.725 97.3344 123.854 97.2486 123.957C97.0729 124.157 96.8639 124.324 96.6309 124.452C96.4544 124.559 96.3037 124.704 96.1896 124.876C96.1768 124.9 96.1658 124.925 96.1569 124.951H94.8201C94.5201 124.802 94.241 124.613 93.9899 124.39Z"
fill="#2CA878"/>
<path
d="M71.8292 114.162C71.8292 114.162 71.3291 114.819 72.0612 115.803C72.7934 116.788 73.4013 117.638 73.1562 118.259C73.1562 118.259 72.0482 116.411 71.1493 116.384C70.2505 116.358 70.8486 115.272 71.8292 114.162Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M71.8292 114.162C71.7854 114.226 71.7512 114.295 71.7279 114.369C70.8487 115.403 70.3813 116.371 71.2245 116.394C72.0123 116.417 72.9569 117.832 73.1857 118.186C73.1789 118.214 73.1702 118.242 73.1595 118.268C73.1595 118.268 72.0515 116.42 71.1526 116.394C70.2538 116.368 70.8487 115.272 71.8292 114.162Z"
fill="black"/>
<path
d="M70.9075 115C70.9075 115.233 70.9336 115.42 70.9663 115.42C70.999 115.42 71.0219 115.233 71.0219 115C71.0219 114.767 70.9925 114.878 70.9598 114.878C70.9271 114.878 70.9075 114.77 70.9075 115Z"
fill="#FFD037"/>
<path
d="M70.5873 115.278C70.7899 115.39 70.9664 115.455 70.9828 115.426C70.9991 115.396 70.8455 115.285 70.6559 115.176C70.4663 115.068 70.5317 115.144 70.5186 115.176C70.5056 115.209 70.3846 115.183 70.5873 115.278Z"
fill="#FFD037"/>
<path
d="M74.4897 114.162C74.4897 114.162 74.9931 114.819 74.2577 115.803C73.5222 116.788 72.9208 117.638 73.166 118.259C73.166 118.259 74.2707 116.411 75.1729 116.384C76.075 116.358 75.4801 115.272 74.4897 114.162Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M74.4897 114.162C74.5336 114.226 74.5678 114.295 74.5911 114.369C75.4703 115.403 75.9377 116.371 75.0944 116.394C74.3067 116.417 73.3654 117.832 73.1333 118.186C73.14 118.214 73.1488 118.242 73.1595 118.268C73.1595 118.268 74.2642 116.42 75.1663 116.394C76.0684 116.368 75.4801 115.272 74.4897 114.162Z"
fill="black"/>
<path
d="M75.4247 115C75.4247 115.233 75.4018 115.42 75.3691 115.42C75.3364 115.42 75.3103 115.233 75.3103 115C75.3103 114.767 75.343 114.878 75.3757 114.878C75.4084 114.878 75.4247 114.77 75.4247 115Z"
fill="#FFD037"/>
<path
d="M75.7514 115.278C75.5488 115.39 75.3723 115.455 75.3592 115.426C75.3461 115.396 75.4932 115.285 75.6861 115.176C75.8789 115.068 75.8103 115.144 75.8266 115.176C75.843 115.209 75.9443 115.183 75.7514 115.278Z"
fill="#FFD037"/>
<path
d="M75.2646 118.183C75.2646 118.183 73.8591 118.141 73.4374 117.839C73.0158 117.537 71.2802 117.182 71.1756 117.658C71.071 118.134 69.0674 120.071 70.6494 120.084C72.2314 120.097 74.3298 119.834 74.7514 119.578C75.173 119.322 75.2646 118.183 75.2646 118.183Z"
fill="#A8A8A8"/>
<path opacity="0.2"
d="M70.6231 119.913C72.2051 119.913 74.3035 119.667 74.7251 119.407C75.052 119.21 75.1729 118.505 75.2154 118.18H75.2645C75.2645 118.18 75.1729 119.315 74.7513 119.575C74.3297 119.834 72.2345 120.093 70.6493 120.08C70.195 120.08 70.0348 119.913 70.0446 119.67C70.1067 119.821 70.2832 119.91 70.6231 119.913Z"
fill="black"/>
<path opacity="0.1"
d="M63.7919 164.62C70.0595 164.62 75.1403 163.866 75.1403 162.936C75.1403 162.006 70.0595 161.252 63.7919 161.252C57.5244 161.252 52.4436 162.006 52.4436 162.936C52.4436 163.866 57.5244 164.62 63.7919 164.62Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M95.4902 178.563C101.758 178.563 106.839 177.809 106.839 176.879C106.839 175.949 101.758 175.195 95.4902 175.195C89.2227 175.195 84.1418 175.949 84.1418 176.879C84.1418 177.809 89.2227 178.563 95.4902 178.563Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M48.8613 194.427C55.1288 194.427 60.2096 193.673 60.2096 192.743C60.2096 191.813 55.1288 191.06 48.8613 191.06C42.5938 191.06 37.5129 191.813 37.5129 192.743C37.5129 193.673 42.5938 194.427 48.8613 194.427Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M289.311 229.274C295.578 229.274 300.659 228.52 300.659 227.59C300.659 226.66 295.578 225.906 289.311 225.906C283.043 225.906 277.962 226.66 277.962 227.59C277.962 228.52 283.043 229.274 289.311 229.274Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M160.514 189.687C164.664 189.687 168.029 189.309 168.029 188.844C168.029 188.378 164.664 188 160.514 188C156.364 188 153 188.378 153 188.844C153 189.309 156.364 189.687 160.514 189.687Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M268.049 146.36C272.199 146.36 275.563 145.982 275.563 145.516C275.563 145.051 272.199 144.673 268.049 144.673C263.899 144.673 260.535 145.051 260.535 145.516C260.535 145.982 263.899 146.36 268.049 146.36Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M209.539 210.367C213.689 210.367 217.053 209.989 217.053 209.523C217.053 209.057 213.689 208.68 209.539 208.68C205.389 208.68 202.025 209.057 202.025 209.523C202.025 209.989 205.389 210.367 209.539 210.367Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M144.678 204.799C148.828 204.799 152.193 204.422 152.193 203.956C152.193 203.49 148.828 203.112 144.678 203.112C140.528 203.112 137.164 203.49 137.164 203.956C137.164 204.422 140.528 204.799 144.678 204.799Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M83.8935 202.61C88.0436 202.61 91.4078 202.232 91.4078 201.766C91.4078 201.301 88.0436 200.923 83.8935 200.923C79.7434 200.923 76.3792 201.301 76.3792 201.766C76.3792 202.232 79.7434 202.61 83.8935 202.61Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M68.0542 217.722C72.2042 217.722 75.5685 217.345 75.5685 216.879C75.5685 216.413 72.2042 216.035 68.0542 216.035C63.9041 216.035 60.5398 216.413 60.5398 216.879C60.5398 217.345 63.9041 217.722 68.0542 217.722Z"
fill="#2CA878"/>
<g opacity="0.1">
<path opacity="0.1"
d="M166.303 100.898C193.763 100.898 216.024 90.9207 216.024 78.6134C216.024 66.3061 193.763 56.3291 166.303 56.3291C138.843 56.3291 116.582 66.3061 116.582 78.6134C116.582 90.9207 138.843 100.898 166.303 100.898Z"
fill="black"/>
<path opacity="0.1"
d="M216.887 86.5803C214.687 98.2001 192.951 107.322 166.457 107.322C139.73 107.322 117.844 98.0392 115.968 86.2783H115.781V105.215C124.521 114.386 143.871 120.763 166.332 120.763C188.794 120.763 208.143 114.386 216.887 105.215V86.5803Z"
fill="black"/>
<path opacity="0.1"
d="M216.887 109.623C214.687 121.239 192.951 130.364 166.457 130.364C139.73 130.364 117.844 121.082 115.968 109.317H115.781V128.257C124.521 137.428 143.871 143.802 166.332 143.802C188.794 143.802 208.143 137.428 216.887 128.257V109.623Z"
fill="black"/>
<path opacity="0.1"
d="M216.887 134.966C214.687 146.586 192.951 155.708 166.457 155.708C139.73 155.708 117.844 146.428 115.968 134.664H115.781V155.087C115.78 155.522 115.869 155.953 116.043 156.352C116.216 156.751 116.47 157.109 116.788 157.405C125.94 165.844 144.698 169.149 166.332 169.149C187.967 169.149 206.715 165.844 215.88 157.405C216.198 157.109 216.452 156.751 216.625 156.352C216.799 155.953 216.888 155.522 216.887 155.087V134.966Z"
fill="black"/>
</g>
<path
d="M166.303 99.5853C193.763 99.5853 216.024 89.6082 216.024 77.3009C216.024 64.9936 193.763 55.0166 166.303 55.0166C138.843 55.0166 116.582 64.9936 116.582 77.3009C116.582 89.6082 138.843 99.5853 166.303 99.5853Z"
fill="#2CA878"/>
<path
d="M216.887 85.2678C214.687 96.8876 192.951 106.009 166.457 106.009C139.73 106.009 117.844 96.7267 115.968 84.9658H115.781V103.902C124.521 113.073 143.871 119.451 166.332 119.451C188.794 119.451 208.143 113.073 216.887 103.902V85.2678Z"
fill="#2CA878"/>
<path
d="M216.887 108.31C214.687 119.927 192.951 129.052 166.457 129.052C139.73 129.052 117.844 119.769 115.968 108.005H115.781V126.944C124.521 136.115 143.871 142.49 166.332 142.49C188.794 142.49 208.143 136.115 216.887 126.944V108.31Z"
fill="#2CA878"/>
<path
d="M216.887 133.654C214.687 145.273 192.951 154.395 166.457 154.395C139.73 154.395 117.844 145.116 115.968 133.352H115.781V153.775C115.78 154.21 115.869 154.641 116.043 155.039C116.216 155.438 116.47 155.797 116.788 156.092C125.94 164.531 144.698 167.837 166.332 167.837C187.967 167.837 206.715 164.531 215.88 156.092C216.198 155.797 216.452 155.438 216.625 155.039C216.799 154.641 216.888 154.21 216.887 153.775V133.654Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M239.593 190.876C249.882 190.876 258.224 189.368 258.224 187.508C258.224 185.648 249.882 184.141 239.593 184.141C229.304 184.141 220.962 185.648 220.962 187.508C220.962 189.368 229.304 190.876 239.593 190.876Z"
fill="#2CA878"/>
<path
d="M229.134 144.597C230.075 145.155 231.553 145.52 233.412 144.318L233.396 146.98C233.527 148.805 235.776 155.459 235.776 155.459L237.361 160.931C238.499 164.798 239.085 168.807 239.103 172.839C239.103 174.172 239.227 175.551 239.609 176.588C240.456 178.886 239.283 181.84 238.769 182.93C237.911 183.221 237.038 183.467 236.155 183.668C236.076 183.687 235.998 183.711 235.923 183.74V183.724C235.923 183.724 233.543 184.672 232.484 184.541C231.696 184.446 231.203 185.434 231.83 186.159C231.318 186.316 230.784 186.387 230.248 186.369C229.189 186.238 228.66 188.063 230.644 188.585C232.628 189.107 243.868 188.585 243.868 188.585C244.184 188.426 244.438 188.167 244.591 187.847C244.743 187.527 244.786 187.166 244.712 186.819L246.117 186.76C246.117 186.76 248.101 185.848 246.117 183.369L246.009 183.448L245.588 182.194V180.504C245.588 180.504 248.101 168.359 247.307 166.938C246.513 165.516 246.091 158.183 246.091 158.183L246.369 150.24C247.085 148.756 247.451 146.426 247.637 144.604C247.908 143.736 248.078 142.839 248.144 141.932C248.176 141.502 248.202 141.056 248.219 140.59C248.23 139.567 248.066 138.55 247.735 137.583C248.632 136.246 249.635 134.984 250.732 133.808C251.189 133.382 251.536 132.85 251.744 132.26C251.951 131.669 252.014 131.037 251.925 130.417L251.272 125.983L249.683 118.433C249.882 116.336 249.448 115.128 248.964 114.445C249.078 113.766 249.186 113.09 249.268 112.407C249.618 109.448 249.461 106.45 248.804 103.544C248.575 102.533 248.261 101.493 247.552 100.725C246.751 99.8515 245.45 99.323 245.15 98.184C245.107 97.8937 245.046 97.6064 244.967 97.324C244.676 96.5789 243.761 96.3065 242.956 96.208C241.525 96.0341 239.456 95.9028 238.08 96.4673C236.872 97.0036 235.878 97.9303 235.256 99.0998C234.946 99.5804 234.829 100.161 234.929 100.725C234.978 100.92 235.054 101.108 235.154 101.283C234.696 102.53 234.746 103.908 235.293 105.118C235.84 106.329 236.84 107.274 238.077 107.749C238.109 107.851 238.139 107.949 238.162 108.044C238.222 108.278 238.266 108.516 238.292 108.757C238.266 109.216 238.237 109.679 238.217 110.142L238.168 110.329C237.03 110.835 235.921 111.402 234.844 112.029C235.635 113.204 234.71 118.161 234.71 118.161C232.726 119.333 230.745 122.333 232.86 125.464C234.975 128.596 233.785 131.983 233.785 131.983L233.458 137.084L233.393 138.105H233.416L233.393 138.492H233.448V138.909C232.52 139.254 231.16 139.943 230.353 141.223C229.97 141.851 229.505 142.424 228.97 142.927C228.846 143.034 228.749 143.17 228.687 143.323C228.626 143.475 228.603 143.641 228.619 143.805C228.635 143.969 228.69 144.127 228.78 144.264C228.869 144.402 228.991 144.517 229.134 144.597ZM245.101 131.048C245.021 130.165 245.184 129.277 245.571 128.481H246.506C246.534 128.688 246.572 128.894 246.62 129.098C246.889 130.263 245.816 130.811 245.101 131.048Z"
fill="url(#paint0_linear_2254_118640)"/>
<path
d="M245.362 181.597L245.872 183.129L244.218 184.79L238.747 186.084L234.802 185.7L235.056 184.295C235.132 183.992 235.288 183.717 235.509 183.498C235.73 183.279 236.006 183.125 236.308 183.054C237.943 182.673 241.022 181.8 241.672 180.589L245.362 181.597Z"
fill="#DB8B8B"/>
<path opacity="0.1"
d="M245.362 181.597L245.872 183.129L244.218 184.79L238.747 186.084L234.802 185.7L235.056 184.295C235.132 183.992 235.288 183.717 235.509 183.498C235.73 183.279 236.006 183.125 236.308 183.054C237.943 182.673 241.022 181.8 241.672 180.589L245.362 181.597Z"
fill="black"/>
<path
d="M236.073 185.302L236.838 184.281L236.073 183.096C236.073 183.096 233.785 184.025 232.765 183.897C231.745 183.769 231.239 185.558 233.148 186.07C235.056 186.582 245.872 186.07 245.872 186.07C245.872 186.07 247.781 185.174 245.872 182.745L244.218 184.025C244.218 184.025 241.417 185.558 240.018 185.302H236.073Z"
fill="#C17174"/>
<path
d="M232.641 187.475L236.586 187.859L242.058 186.579L243.711 184.918L243.577 184.514L243.388 183.943L243.201 183.385L239.505 182.364C239.317 182.67 239.061 182.928 238.756 183.116C237.557 183.94 235.406 184.534 234.141 184.829C234.088 184.842 234.035 184.859 233.984 184.879C233.738 184.957 233.514 185.093 233.331 185.276C233.11 185.493 232.953 185.768 232.879 186.07L232.641 187.475Z"
fill="#DB8B8B"/>
<path opacity="0.1"
d="M232.641 187.475L236.586 187.859L242.058 186.579L243.711 184.918L243.577 184.515L242.058 185.686C242.058 185.686 239.256 187.219 237.857 186.963H233.909L234.674 185.943L233.988 184.879C233.741 184.958 233.517 185.094 233.334 185.276C233.113 185.494 232.957 185.769 232.883 186.071L232.641 187.475Z"
fill="black"/>
<path
d="M233.909 187.091L234.674 186.07L233.909 184.885C233.909 184.885 231.621 185.814 230.601 185.686C229.582 185.558 229.075 187.347 230.984 187.859C232.893 188.371 243.708 187.859 243.708 187.859C243.708 187.859 245.617 186.963 243.708 184.537L242.054 185.814C242.054 185.814 239.253 187.347 237.854 187.091H233.909Z"
fill="#C17174"/>
<path
d="M237.089 112.974C237.089 112.974 243.198 113.63 242.319 111.057C242.068 110.332 242.003 109.555 242.129 108.799C242.287 107.862 242.656 106.973 243.208 106.202L237.482 107.095C237.826 107.671 238.088 108.293 238.259 108.943C238.93 111.654 237.089 112.974 237.089 112.974Z"
fill="#DB8B8B"/>
<path
d="M247.526 142.1C247.526 142.1 247.398 147.594 246.127 150.279L245.872 158.075C245.872 158.075 246.251 165.231 247.016 166.636C247.781 168.041 245.362 179.926 245.362 179.926V181.971C245.362 181.971 241.289 183.504 240.132 181.715L241.276 166.508L240.766 145.425L247.526 142.1Z"
fill="#474463"/>
<path opacity="0.1"
d="M247.526 142.1C247.526 142.1 247.398 147.594 246.127 150.279L245.872 158.075C245.872 158.075 246.251 165.231 247.016 166.636C247.781 168.041 245.362 179.926 245.362 179.926V181.971C245.362 181.971 241.289 183.504 240.132 181.715L241.276 166.508L240.766 145.425L247.526 142.1Z"
fill="black"/>
<path
d="M250.069 124.476L250.834 126.521L251.487 130.863C251.572 131.469 251.513 132.087 251.313 132.665C251.114 133.243 250.78 133.765 250.34 134.188C248.814 135.721 246.777 138.915 246.777 138.915L244.362 131.631C244.362 131.631 246.777 131.247 246.395 129.586C246.012 127.925 246.143 124.86 246.143 124.86L250.069 124.476Z"
fill="#FF748E"/>
<path opacity="0.05"
d="M250.098 124.446L250.86 126.491L251.514 130.837C251.6 131.442 251.541 132.059 251.342 132.637C251.143 133.215 250.81 133.737 250.37 134.159C248.84 135.692 246.807 138.889 246.807 138.889L244.372 131.595C244.372 131.595 246.791 131.211 246.408 129.55C246.026 127.89 246.153 124.82 246.153 124.82L250.098 124.446Z"
fill="black"/>
<path opacity="0.1"
d="M238.76 183.129C240.394 185.709 242.934 184.245 243.385 183.956L243.198 183.398L239.505 182.364C239.32 182.675 239.065 182.937 238.76 183.129Z"
fill="black"/>
<path
d="M233.654 147.095C233.782 148.884 235.942 155.4 235.942 155.4L237.469 160.763C238.561 164.556 239.125 168.482 239.145 172.429C239.107 173.672 239.272 174.913 239.632 176.102C240.652 178.915 238.616 182.746 238.616 182.746C240.397 185.943 243.45 183.77 243.45 183.77V181.725C243.45 181.725 245.869 170.607 245.231 168.434C244.594 166.261 244.342 160.386 244.342 160.386V149.777C246.38 147.738 247.619 145.03 247.833 142.149C247.863 141.729 247.889 141.289 247.905 140.836C248.006 137.954 246.042 134.573 245.29 133.368C245.107 133.076 244.99 132.912 244.99 132.912L233.756 137.389H233.729V137.832V138.797L233.654 147.095Z"
fill="#474463"/>
<path opacity="0.1"
d="M237.472 107.095C237.816 107.671 238.078 108.293 238.25 108.943C238.872 109.18 239.535 109.289 240.199 109.264C240.864 109.24 241.517 109.081 242.12 108.799C242.277 107.862 242.646 106.973 243.198 106.202L237.472 107.095Z"
fill="black"/>
<path
d="M240.018 109.012C242.758 109.012 244.98 106.781 244.98 104.03C244.98 101.278 242.758 99.0469 240.018 99.0469C237.278 99.0469 235.056 101.278 235.056 104.03C235.056 106.781 237.278 109.012 240.018 109.012Z"
fill="#DB8B8B"/>
<path opacity="0.1"
d="M233.654 138.787H233.71C239.044 139.89 245.745 134.307 245.745 134.307C245.565 134.002 245.41 133.684 245.28 133.355C245.097 133.063 244.98 132.898 244.98 132.898L233.746 137.376L233.716 137.822L233.654 138.787Z"
fill="black"/>
<path
d="M235.057 112.846C235.057 112.846 242.182 108.5 244.47 110.289L247.653 114.379C247.653 114.379 249.69 115.019 249.311 119.122L250.837 126.534L246.261 127.683C246.261 127.683 243.591 130.24 245.754 133.946C245.754 133.946 239.008 139.569 233.661 138.416L234.043 132.413C234.043 132.413 235.187 129.088 233.154 126.022C231.121 122.956 233.027 120.015 234.936 118.866C234.929 118.853 235.818 113.995 235.057 112.846Z"
fill="#FF748E"/>
<path
d="M234.674 138.915C234.674 138.915 232 139.427 230.729 141.472C230.361 142.087 229.913 142.649 229.395 143.143C229.279 143.249 229.189 143.381 229.133 143.528C229.077 143.675 229.056 143.834 229.071 143.991C229.087 144.148 229.139 144.299 229.223 144.432C229.307 144.565 229.421 144.677 229.555 144.758C230.565 145.368 232.209 145.742 234.292 144.026C237.871 141.088 234.674 138.915 234.674 138.915Z"
fill="#DB8B8B"/>
<path opacity="0.1"
d="M238.619 115.275C238.619 115.275 241.162 115.019 241.799 121.662C242.437 128.306 243.453 129.842 243.453 129.842C243.453 129.842 244.091 133.292 242.561 135.337C241.031 137.382 238.109 141.728 238.109 141.728C238.109 141.728 234.929 142.621 234.292 139.43L238.237 132.019C238.237 132.019 238.871 129.461 237.344 127.673C235.818 125.884 234.674 113.995 238.619 115.275Z"
fill="black"/>
<path
d="M238.364 114.892C238.364 114.892 240.907 114.636 241.544 121.283C242.182 127.929 243.198 129.459 243.198 129.459C243.198 129.459 243.852 132.909 242.309 134.954C240.767 136.999 237.871 141.345 237.871 141.345C237.871 141.345 234.69 142.237 234.053 139.047L237.998 131.635C237.998 131.635 238.635 129.078 237.106 127.289C235.576 125.5 234.419 113.615 238.364 114.892Z"
fill="#FF748E"/>
<path opacity="0.1"
d="M236.263 119.684C236.263 119.684 236.011 123.645 237.155 125.818C238.299 127.991 238.299 130.801 237.155 132.334"
fill="black"/>
<path opacity="0.1"
d="M235.628 114.826C235.628 114.826 236.645 113.805 239.574 113.677C242.502 113.549 243.77 112.656 243.77 112.656"
fill="black"/>
<path opacity="0.1"
d="M242.705 97.5896C243.479 97.6848 244.362 97.9507 244.64 98.6794C244.716 98.9567 244.776 99.2384 244.82 99.5229C245.107 100.639 246.359 101.164 247.13 102.011C247.81 102.766 248.111 103.784 248.333 104.778C248.967 107.626 249.118 110.561 248.781 113.46C248.533 115.577 248.023 117.665 247.882 119.792C247.742 121.919 248 124.161 249.157 125.95C249.657 126.718 250.314 127.394 250.667 128.247C249.938 128.841 248.931 128.927 247.99 128.96C246.901 128.997 245.806 128.997 244.705 128.96C244.129 128.983 243.556 128.862 243.038 128.608C242.672 128.374 242.37 128.05 242.162 127.666C241.535 126.6 241.378 125.316 241.404 124.079C241.43 122.841 241.626 121.61 241.649 120.37C241.714 119.693 241.564 119.013 241.221 118.426C240.905 118.058 240.547 117.727 240.155 117.442C238.792 116.227 238.325 114.284 238.279 112.456C238.233 110.627 238.534 108.799 238.381 106.977C238.322 106.265 238.054 105.408 237.358 105.27C237.138 105.261 236.919 105.233 236.704 105.185C236.145 104.968 236.2 104.177 236.05 103.596C235.87 102.94 235.315 102.438 235.158 101.781C234.792 100.235 236.753 98.3872 238.037 97.8423C239.322 97.2975 241.329 97.4189 242.705 97.5896Z"
fill="black"/>
<path
d="M242.832 97.3331C243.607 97.4283 244.489 97.6942 244.767 98.4261C244.844 98.7023 244.904 98.9829 244.947 99.2664C245.235 100.382 246.487 100.908 247.258 101.755C247.938 102.509 248.238 103.527 248.461 104.522C249.094 107.373 249.245 110.312 248.905 113.213C248.66 115.331 248.15 117.418 248.01 119.548C247.869 121.679 248.127 123.914 249.284 125.703C249.784 126.474 250.441 127.147 250.794 128.001C250.066 128.598 249.059 128.68 248.118 128.713C247.028 128.748 245.933 128.748 244.833 128.713C244.257 128.737 243.684 128.618 243.166 128.365C242.799 128.129 242.498 127.804 242.29 127.42C241.662 126.353 241.505 125.07 241.531 123.832C241.558 122.595 241.754 121.364 241.777 120.126C241.777 119.45 241.737 118.734 241.348 118.183C241.033 117.814 240.676 117.483 240.283 117.198C238.92 115.984 238.452 114.041 238.407 112.212C238.361 110.384 238.662 108.556 238.508 106.734C238.449 106.022 238.181 105.165 237.485 105.027C237.265 105.018 237.046 104.99 236.831 104.942C236.272 104.725 236.328 103.934 236.178 103.353C235.998 102.697 235.442 102.198 235.285 101.538C234.919 99.9919 236.88 98.1439 238.165 97.599C239.449 97.0541 241.466 97.1624 242.832 97.3331Z"
fill="#464353"/>
<path
d="M41.8503 188.007C41.8503 188.007 45.3542 187.899 46.4067 187.144C47.4591 186.389 51.7932 185.486 52.0547 186.697C52.3162 187.909 57.317 192.717 53.3621 192.75C49.4072 192.783 44.1775 192.13 43.1218 191.486C42.0661 190.843 41.8503 188.007 41.8503 188.007Z"
fill="#A8A8A8"/>
<path opacity="0.2"
d="M53.434 192.32C49.4824 192.353 44.2494 191.703 43.197 191.06C42.3962 190.568 42.0759 188.808 41.968 187.994H41.8503C41.8503 187.994 42.0726 190.83 43.1251 191.474C44.1775 192.117 49.4105 192.767 53.3654 192.737C54.5061 192.737 54.8983 192.32 54.8787 191.716C54.7186 192.094 54.2838 192.32 53.434 192.32Z"
fill="black"/>
<path
d="M264.401 141.84C264.401 141.84 266.804 141.764 267.529 141.246C268.255 140.727 271.226 140.11 271.406 140.941C271.586 141.771 275.021 145.073 272.305 145.096C269.589 145.119 266 144.669 265.277 144.229C264.555 143.79 264.401 141.84 264.401 141.84Z"
fill="#A8A8A8"/>
<path opacity="0.2"
d="M272.354 144.808C269.641 144.827 266.049 144.381 265.326 143.941C264.774 143.613 264.555 142.395 264.483 141.837H264.401C264.401 141.837 264.555 143.783 265.277 144.227C266 144.67 269.592 145.113 272.305 145.093C273.089 145.093 273.36 144.808 273.344 144.391C273.236 144.647 272.939 144.801 272.354 144.808Z"
fill="black"/>
<path opacity="0.1"
d="M10.2273 14.1997C14.1319 14.1997 17.2971 11.021 17.2971 7.09987C17.2971 3.17872 14.1319 0 10.2273 0C6.32274 0 3.15747 3.17872 3.15747 7.09987C3.15747 11.021 6.32274 14.1997 10.2273 14.1997Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M314.201 34.5503C318.105 34.5503 321.271 31.3716 321.271 27.4505C321.271 23.5293 318.105 20.3506 314.201 20.3506C310.296 20.3506 307.131 23.5293 307.131 27.4505C307.131 31.3716 310.296 34.5503 314.201 34.5503Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M5.22965 40.1043C8.11791 40.1043 10.4593 37.753 10.4593 34.8525C10.4593 31.9519 8.11791 29.6006 5.22965 29.6006C2.34139 29.6006 0 31.9519 0 34.8525C0 37.753 2.34139 40.1043 5.22965 40.1043Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M58.1798 10.5633C61.0681 10.5633 63.4095 8.21196 63.4095 5.31144C63.4095 2.41091 61.0681 0.0595703 58.1798 0.0595703C55.2916 0.0595703 52.9502 2.41091 52.9502 5.31144C52.9502 8.21196 55.2916 10.5633 58.1798 10.5633Z"
fill="#2CA878"/>
<path opacity="0.1"
d="M272.595 11.2196C275.484 11.2196 277.825 8.86821 277.825 5.96769C277.825 3.06716 275.484 0.71582 272.595 0.71582C269.707 0.71582 267.366 3.06716 267.366 5.96769C267.366 8.86821 269.707 11.2196 272.595 11.2196Z"
fill="#2CA878"/>
<path
d="M290.775 212.176C290.775 212.176 292.083 213.896 290.17 216.492C288.258 219.089 286.68 221.278 287.317 222.896C287.317 222.896 290.203 218.078 292.547 218.009C294.89 217.94 293.357 215.077 290.775 212.176Z"
fill="#FC6681"/>
<path opacity="0.1"
d="M290.775 212.176C290.889 212.343 290.978 212.525 291.04 212.717C293.328 215.422 294.557 217.946 292.347 218.009C290.295 218.068 287.833 221.754 287.242 222.686C287.261 222.758 287.285 222.828 287.314 222.896C287.314 222.896 290.2 218.078 292.543 218.009C294.887 217.94 293.357 215.077 290.775 212.176Z"
fill="black"/>
<path
d="M293.207 214.368C293.207 214.972 293.138 215.464 293.056 215.464C292.975 215.464 292.906 214.972 292.906 214.368C292.906 213.764 292.991 214.04 293.073 214.04C293.154 214.04 293.207 213.761 293.207 214.368Z"
fill="#FFD037"/>
<path
d="M294.044 215.09C293.514 215.379 293.063 215.553 293.011 215.481C292.959 215.409 293.37 215.113 293.897 214.824C294.423 214.536 294.224 214.746 294.259 214.824C294.295 214.903 294.573 214.802 294.044 215.09Z"
fill="#FFD037"/>
<path
d="M283.859 212.176C283.859 212.176 282.551 213.896 284.463 216.492C286.376 219.089 287.954 221.278 287.317 222.896C287.317 222.896 284.427 218.078 282.087 218.009C279.747 217.94 281.273 215.077 283.859 212.176Z"
fill="#FC6681"/>
<path opacity="0.1"
d="M283.859 212.176C283.745 212.343 283.655 212.525 283.591 212.717C281.303 215.422 280.077 217.946 282.283 218.009C284.336 218.068 286.801 221.754 287.389 222.686C287.369 222.755 287.346 222.827 287.32 222.896C287.32 222.896 284.431 218.078 282.091 218.009C279.75 217.94 281.273 215.077 283.859 212.176Z"
fill="black"/>
<path
d="M281.424 214.368C281.424 214.972 281.492 215.464 281.577 215.464C281.662 215.464 281.728 214.972 281.728 214.368C281.728 213.764 281.643 214.04 281.558 214.04C281.473 214.04 281.424 213.761 281.424 214.368Z"
fill="#FFD037"/>
<path
d="M280.59 215.09C281.12 215.379 281.571 215.553 281.62 215.481C281.669 215.409 281.264 215.113 280.734 214.824C280.205 214.536 280.407 214.746 280.375 214.824C280.342 214.903 280.061 214.802 280.59 215.09Z"
fill="#FFD037"/>
<path
d="M281.842 222.673C281.842 222.673 285.503 222.558 286.605 221.77C287.706 220.982 292.236 220.04 292.511 221.304C292.785 222.568 298.008 227.596 293.877 227.629C289.746 227.662 284.274 226.973 283.176 226.316C282.078 225.66 281.842 222.673 281.842 222.673Z"
fill="#A8A8A8"/>
<path opacity="0.2"
d="M293.952 227.19C289.818 227.222 284.349 226.533 283.248 225.877C282.411 225.364 282.074 223.523 281.963 222.673H281.842C281.842 222.673 282.074 225.627 283.176 226.31C284.277 226.993 289.746 227.662 293.877 227.623C295.07 227.623 295.482 227.186 295.459 226.556C295.296 226.943 294.838 227.183 293.952 227.19Z"
fill="black"/>
<g opacity="0.7">
<path
d="M103.433 41.3486C102.075 41.3486 100.748 41.7529 99.6194 42.5103C98.4906 43.2677 97.6109 44.3443 97.0913 45.6038C96.5718 46.8634 96.4359 48.2494 96.7007 49.5865C96.9656 50.9236 97.6193 52.1518 98.5792 53.1159C99.5392 54.0799 100.762 54.7364 102.094 55.0023C103.425 55.2683 104.805 55.1318 106.059 54.6101C107.314 54.0884 108.386 53.2049 109.14 52.0713C109.894 50.9377 110.297 49.605 110.297 48.2417C110.297 46.4136 109.574 44.6603 108.286 43.3676C106.999 42.0749 105.253 41.3486 103.433 41.3486ZM103.433 42.6846C104.532 42.6846 105.606 43.0118 106.52 43.625C107.434 44.2381 108.146 45.1096 108.566 46.1293C108.987 47.1489 109.097 48.2709 108.882 49.3533C108.668 50.4358 108.139 51.43 107.362 52.2104C106.585 52.9908 105.595 53.5223 104.517 53.7376C103.439 53.9529 102.322 53.8424 101.306 53.42C100.291 52.9977 99.4233 52.2825 98.8127 51.3648C98.2021 50.4472 97.8763 49.3683 97.8763 48.2647C97.8763 46.7848 98.4617 45.3654 99.5037 44.319C100.546 43.2725 101.959 42.6846 103.433 42.6846ZM107.319 46.3116L106.695 45.6781C106.664 45.6464 106.628 45.6211 106.587 45.6038C106.547 45.5866 106.503 45.5777 106.459 45.5777C106.415 45.5777 106.372 45.5866 106.332 45.6038C106.291 45.6211 106.255 45.6464 106.224 45.6781L102.302 49.5809L100.648 47.8938C100.617 47.862 100.581 47.8367 100.54 47.8195C100.5 47.8022 100.457 47.7933 100.413 47.7933C100.369 47.7933 100.325 47.8022 100.285 47.8195C100.244 47.8367 100.208 47.862 100.177 47.8938L99.5497 48.5207C99.5181 48.5513 99.493 48.588 99.4758 48.6287C99.4586 48.6693 99.4497 48.7129 99.4497 48.7571C99.4497 48.8012 99.4586 48.8448 99.4758 48.8854C99.493 48.9261 99.5181 48.9628 99.5497 48.9934L102.067 51.5405C102.097 51.5723 102.134 51.5976 102.174 51.6148C102.214 51.6321 102.258 51.641 102.302 51.641C102.346 51.641 102.389 51.6321 102.43 51.6148C102.47 51.5976 102.507 51.5723 102.537 51.5405L107.319 46.7745C107.351 46.7439 107.376 46.7071 107.393 46.6665C107.41 46.6259 107.419 46.5823 107.419 46.5381C107.419 46.494 107.41 46.4503 107.393 46.4097C107.376 46.3691 107.351 46.3324 107.319 46.3018V46.3116Z"
fill="#2CA878"/>
<path
d="M103.433 41.3486C102.075 41.3486 100.748 41.7529 99.6194 42.5103C98.4906 43.2677 97.6109 44.3443 97.0913 45.6038C96.5718 46.8634 96.4359 48.2494 96.7007 49.5865C96.9656 50.9236 97.6193 52.1518 98.5792 53.1159C99.5392 54.0799 100.762 54.7364 102.094 55.0023C103.425 55.2683 104.805 55.1318 106.059 54.6101C107.314 54.0884 108.386 53.2049 109.14 52.0713C109.894 50.9377 110.297 49.605 110.297 48.2417C110.297 46.4136 109.574 44.6603 108.286 43.3676C106.999 42.0749 105.253 41.3486 103.433 41.3486ZM103.433 42.6846C104.532 42.6846 105.606 43.0118 106.52 43.625C107.434 44.2381 108.146 45.1096 108.566 46.1293C108.987 47.1489 109.097 48.2709 108.882 49.3533C108.668 50.4358 108.139 51.43 107.362 52.2104C106.585 52.9908 105.595 53.5223 104.517 53.7376C103.439 53.9529 102.322 53.8424 101.306 53.42C100.291 52.9977 99.4233 52.2825 98.8127 51.3648C98.2021 50.4472 97.8763 49.3683 97.8763 48.2647C97.8763 46.7848 98.4617 45.3654 99.5037 44.319C100.546 43.2725 101.959 42.6846 103.433 42.6846ZM107.319 46.3116L106.695 45.6781C106.664 45.6464 106.628 45.6211 106.587 45.6038C106.547 45.5866 106.503 45.5777 106.459 45.5777C106.415 45.5777 106.372 45.5866 106.332 45.6038C106.291 45.6211 106.255 45.6464 106.224 45.6781L102.302 49.5809L100.648 47.8938C100.617 47.862 100.581 47.8367 100.54 47.8195C100.5 47.8022 100.457 47.7933 100.413 47.7933C100.369 47.7933 100.325 47.8022 100.285 47.8195C100.244 47.8367 100.208 47.862 100.177 47.8938L99.5497 48.5207C99.5181 48.5513 99.493 48.588 99.4758 48.6287C99.4586 48.6693 99.4497 48.7129 99.4497 48.7571C99.4497 48.8012 99.4586 48.8448 99.4758 48.8854C99.493 48.9261 99.5181 48.9628 99.5497 48.9934L102.067 51.5405C102.097 51.5723 102.134 51.5976 102.174 51.6148C102.214 51.6321 102.258 51.641 102.302 51.641C102.346 51.641 102.389 51.6321 102.43 51.6148C102.47 51.5976 102.507 51.5723 102.537 51.5405L107.319 46.7745C107.351 46.7439 107.376 46.7071 107.393 46.6665C107.41 46.6259 107.419 46.5823 107.419 46.5381C107.419 46.494 107.41 46.4503 107.393 46.4097C107.376 46.3691 107.351 46.3324 107.319 46.3018V46.3116Z"
fill="white" fill-opacity="0.2"/>
</g>
<g opacity="0.7">
<path
d="M27.4458 76.2471C26.1243 76.5554 24.9233 77.2501 23.9947 78.2435C23.0661 79.2368 22.4514 80.4843 22.2284 81.8283C22.0054 83.1722 22.1839 84.5525 22.7416 85.7946C23.2992 87.0367 24.2109 88.085 25.3615 88.807C26.512 89.5291 27.8499 89.8925 29.206 89.8514C30.5622 89.8103 31.8757 89.3665 32.9809 88.576C34.086 87.7856 34.933 86.684 35.4151 85.4104C35.8971 84.1368 35.9924 82.7482 35.6891 81.4202C35.4872 80.5364 35.1136 79.7012 34.5898 78.9626C34.0659 78.224 33.4021 77.5964 32.6364 77.1159C31.8706 76.6353 31.018 76.3112 30.1273 76.1621C29.2366 76.0131 28.3254 76.042 27.4458 76.2471ZM27.7465 77.5601C28.8179 77.309 29.9393 77.383 30.9687 77.7726C31.9981 78.1621 32.8892 78.8498 33.5291 79.7485C34.169 80.6471 34.5289 81.7163 34.5631 82.8205C34.5973 83.9248 34.3044 85.0143 33.7215 85.9512C33.1385 86.8881 32.2917 87.6302 31.2884 88.0833C30.2851 88.5364 29.1705 88.6803 28.0857 88.4967C27.0009 88.313 25.9948 87.8102 25.1949 87.0518C24.3949 86.2934 23.8372 85.3136 23.5922 84.2365C23.2652 82.7982 23.5186 81.2883 24.2971 80.0372C25.0756 78.786 26.3159 77.8954 27.7465 77.5601ZM32.3454 80.2189L31.5936 79.7331C31.5569 79.7091 31.5158 79.6928 31.4727 79.6851C31.4296 79.6774 31.3853 79.6785 31.3427 79.6883C31.3 79.6981 31.2597 79.7164 31.2242 79.7421C31.1887 79.7679 31.1587 79.8005 31.136 79.8381L28.1943 84.5254L26.2038 83.2617C26.1671 83.2377 26.126 83.2214 26.0829 83.2137C26.0397 83.206 25.9955 83.2071 25.9529 83.2169C25.9102 83.2267 25.8699 83.245 25.8344 83.2707C25.7989 83.2965 25.7689 83.3291 25.7462 83.3667L25.2723 84.1216C25.2484 84.1585 25.2322 84.1998 25.2245 84.2431C25.2169 84.2864 25.2179 84.3308 25.2277 84.3737C25.2374 84.4165 25.2557 84.457 25.2813 84.4926C25.3069 84.5283 25.3394 84.5584 25.3769 84.5812L28.397 86.4948C28.4338 86.5193 28.4752 86.5361 28.5186 86.544C28.5621 86.552 28.6067 86.5511 28.6498 86.5413C28.6928 86.5314 28.7335 86.513 28.7693 86.4869C28.805 86.4609 28.8352 86.4278 28.8578 86.3898L32.4532 80.6685C32.4772 80.6319 32.4937 80.5907 32.5016 80.5476C32.5095 80.5044 32.5087 80.4601 32.4992 80.4173C32.4898 80.3744 32.4719 80.3339 32.4466 80.2982C32.4213 80.2624 32.3891 80.2321 32.3519 80.209L32.3454 80.2189Z"
fill="#2CA878"/>
<path
d="M27.4458 76.2471C26.1243 76.5554 24.9233 77.2501 23.9947 78.2435C23.0661 79.2368 22.4514 80.4843 22.2284 81.8283C22.0054 83.1722 22.1839 84.5525 22.7416 85.7946C23.2992 87.0367 24.2109 88.085 25.3615 88.807C26.512 89.5291 27.8499 89.8925 29.206 89.8514C30.5622 89.8103 31.8757 89.3665 32.9809 88.576C34.086 87.7856 34.933 86.684 35.4151 85.4104C35.8971 84.1368 35.9924 82.7482 35.6891 81.4202C35.4872 80.5364 35.1136 79.7012 34.5898 78.9626C34.0659 78.224 33.4021 77.5964 32.6364 77.1159C31.8706 76.6353 31.018 76.3112 30.1273 76.1621C29.2366 76.0131 28.3254 76.042 27.4458 76.2471ZM27.7465 77.5601C28.8179 77.309 29.9393 77.383 30.9687 77.7726C31.9981 78.1621 32.8892 78.8498 33.5291 79.7485C34.169 80.6471 34.5289 81.7163 34.5631 82.8205C34.5973 83.9248 34.3044 85.0143 33.7215 85.9512C33.1385 86.8881 32.2917 87.6302 31.2884 88.0833C30.2851 88.5364 29.1705 88.6803 28.0857 88.4967C27.0009 88.313 25.9948 87.8102 25.1949 87.0518C24.3949 86.2934 23.8372 85.3136 23.5922 84.2365C23.2652 82.7982 23.5186 81.2883 24.2971 80.0372C25.0756 78.786 26.3159 77.8954 27.7465 77.5601ZM32.3454 80.2189L31.5936 79.7331C31.5569 79.7091 31.5158 79.6928 31.4727 79.6851C31.4296 79.6774 31.3853 79.6785 31.3427 79.6883C31.3 79.6981 31.2597 79.7164 31.2242 79.7421C31.1887 79.7679 31.1587 79.8005 31.136 79.8381L28.1943 84.5254L26.2038 83.2617C26.1671 83.2377 26.126 83.2214 26.0829 83.2137C26.0397 83.206 25.9955 83.2071 25.9529 83.2169C25.9102 83.2267 25.8699 83.245 25.8344 83.2707C25.7989 83.2965 25.7689 83.3291 25.7462 83.3667L25.2723 84.1216C25.2484 84.1585 25.2322 84.1998 25.2245 84.2431C25.2169 84.2864 25.2179 84.3308 25.2277 84.3737C25.2374 84.4165 25.2557 84.457 25.2813 84.4926C25.3069 84.5283 25.3394 84.5584 25.3769 84.5812L28.397 86.4948C28.4338 86.5193 28.4752 86.5361 28.5186 86.544C28.5621 86.552 28.6067 86.5511 28.6498 86.5413C28.6928 86.5314 28.7335 86.513 28.7693 86.4869C28.805 86.4609 28.8352 86.4278 28.8578 86.3898L32.4532 80.6685C32.4772 80.6319 32.4937 80.5907 32.5016 80.5476C32.5095 80.5044 32.5087 80.4601 32.4992 80.4173C32.4898 80.3744 32.4719 80.3339 32.4466 80.2982C32.4213 80.2624 32.3891 80.2321 32.3519 80.209L32.3454 80.2189Z"
fill="white" fill-opacity="0.2"/>
</g>
<g opacity="0.7">
<path
d="M203.473 42.3467C202.115 42.3467 200.788 42.751 199.659 43.5084C198.53 44.2658 197.651 45.3423 197.131 46.6019C196.612 47.8614 196.476 49.2474 196.741 50.5845C197.005 51.9217 197.659 53.1499 198.619 54.1139C199.579 55.0779 200.802 55.7344 202.133 56.0004C203.465 56.2664 204.845 56.1298 206.099 55.6081C207.353 55.0864 208.425 54.2029 209.18 53.0693C209.934 51.9358 210.336 50.6031 210.336 49.2398C210.336 47.4116 209.613 45.6583 208.326 44.3656C207.039 43.0729 205.293 42.3467 203.473 42.3467ZM203.473 43.6826C204.572 43.6826 205.646 44.0099 206.56 44.623C207.473 45.2362 208.186 46.1077 208.606 47.1273C209.027 48.1469 209.137 49.2689 208.922 50.3514C208.708 51.4338 208.179 52.4281 207.402 53.2085C206.625 53.9889 205.634 54.5203 204.557 54.7356C203.479 54.9509 202.361 54.8404 201.346 54.4181C200.331 53.9957 199.463 53.2805 198.852 52.3629C198.242 51.4452 197.916 50.3664 197.916 49.2627C197.916 47.7828 198.501 46.3635 199.544 45.317C200.586 44.2705 201.999 43.6826 203.473 43.6826ZM207.359 47.3097L206.735 46.6762C206.704 46.6444 206.668 46.6192 206.627 46.6019C206.587 46.5846 206.543 46.5757 206.499 46.5757C206.455 46.5757 206.412 46.5846 206.371 46.6019C206.331 46.6192 206.294 46.6444 206.264 46.6762L202.342 50.579L200.684 48.9017C200.654 48.8699 200.617 48.8446 200.577 48.8274C200.537 48.8101 200.493 48.8012 200.449 48.8012C200.405 48.8012 200.362 48.8101 200.321 48.8274C200.281 48.8446 200.244 48.8699 200.214 48.9017L199.59 49.5352C199.558 49.5658 199.533 49.6025 199.516 49.6431C199.498 49.6837 199.49 49.7274 199.49 49.7715C199.49 49.8156 199.498 49.8593 199.516 49.8999C199.533 49.9405 199.558 49.9772 199.59 50.0078L202.106 52.555C202.137 52.5868 202.173 52.612 202.214 52.6293C202.254 52.6466 202.298 52.6554 202.342 52.6554C202.386 52.6554 202.429 52.6466 202.469 52.6293C202.51 52.612 202.546 52.5868 202.577 52.555L207.359 47.7889C207.39 47.7583 207.416 47.7216 207.433 47.681C207.45 47.6404 207.459 47.5967 207.459 47.5526C207.459 47.5085 207.45 47.4648 207.433 47.4242C207.416 47.3836 207.39 47.3469 207.359 47.3163V47.3097Z"
fill="#2CA878"/>
<path
d="M203.473 42.3467C202.115 42.3467 200.788 42.751 199.659 43.5084C198.53 44.2658 197.651 45.3423 197.131 46.6019C196.612 47.8614 196.476 49.2474 196.741 50.5845C197.005 51.9217 197.659 53.1499 198.619 54.1139C199.579 55.0779 200.802 55.7344 202.133 56.0004C203.465 56.2664 204.845 56.1298 206.099 55.6081C207.353 55.0864 208.425 54.2029 209.18 53.0693C209.934 51.9358 210.336 50.6031 210.336 49.2398C210.336 47.4116 209.613 45.6583 208.326 44.3656C207.039 43.0729 205.293 42.3467 203.473 42.3467ZM203.473 43.6826C204.572 43.6826 205.646 44.0099 206.56 44.623C207.473 45.2362 208.186 46.1077 208.606 47.1273C209.027 48.1469 209.137 49.2689 208.922 50.3514C208.708 51.4338 208.179 52.4281 207.402 53.2085C206.625 53.9889 205.634 54.5203 204.557 54.7356C203.479 54.9509 202.361 54.8404 201.346 54.4181C200.331 53.9957 199.463 53.2805 198.852 52.3629C198.242 51.4452 197.916 50.3664 197.916 49.2627C197.916 47.7828 198.501 46.3635 199.544 45.317C200.586 44.2705 201.999 43.6826 203.473 43.6826ZM207.359 47.3097L206.735 46.6762C206.704 46.6444 206.668 46.6192 206.627 46.6019C206.587 46.5846 206.543 46.5757 206.499 46.5757C206.455 46.5757 206.412 46.5846 206.371 46.6019C206.331 46.6192 206.294 46.6444 206.264 46.6762L202.342 50.579L200.684 48.9017C200.654 48.8699 200.617 48.8446 200.577 48.8274C200.537 48.8101 200.493 48.8012 200.449 48.8012C200.405 48.8012 200.362 48.8101 200.321 48.8274C200.281 48.8446 200.244 48.8699 200.214 48.9017L199.59 49.5352C199.558 49.5658 199.533 49.6025 199.516 49.6431C199.498 49.6837 199.49 49.7274 199.49 49.7715C199.49 49.8156 199.498 49.8593 199.516 49.8999C199.533 49.9405 199.558 49.9772 199.59 50.0078L202.106 52.555C202.137 52.5868 202.173 52.612 202.214 52.6293C202.254 52.6466 202.298 52.6554 202.342 52.6554C202.386 52.6554 202.429 52.6466 202.469 52.6293C202.51 52.612 202.546 52.5868 202.577 52.555L207.359 47.7889C207.39 47.7583 207.416 47.7216 207.433 47.681C207.45 47.6404 207.459 47.5967 207.459 47.5526C207.459 47.5085 207.45 47.4648 207.433 47.4242C207.416 47.3836 207.39 47.3469 207.359 47.3163V47.3097Z"
fill="white" fill-opacity="0.2"/>
</g>
<g opacity="0.7">
<path
d="M282.221 76.3888C280.868 76.2834 279.514 76.5831 278.331 77.2502C277.147 77.9172 276.187 78.9216 275.572 80.1365C274.957 81.3513 274.714 82.7221 274.874 84.0754C275.035 85.4288 275.591 86.7041 276.473 87.7401C277.354 88.7761 278.522 89.5263 279.829 89.896C281.135 90.2656 282.521 90.238 283.812 89.8168C285.102 89.3955 286.24 88.5994 287.08 87.5292C287.92 86.4589 288.426 85.1625 288.533 83.8038C288.604 82.9006 288.497 81.9922 288.218 81.1306C287.939 80.269 287.494 79.471 286.908 78.7824C286.321 78.0937 285.606 77.5278 284.802 77.1171C283.997 76.7064 283.121 76.4589 282.221 76.3888ZM282.117 77.7182C283.214 77.801 284.261 78.2089 285.127 78.8903C285.993 79.5716 286.638 80.4958 286.981 81.5457C287.323 82.5955 287.348 83.7238 287.051 84.7876C286.754 85.8514 286.15 86.8028 285.314 87.5213C284.478 88.2398 283.449 88.6931 282.357 88.8236C281.265 88.9542 280.158 88.7562 279.178 88.2547C278.198 87.7532 277.388 86.9709 276.851 86.0067C276.314 85.0426 276.074 83.94 276.161 82.8388C276.217 82.1097 276.415 81.3988 276.745 80.7468C277.075 80.0947 277.529 79.5143 278.082 79.0388C278.635 78.5632 279.276 78.2018 279.968 77.9752C280.661 77.7486 281.391 77.6613 282.117 77.7182ZM285.712 81.6374L285.137 80.958C285.109 80.924 285.075 80.896 285.036 80.8755C284.997 80.8551 284.955 80.8427 284.911 80.839C284.868 80.8353 284.824 80.8405 284.782 80.8541C284.741 80.8678 284.702 80.8897 284.669 80.9186L280.463 84.5062L278.94 82.7042C278.912 82.6697 278.877 82.6413 278.838 82.6206C278.799 82.5998 278.757 82.5872 278.713 82.5835C278.669 82.5799 278.624 82.5852 278.582 82.5991C278.541 82.6131 278.502 82.6354 278.469 82.6648L277.792 83.2425C277.759 83.2706 277.731 83.3052 277.711 83.3442C277.691 83.3832 277.679 83.4258 277.676 83.4696C277.672 83.5133 277.678 83.5573 277.692 83.599C277.705 83.6406 277.727 83.679 277.756 83.7119L280.067 86.4462C280.095 86.4801 280.129 86.5081 280.168 86.5286C280.207 86.549 280.249 86.5614 280.293 86.5651C280.336 86.5688 280.38 86.5636 280.422 86.55C280.463 86.5363 280.502 86.5144 280.535 86.4855L285.673 82.1068C285.707 82.0789 285.734 82.0446 285.755 82.0057C285.775 81.9668 285.788 81.9242 285.791 81.8804C285.795 81.8367 285.79 81.7926 285.776 81.7509C285.763 81.7091 285.741 81.6705 285.712 81.6374Z"
fill="#2CA878"/>
<path
d="M282.221 76.3888C280.868 76.2834 279.514 76.5831 278.331 77.2502C277.147 77.9172 276.187 78.9216 275.572 80.1365C274.957 81.3513 274.714 82.7221 274.874 84.0754C275.035 85.4288 275.591 86.7041 276.473 87.7401C277.354 88.7761 278.522 89.5263 279.829 89.896C281.135 90.2656 282.521 90.238 283.812 89.8168C285.102 89.3955 286.24 88.5994 287.08 87.5292C287.92 86.4589 288.426 85.1625 288.533 83.8038C288.604 82.9006 288.497 81.9922 288.218 81.1306C287.939 80.269 287.494 79.471 286.908 78.7824C286.321 78.0937 285.606 77.5278 284.802 77.1171C283.997 76.7064 283.121 76.4589 282.221 76.3888ZM282.117 77.7182C283.214 77.801 284.261 78.2089 285.127 78.8903C285.993 79.5716 286.638 80.4958 286.981 81.5457C287.323 82.5955 287.348 83.7238 287.051 84.7876C286.754 85.8514 286.15 86.8028 285.314 87.5213C284.478 88.2398 283.449 88.6931 282.357 88.8236C281.265 88.9542 280.158 88.7562 279.178 88.2547C278.198 87.7532 277.388 86.9709 276.851 86.0067C276.314 85.0426 276.074 83.94 276.161 82.8388C276.217 82.1097 276.415 81.3988 276.745 80.7468C277.075 80.0947 277.529 79.5143 278.082 79.0388C278.635 78.5632 279.276 78.2018 279.968 77.9752C280.661 77.7486 281.391 77.6613 282.117 77.7182ZM285.712 81.6374L285.137 80.958C285.109 80.924 285.075 80.896 285.036 80.8755C284.997 80.8551 284.955 80.8427 284.911 80.839C284.868 80.8353 284.824 80.8405 284.782 80.8541C284.741 80.8678 284.702 80.8897 284.669 80.9186L280.463 84.5062L278.94 82.7042C278.912 82.6697 278.877 82.6413 278.838 82.6206C278.799 82.5998 278.757 82.5872 278.713 82.5835C278.669 82.5799 278.624 82.5852 278.582 82.5991C278.541 82.6131 278.502 82.6354 278.469 82.6648L277.792 83.2425C277.759 83.2706 277.731 83.3052 277.711 83.3442C277.691 83.3832 277.679 83.4258 277.676 83.4696C277.672 83.5133 277.678 83.5573 277.692 83.599C277.705 83.6406 277.727 83.679 277.756 83.7119L280.067 86.4462C280.095 86.4801 280.129 86.5081 280.168 86.5286C280.207 86.549 280.249 86.5614 280.293 86.5651C280.336 86.5688 280.38 86.5636 280.422 86.55C280.463 86.5363 280.502 86.5144 280.535 86.4855L285.673 82.1068C285.707 82.0789 285.734 82.0446 285.755 82.0057C285.775 81.9668 285.788 81.9242 285.791 81.8804C285.795 81.8367 285.79 81.7926 285.776 81.7509C285.763 81.7091 285.741 81.6705 285.712 81.6374Z"
fill="white" fill-opacity="0.2"/>
</g>
</g>
<defs>
<linearGradient id="paint0_linear_2254_118640" x1="240.289" y1="188.818" x2="240.289" y2="96.0439"
gradientUnits="userSpaceOnUse">
<stop stop-color="#808080" stop-opacity="0.25"/>
<stop offset="0.54" stop-color="#808080" stop-opacity="0.12"/>
<stop offset="1" stop-color="#808080" stop-opacity="0.1"/>
</linearGradient>
<clipPath id="clip0_2254_118640">
<rect width="345" height="254" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -24,6 +24,7 @@
</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";
@@ -33,6 +34,7 @@ import UiButton from "@/components/ui/UiButton.vue";
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");
@@ -62,7 +64,7 @@ async function handleSubmit() {
isInvalidPassword.value = true;
error.value = t("password-invalid");
} else {
error.value = t("error-occured");
error.value = t("error-occurred");
console.error(err);
}
}

View File

@@ -14,66 +14,66 @@
</UiActionButton>
</UiFilterGroup>
<UiModal
v-if="isOpen"
:icon="faFilter"
@submit.prevent="handleSubmit"
@close="handleCancel"
>
<div class="rows">
<CollectionFilterRow
v-for="(newFilter, index) in newFilters"
:key="newFilter.id"
v-model="newFilters[index]"
:available-filters="availableFilters"
@remove="removeNewFilter"
/>
</div>
<UiModal v-model="isOpen">
<ConfirmModalLayout @submit.prevent="handleSubmit">
<template #default>
<div class="rows">
<CollectionFilterRow
v-for="(newFilter, index) in newFilters"
:key="newFilter.id"
v-model="newFilters[index]"
:available-filters="availableFilters"
@remove="removeNewFilter"
/>
</div>
<div
v-if="newFilters.some((filter) => filter.isAdvanced)"
class="available-properties"
>
{{ $t("available-properties-for-advanced-filter") }}
<div class="properties">
<UiBadge
v-for="(filter, property) in availableFilters"
:key="property"
:icon="getFilterIcon(filter)"
<div
v-if="newFilters.some((filter) => filter.isAdvanced)"
class="available-properties"
>
{{ property }}
</UiBadge>
</div>
</div>
{{ $t("available-properties-for-advanced-filter") }}
<div class="properties">
<UiBadge
v-for="(filter, property) in availableFilters"
:key="property"
:icon="getFilterIcon(filter)"
>
{{ property }}
</UiBadge>
</div>
</div>
</template>
<template #buttons>
<UiButton transparent @click="addNewFilter">
{{ $t("add-or") }}
</UiButton>
<UiButton :disabled="!isFilterValid" type="submit">
{{ $t(editedFilter ? "update" : "add") }}
</UiButton>
<UiButton outlined @click="handleCancel">
{{ $t("cancel") }}
</UiButton>
</template>
<template #buttons>
<UiButton transparent @click="addNewFilter">
{{ $t("add-or") }}
</UiButton>
<UiButton :disabled="!isFilterValid" type="submit">
{{ $t(editedFilter ? "update" : "add") }}
</UiButton>
<UiButton outlined @click="close">
{{ $t("cancel") }}
</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
</template>
<script lang="ts" setup>
import { Or, parse } from "complex-matcher";
import { computed, ref } from "vue";
import type { Filters, NewFilter } from "@/types/filter";
import { faFilter, faPlus } from "@fortawesome/free-solid-svg-icons";
import CollectionFilterRow from "@/components/CollectionFilterRow.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiActionButton from "@/components/ui/UiActionButton.vue";
import UiBadge from "@/components/ui/UiBadge.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiFilter from "@/components/ui/UiFilter.vue";
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import { getFilterIcon } from "@/libs/utils";
import type { Filters, NewFilter } from "@/types/filter";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { Or, parse } from "complex-matcher";
import { computed, ref } from "vue";
defineProps<{
activeFilters: string[];
@@ -85,7 +85,7 @@ const emit = defineEmits<{
(event: "removeFilter", filter: string): void;
}>();
const { isOpen, open, close } = useModal();
const { isOpen, open, close } = useModal({ onClose: () => reset() });
const newFilters = ref<NewFilter[]>([]);
let newFilterId = 0;
@@ -156,11 +156,6 @@ const handleSubmit = () => {
reset();
close();
};
const handleCancel = () => {
reset();
close();
};
</script>
<style lang="postcss" scoped>
@@ -190,4 +185,10 @@ const handleCancel = () => {
margin-top: 0.6rem;
gap: 0.5rem;
}
.rows {
display: flex;
flex-direction: column;
gap: 1rem;
}
</style>

View File

@@ -219,7 +219,6 @@ const valueInputAfter = computed(() =>
.collection-filter-row {
display: flex;
align-items: center;
padding: 1rem 0;
border-bottom: 1px solid var(--background-color-secondary);
gap: 1rem;
@@ -242,4 +241,8 @@ const valueInputAfter = computed(() =>
.form-widget-advanced {
flex: 1;
}
.ui-action-button:first-of-type {
margin-left: auto;
}
</style>

View File

@@ -17,56 +17,56 @@
</UiActionButton>
</UiFilterGroup>
<UiModal
v-if="isOpen"
:icon="faSort"
@submit.prevent="handleSubmit"
@close="handleCancel"
>
<div class="form-widgets">
<FormWidget :label="$t('sort-by')">
<select v-model="newSortProperty">
<option v-if="!newSortProperty"></option>
<option
v-for="(sort, property) in availableSorts"
:key="property"
:value="property"
>
{{ sort.label ?? property }}
</option>
</select>
</FormWidget>
<FormWidget>
<select v-model="newSortIsAscending">
<option :value="true">{{ $t("ascending") }}</option>
<option :value="false">{{ $t("descending") }}</option>
</select>
</FormWidget>
</div>
<template #buttons>
<UiButton type="submit">{{ $t("add") }}</UiButton>
<UiButton outlined @click="handleCancel">
{{ $t("cancel") }}
</UiButton>
</template>
<UiModal v-model="isOpen">
<ConfirmModalLayout @submit.prevent="handleSubmit">
<template #default>
<div class="form-widgets">
<FormWidget :label="$t('sort-by')">
<select v-model="newSortProperty">
<option v-if="!newSortProperty"></option>
<option
v-for="(sort, property) in availableSorts"
:key="property"
:value="property"
>
{{ sort.label ?? property }}
</option>
</select>
</FormWidget>
<FormWidget>
<select v-model="newSortIsAscending">
<option :value="true">{{ $t("ascending") }}</option>
<option :value="false">{{ $t("descending") }}</option>
</select>
</FormWidget>
</div>
</template>
<template #buttons>
<UiButton type="submit">{{ $t("add") }}</UiButton>
<UiButton outlined @click="close">
{{ $t("cancel") }}
</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
</template>
<script lang="ts" setup>
import FormWidget from "@/components/FormWidget.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiActionButton from "@/components/ui/UiActionButton.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiFilter from "@/components/ui/UiFilter.vue";
import UiFilterGroup from "@/components/ui/UiFilterGroup.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import type { ActiveSorts, Sorts } from "@/types/sort";
import {
faCaretDown,
faCaretUp,
faPlus,
faSort,
} from "@fortawesome/free-solid-svg-icons";
import { ref } from "vue";
@@ -81,7 +81,7 @@ const emit = defineEmits<{
(event: "removeSort", property: string): void;
}>();
const { isOpen, open, close } = useModal();
const { isOpen, open, close } = useModal({ onClose: () => reset() });
const newSortProperty = ref();
const newSortIsAscending = ref<boolean>(true);
@@ -96,11 +96,6 @@ const handleSubmit = () => {
reset();
close();
};
const handleCancel = () => {
reset();
close();
};
</script>
<style lang="postcss" scoped>

View File

@@ -28,13 +28,9 @@
</tr>
</thead>
<tbody>
<tr v-for="item in filteredAndSortedCollection" :key="item[idProperty]">
<tr v-for="item in filteredAndSortedCollection" :key="item.$ref">
<td v-if="isSelectable">
<input
v-model="selected"
:value="item[props.idProperty]"
type="checkbox"
/>
<input v-model="selected" :value="item.$ref" type="checkbox" />
</td>
<slot :item="item" name="body-row" />
</tr>
@@ -42,10 +38,7 @@
</UiTable>
</template>
<script lang="ts" setup>
import { computed, toRef, watch } from "vue";
import type { Filters } from "@/types/filter";
import type { Sorts } from "@/types/sort";
<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";
@@ -54,17 +47,20 @@ 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?: string[];
modelValue?: T["$ref"][];
availableFilters?: Filters;
availableSorts?: Sorts;
collection: Record<string, any>[];
idProperty: string;
collection: T[];
}>();
const emit = defineEmits<{
(event: "update:modelValue", selectedRefs: string[]): void;
(event: "update:modelValue", selectedRefs: T["$ref"][]): void;
}>();
const isSelectable = computed(() => props.modelValue !== undefined);
@@ -85,12 +81,10 @@ const filteredAndSortedCollection = useSortedCollection(
compareFn
);
const usableRefs = computed(() =>
props.collection.map((item) => item[props.idProperty])
);
const usableRefs = computed(() => props.collection.map((item) => item["$ref"]));
const selectableRefs = computed(() =>
filteredAndSortedCollection.value.map((item) => item[props.idProperty])
filteredAndSortedCollection.value.map((item) => item["$ref"])
);
const { selected, areAllSelected } = useMultiSelect(usableRefs, selectableRefs);

View File

@@ -0,0 +1,71 @@
<template>
<UiCardSpinner v-if="!areSomeLoaded" />
<UiTable v-else class="hosts-patches-table" :class="{ desktop: isDesktop }">
<tr v-for="patch in sortedPatches" :key="patch.$id">
<th>{{ patch.name }}</th>
<td>
<div class="version">
{{ patch.version }}
<template v-if="hasMultipleHosts">
<UiSpinner v-if="!areAllLoaded" />
<UiCounter
v-else
v-tooltip="{
placement: 'left',
content: $t('n-hosts-awaiting-patch', {
n: patch.$hostRefs.size,
}),
}"
:value="patch.$hostRefs.size"
class="counter"
color="error"
/>
</template>
</div>
</td>
</tr>
</UiTable>
</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";
const props = defineProps<{
patches: XenApiPatchWithHostRefs[];
hasMultipleHosts: boolean;
areAllLoaded: boolean;
areSomeLoaded: boolean;
}>();
const sortedPatches = computed(() =>
[...props.patches].sort(
(patch1, patch2) => patch1.changelog.date - patch2.changelog.date
)
);
const { isDesktop } = useUiStore();
</script>
<style lang="postcss" scoped>
.hosts-patches-table.desktop {
max-width: 45rem;
}
.version {
display: flex;
gap: 1rem;
justify-content: flex-end;
align-items: center;
}
.counter {
font-size: 1rem;
}
</style>

View File

@@ -0,0 +1,96 @@
<template>
<div class="object-link">
<UiSpinner v-if="!isReady" />
<template v-else-if="record !== undefined">
<RouterLink v-if="objectRoute" :to="objectRoute">
{{ record.name_label }}
</RouterLink>
<span v-else>{{ record.name_label }}</span>
</template>
<span v-else class="unknown">{{ uuid }}</span>
</div>
</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";
type HandledTypes = "host" | "vm" | "sr" | "pool";
type XRecord = ObjectTypeToRecord<T>;
type Config = Partial<
Record<
ObjectType,
{
useStore: StoreDefinition<any, any, any, any>;
routeName: RouteRecordName | undefined;
}
>
>;
const props = defineProps<{
type: T;
uuid: XRecord["uuid"];
}>();
const config: Config = {
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>;
const store = computed(() => config[props.type]?.useStore());
const subscriptionId = Symbol();
watch(
store,
(nextStore, previousStore) => {
previousStore?.unsubscribe(subscriptionId);
nextStore?.subscribe(subscriptionId);
},
{ immediate: true }
);
onUnmounted(() => {
store.value?.unsubscribe(subscriptionId);
});
const record = computed<ObjectTypeToRecord<HandledTypes> | undefined>(
() => store.value?.getByUuid(props.uuid as any)
);
const isReady = computed(() => {
return store.value?.isReady ?? true;
});
const objectRoute = computed(() => {
const { routeName } = config[props.type] ?? {};
if (routeName === undefined) {
return;
}
return {
name: routeName,
params: { uuid: props.uuid },
};
});
</script>
<style lang="postcss" scoped>
.unknown {
color: var(--color-blue-scale-300);
font-style: italic;
}
</style>

View File

@@ -7,12 +7,12 @@
</template>
<script
generic="T extends XenApiRecord<string>, I extends T['uuid']"
generic="T extends XenApiRecord<ObjectType>, I extends T['uuid']"
lang="ts"
setup
>
import UiSpinner from "@/components/ui/UiSpinner.vue";
import type { XenApiRecord } from "@/libs/xen-api";
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";

View File

@@ -4,7 +4,7 @@
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { POWER_STATE } from "@/libs/xen-api";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import {
faMoon,
faPause,
@@ -15,14 +15,14 @@ import {
import { computed } from "vue";
const props = defineProps<{
state: POWER_STATE;
state: VM_POWER_STATE;
}>();
const icons = {
[POWER_STATE.RUNNING]: faPlay,
[POWER_STATE.PAUSED]: faPause,
[POWER_STATE.SUSPENDED]: faMoon,
[POWER_STATE.HALTED]: faStop,
[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);

View File

@@ -4,19 +4,16 @@
<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";
const props = withDefaults(
defineProps<{
date: Date | number | string;
interval?: number;
}>(),
{ interval: 1000 }
);
const props = defineProps<{
date: Date | number | string;
}>();
const date = computed(() => new Date(props.date));
const now = useNow({ interval: props.interval });
const date = computed(() => new Date(parseDateTime(props.date)));
const now = useNow({ interval: 1000 });
const relativeTime = useRelativeTime(date, now);
</script>

View File

@@ -3,11 +3,11 @@
</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 VncClient from "@novnc/novnc/core/rfb";
import { useXenApiStore } from "@/stores/xen-api.store";
import { promiseTimeout } from "@vueuse/shared";
const N_TOTAL_TRIES = 8;
const FIBONACCI_MS_ARRAY: number[] = Array.from(

View File

@@ -1,9 +1,5 @@
<template>
<RouterLink
v-slot="{ isActive, href }"
:to="disabled || isTabBarDisabled ? '' : to"
custom
>
<RouterLink v-slot="{ isActive, href }" :to="isDisabled ? '' : to" custom>
<UiTab :active="isActive" :disabled="disabled" :href="href" tag="a">
<slot />
</UiTab>
@@ -11,20 +7,20 @@
</template>
<script lang="ts" setup>
import { IK_TAB_BAR_DISABLED } from "@/types/injection-keys";
import { computed, inject } from "vue";
import { useContext } from "@/composables/context.composable";
import { DisabledContext } from "@/context";
import type { RouteLocationRaw } from "vue-router";
import UiTab from "@/components/ui/UiTab.vue";
defineProps<{
to: RouteLocationRaw;
disabled?: boolean;
}>();
const isTabBarDisabled = inject(
IK_TAB_BAR_DISABLED,
computed(() => false)
const props = withDefaults(
defineProps<{
to: RouteLocationRaw;
disabled?: boolean;
}>(),
{ disabled: undefined }
);
const isDisabled = useContext(DisabledContext, () => props.disabled);
</script>
<style lang="postcss" scoped></style>

View File

@@ -1,45 +1,58 @@
<template>
<UiModal
v-if="isSslModalOpen"
:icon="faServer"
color="error"
@close="clearUnreachableHostsUrls"
>
<template #title>{{ $t("unreachable-hosts") }}</template>
<div class="description">
<p>{{ $t("following-hosts-unreachable") }}</p>
<p>{{ $t("allow-self-signed-ssl") }}</p>
<ul>
<li v-for="url in unreachableHostsUrls" :key="url">
<a :href="url" class="link" rel="noopener" target="_blank">{{
url
}}</a>
</li>
</ul>
</div>
<template #buttons>
<UiButton color="success" @click="reload">
{{ $t("unreachable-hosts-reload-page") }}
</UiButton>
<UiButton @click="clearUnreachableHostsUrls">{{ $t("cancel") }}</UiButton>
</template>
<UiModal v-model="isSslModalOpen" color="error">
<ConfirmModalLayout :icon="faServer">
<template #title>{{ $t("unreachable-hosts") }}</template>
<template #default>
<div class="description">
<p>{{ $t("following-hosts-unreachable") }}</p>
<p>{{ $t("allow-self-signed-ssl") }}</p>
<ul>
<li v-for="url in unreachableHostsUrls" :key="url">
<a :href="url" class="link" rel="noopener" target="_blank">{{
url
}}</a>
</li>
</ul>
</div>
</template>
<template #buttons>
<UiButton color="success" @click="reload">
{{ $t("unreachable-hosts-reload-page") }}
</UiButton>
<UiButton @click="closeSslModal">{{ $t("cancel") }}</UiButton>
</template>
</ConfirmModalLayout>
</UiModal>
</template>
<script lang="ts" setup>
import { faServer } from "@fortawesome/free-solid-svg-icons";
import UiModal from "@/components/ui/UiModal.vue";
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import { computed, ref, watch } from "vue";
import useModal from "@/composables/modal.composable";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { faServer } from "@fortawesome/free-solid-svg-icons";
import { difference } from "lodash-es";
import { useHostStore } from "@/stores/host.store";
import { ref, watch } from "vue";
const { records: hosts } = useHostStore().subscribe();
const { records: hosts } = useHostCollection();
const unreachableHostsUrls = ref<Set<string>>(new Set());
const clearUnreachableHostsUrls = () => unreachableHostsUrls.value.clear();
const isSslModalOpen = computed(() => unreachableHostsUrls.value.size > 0);
const reload = () => window.location.reload();
const { isOpen: isSslModalOpen, close: closeSslModal } = useModal({
onClose: () => unreachableHostsUrls.value.clear(),
});
watch(
() => unreachableHostsUrls.value.size,
(size) => {
isSslModalOpen.value = size > 0;
},
{ immediate: true }
);
watch(hosts, (nextHosts, previousHosts) => {
difference(nextHosts, previousHosts).forEach((host) => {
const url = new URL("http://localhost");
@@ -53,7 +66,11 @@ watch(hosts, (nextHosts, previousHosts) => {
</script>
<style lang="postcss" scoped>
.description p {
margin: 1rem 0;
.description {
text-align: center;
p {
margin: 1rem 0;
}
}
</style>

View File

@@ -33,7 +33,7 @@
</AppMenu>
</UiTabBar>
<div class="tabs">
<div :class="{ 'full-width': fullWidthComponent }" class="tabs">
<UiCard v-if="selectedTab === TAB.NONE" class="tab-content">
<i>No configuration defined</i>
</UiCard>
@@ -102,11 +102,11 @@ import StorySettingParams from "@/components/component-story/StorySettingParams.
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 UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiTab from "@/components/ui/UiTab.vue";
import UiTabBar from "@/components/ui/UiTabBar.vue";
import {
@@ -140,6 +140,7 @@ const props = defineProps<{
settings?: Record<string, any>;
}
>;
fullWidthComponent?: boolean;
}>();
enum TAB {
@@ -329,6 +330,10 @@ const applyPreset = (preset: {
padding: 1rem;
gap: 1rem;
&.full-width {
flex-direction: column;
}
.tab-content {
flex: 1;
height: auto;

View File

@@ -2,32 +2,80 @@
<RouterLink :to="{ name: 'story' }">
<UiTitle type="h4">Stories</UiTitle>
</RouterLink>
<ul class="links">
<li v-for="route in routes" :key="route.name">
<RouterLink class="link" :to="route">
{{ route.meta.storyTitle }}
</RouterLink>
</li>
</ul>
<StoryMenuTree
:tree="tree"
@toggle-directory="toggleDirectory"
:opened-directories="openedDirectories"
/>
</template>
<script lang="ts" setup>
import { useRouter } from "vue-router";
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 routes = getRoutes().filter((route) => route.meta.isStory);
</script>
<style lang="postcss" scoped>
.links {
padding: 1rem;
export type StoryTree = Map<
string,
{ path: string; directory: string; children: StoryTree }
>;
function createTree(routes: RouteRecordNormalized[]) {
const tree: StoryTree = new Map();
for (const route of routes) {
const parts = route.path.slice(7).split("/");
let currentNode = tree;
let currentPath = "";
for (const part of parts) {
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;
}
}
return tree;
}
.link {
display: inline-block;
padding: 0.5rem 1rem;
text-decoration: none;
font-size: 1.6rem;
}
</style>
const tree = createTree(routes);
const currentRoute = useRoute();
const getDefaultOpenedDirectories = (): Set<string> => {
if (!currentRoute.meta.isStory) {
return new Set<string>();
}
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);
}
return openedDirectories;
};
const openedDirectories = ref(getDefaultOpenedDirectories());
const toggleDirectory = (directory: string) => {
if (openedDirectories.value.has(directory)) {
openedDirectories.value.delete(directory);
} else {
openedDirectories.value.add(directory);
}
};
</script>

View File

@@ -0,0 +1,83 @@
<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"
/>
{{ formatName(key) }}
</span>
<RouterLink v-else :to="node.path" class="link">
<UiIcon :icon="faFile" />
{{ formatName(key) }}
</RouterLink>
<StoryMenuTree
v-if="isOpen(node.directory)"
:tree="node.children"
@toggle-directory="emit('toggle-directory', $event)"
:opened-directories="openedDirectories"
/>
</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";
const props = defineProps<{
tree: StoryTree;
openedDirectories: Set<string>;
}>();
const emit = defineEmits<{
(event: "toggle-directory", directory: string): void;
}>();
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(" ");
};
</script>
<style lang="postcss" scoped>
.story-menu-tree {
padding-left: 1rem;
.story-menu-tree {
padding-left: 2.2rem;
}
}
.directory {
font-weight: 500;
}
.link {
padding: 0.5rem 0;
}
.directory {
padding: 0.5rem 0;
}
.link,
.directory {
cursor: pointer;
text-decoration: none;
font-size: 1.6rem;
display: inline-block;
}
</style>

View File

@@ -1,6 +1,8 @@
<template>
<UiModal v-if="isRawValueModalOpen" @close="closeRawValueModal">
<CodeHighlight :code="rawValueModalPayload" />
<UiModal v-model="isRawValueModalOpen">
<BasicModalLayout>
<CodeHighlight :code="rawValueModalPayload" />
</BasicModalLayout>
</UiModal>
<StoryParamsTable>
<thead>
@@ -36,6 +38,15 @@
>
<UiIcon :icon="faRepeat" />
</sup>
<sup
v-if="param.isUsingContext()"
v-tooltip="
`If this prop is not provided, value will be read from context. Otherwise, context will be updated with this prop value.`
"
class="context-indicator"
>
Ctx
</sup>
</th>
<td>
<CodeHighlight :code="param.getTypeLabel()" />
@@ -90,7 +101,8 @@ 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 UiModal from "@/components/ui/UiModal.vue";
import BasicModalLayout from "@/components/ui/modals/layouts/BasicModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import useModal from "@/composables/modal.composable";
import useSortedCollection from "@/composables/sorted-collection.composable";
import { vTooltip } from "@/directives/tooltip.directive";
@@ -121,7 +133,6 @@ const model = useVModel(props, "modelValue", emit);
const {
open: openRawValueModal,
close: closeRawValueModal,
isOpen: isRawValueModalOpen,
payload: rawValueModalPayload,
} = useModal<string>();
@@ -182,7 +193,8 @@ const {
}
}
.v-model-indicator {
.v-model-indicator,
.context-indicator {
color: var(--color-green-infra-base);
}
</style>

View File

@@ -7,7 +7,7 @@
<input
v-model="value"
:class="{ indeterminate: type === 'checkbox' && value === undefined }"
:disabled="isLabelDisabled || disabled"
:disabled="isDisabled"
:type="type === 'radio' ? 'radio' : 'checkbox'"
class="input"
v-bind="$attrs"
@@ -19,23 +19,24 @@
</template>
<script lang="ts" setup>
import {
IK_FORM_HAS_LABEL,
IK_FORM_LABEL_DISABLED,
IK_CHECKBOX_TYPE,
} from "@/types/injection-keys";
import { type HTMLAttributes, computed, 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 UiIcon from "@/components/ui/icon/UiIcon.vue";
import { computed, type HTMLAttributes, inject } from "vue";
defineOptions({ inheritAttrs: false });
const props = defineProps<{
modelValue?: unknown;
disabled?: boolean;
wrapperAttrs?: HTMLAttributes;
}>();
const props = withDefaults(
defineProps<{
modelValue?: unknown;
disabled?: boolean;
wrapperAttrs?: HTMLAttributes;
}>(),
{ disabled: undefined }
);
const emit = defineEmits<{
(event: "update:modelValue", value: boolean): void;
@@ -47,10 +48,7 @@ const hasLabel = inject(
IK_FORM_HAS_LABEL,
computed(() => false)
);
const isLabelDisabled = inject(
IK_FORM_LABEL_DISABLED,
computed(() => false)
);
const isDisabled = useContext(DisabledContext, () => props.disabled);
const icon = computed(() => {
if (type !== "checkbox") {
return faCircle;
@@ -153,7 +151,8 @@ const icon = computed(() => {
align-items: center;
justify-content: center;
height: 1.25em;
transition: background-color 0.125s ease-in-out,
transition:
background-color 0.125s ease-in-out,
border-color 0.125s ease-in-out;
border: var(--checkbox-border-width) solid var(--border-color);
border-radius: var(--checkbox-border-radius);

View File

@@ -6,7 +6,7 @@
ref="inputElement"
v-model="value"
:class="inputClass"
:disabled="disabled || isLabelDisabled"
:disabled="isDisabled"
:required="required"
class="select"
v-bind="$attrs"
@@ -23,7 +23,7 @@
ref="textarea"
v-model="value"
:class="inputClass"
:disabled="disabled || isLabelDisabled"
:disabled="isDisabled"
:required="required"
class="textarea"
v-bind="$attrs"
@@ -34,7 +34,7 @@
ref="inputElement"
v-model="value"
:class="inputClass"
:disabled="disabled || isLabelDisabled"
:disabled="isDisabled"
:required="required"
class="input"
v-bind="$attrs"
@@ -52,13 +52,10 @@
<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_INPUT_COLOR,
IK_FORM_LABEL_DISABLED,
IK_INPUT_ID,
IK_INPUT_TYPE,
} from "@/types/injection-keys";
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";
@@ -87,9 +84,11 @@ const props = withDefaults(
right?: boolean;
wrapperAttrs?: HTMLAttributes;
}>(),
{ color: "info" }
{ disabled: undefined }
);
const { name: contextColor } = useContext(ColorContext, () => props.color);
const inputElement = ref();
const emit = defineEmits<{
@@ -101,25 +100,19 @@ const isEmpty = computed(
() => props.modelValue == null || String(props.modelValue).trim() === ""
);
const inputType = inject(IK_INPUT_TYPE, "input");
const isLabelDisabled = inject(
IK_FORM_LABEL_DISABLED,
computed(() => false)
);
const parentColor = inject(
IK_FORM_INPUT_COLOR,
computed(() => undefined)
);
const isDisabled = useContext(DisabledContext, () => props.disabled);
const wrapperClass = computed(() => [
`form-${inputType}`,
{
disabled: props.disabled === true || isLabelDisabled.value,
disabled: isDisabled.value,
empty: isEmpty.value,
},
]);
const inputClass = computed(() => [
parentColor.value ?? props.color,
contextColor.value,
{
right: props.right,
"has-before": props.before !== undefined,

View File

@@ -4,7 +4,7 @@
v-if="label !== undefined || learnMoreUrl !== undefined"
class="label-container"
>
<label :for="id" class="label">
<label :class="{ light }" :for="id" class="label">
<UiIcon :icon="icon" />
{{ label }}
</label>
@@ -37,13 +37,10 @@
<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_FORM_INPUT_COLOR,
IK_FORM_LABEL_DISABLED,
IK_INPUT_ID,
} from "@/types/injection-keys";
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";
@@ -51,16 +48,20 @@ import { computed, provide, useSlots } from "vue";
const slots = useSlots();
const props = defineProps<{
label?: string;
id?: string;
icon?: IconDefinition;
learnMoreUrl?: string;
warning?: string;
error?: string;
help?: string;
disabled?: boolean;
}>();
const props = withDefaults(
defineProps<{
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);
@@ -77,17 +78,13 @@ const color = computed<Color | undefined>(() => {
return undefined;
});
provide(IK_FORM_INPUT_COLOR, color);
provide(
IK_FORM_HAS_LABEL,
computed(() => slots.label !== undefined)
);
provide(
IK_FORM_LABEL_DISABLED,
computed(() => props.disabled ?? false)
);
useContext(ColorContext, color);
useContext(DisabledContext, () => props.disabled);
</script>
<style lang="postcss" scoped>
@@ -99,14 +96,24 @@ provide(
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.label {
text-transform: uppercase;
font-weight: 700;
color: var(--color-blue-scale-100);
font-size: 1.4rem;
padding: 1rem 0;
&.light {
font-size: 1.6rem;
color: var(--color-blue-scale-300);
font-weight: 400;
}
&:not(.light) {
font-size: 1.4rem;
text-transform: uppercase;
font-weight: 700;
color: var(--color-blue-scale-100);
}
}
.messages-container {

View File

@@ -1,20 +1,28 @@
<template>
<UiModal
@submit.prevent="saveJson"
v-model="isCodeModalOpen"
:color="isJsonValid ? 'success' : 'error'"
v-if="isCodeModalOpen"
:icon="faCode"
@close="closeCodeModal"
closable
>
<FormTextarea class="modal-textarea" v-model="editedJson" />
<template #buttons>
<UiButton transparent @click="formatJson">{{ $t("reformat") }}</UiButton>
<UiButton outlined @click="closeCodeModal">{{ $t("cancel") }}</UiButton>
<UiButton :disabled="!isJsonValid" type="submit"
>{{ $t("save") }}
</UiButton>
</template>
<FormModalLayout @submit.prevent="saveJson" :icon="faCode">
<template #default>
<FormTextarea class="modal-textarea" v-model="editedJson" />
</template>
<template #buttons>
<UiButton transparent @click="formatJson">
{{ $t("reformat") }}
</UiButton>
<UiButton outlined @click="closeCodeModal">
{{ $t("cancel") }}
</UiButton>
<UiButton :disabled="!isJsonValid" type="submit">
{{ $t("save") }}
</UiButton>
</template>
</FormModalLayout>
</UiModal>
<FormInput
@click="openCodeModal"
:model-value="jsonValue"
@@ -26,8 +34,9 @@
<script lang="ts" setup>
import FormInput from "@/components/form/FormInput.vue";
import FormTextarea from "@/components/form/FormTextarea.vue";
import FormModalLayout from "@/components/ui/modals/layouts/FormModalLayout.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiModal from "@/components/ui/UiModal.vue";
import useModal from "@/composables/modal.composable";
import { faCode } from "@fortawesome/free-solid-svg-icons";
import { useVModel, whenever } from "@vueuse/core";

View File

@@ -28,10 +28,10 @@
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";
import { useHostStore } from "@/stores/host.store";
import { usePoolStore } from "@/stores/pool.store";
import type { XenApiHost } from "@/libs/xen-api/xen-api.types";
import { useUiStore } from "@/stores/ui.store";
import {
faAngleDown,
@@ -46,11 +46,10 @@ const props = defineProps<{
hostOpaqueRef: XenApiHost["$ref"];
}>();
const { getByOpaqueRef } = useHostStore().subscribe();
const { getByOpaqueRef } = useHostCollection();
const host = computed(() => getByOpaqueRef(props.hostOpaqueRef));
const { pool } = usePoolStore().subscribe();
const { pool } = usePoolCollection();
const isPoolMaster = computed(() => pool.value?.master === props.hostOpaqueRef);
const uiStore = useUiStore();

View File

@@ -16,9 +16,9 @@
<script lang="ts" setup>
import InfraHostItem from "@/components/infra/InfraHostItem.vue";
import { useHostStore } from "@/stores/host.store";
import { useHostCollection } from "@/stores/xen-api/host.store";
const { records: hosts, isReady, hasError } = useHostStore().subscribe();
const { records: hosts, isReady, hasError } = useHostCollection();
</script>
<style lang="postcss" scoped>

View File

@@ -28,10 +28,10 @@ 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 { usePoolStore } from "@/stores/pool.store";
import { usePoolCollection } from "@/stores/xen-api/pool.store";
import { faBuilding } from "@fortawesome/free-regular-svg-icons";
const { isReady, hasError, pool } = usePoolStore().subscribe();
const { isReady, hasError, pool } = usePoolCollection();
</script>
<style lang="postcss" scoped>

View File

@@ -19,8 +19,8 @@
import InfraAction from "@/components/infra/InfraAction.vue";
import InfraItemLabel from "@/components/infra/InfraItemLabel.vue";
import PowerStateIcon from "@/components/PowerStateIcon.vue";
import type { XenApiVm } from "@/libs/xen-api";
import { useVmStore } from "@/stores/vm.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import type { XenApiVm } from "@/libs/xen-api/xen-api.types";
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
import { useIntersectionObserver } from "@vueuse/core";
import { computed, ref } from "vue";
@@ -29,7 +29,7 @@ const props = defineProps<{
vmOpaqueRef: XenApiVm["$ref"];
}>();
const { getByOpaqueRef } = useVmStore().subscribe();
const { getByOpaqueRef } = useVmCollection();
const vm = computed(() => getByOpaqueRef(props.vmOpaqueRef));
const rootElement = ref();
const isVisible = ref(false);

View File

@@ -11,8 +11,8 @@
<script lang="ts" setup>
import InfraLoadingItem from "@/components/infra/InfraLoadingItem.vue";
import InfraVmItem from "@/components/infra/InfraVmItem.vue";
import type { XenApiHost } from "@/libs/xen-api";
import { useVmStore } from "@/stores/vm.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import type { XenApiHost } from "@/libs/xen-api/xen-api.types";
import { faDisplay } from "@fortawesome/free-solid-svg-icons";
import { computed } from "vue";
@@ -20,7 +20,7 @@ const props = defineProps<{
hostOpaqueRef?: XenApiHost["$ref"];
}>();
const { isReady, recordsByHostRef, hasError } = useVmStore().subscribe();
const { isReady, recordsByHostRef, hasError } = useVmCollection();
const vms = computed(() =>
recordsByHostRef.value.get(

View File

@@ -14,9 +14,10 @@
</template>
<script lang="ts" setup>
import { useContext } from "@/composables/context.composable";
import { DisabledContext } from "@/context";
import {
IK_CLOSE_MENU,
IK_MENU_DISABLED,
IK_MENU_HORIZONTAL,
IK_MENU_TELEPORTED,
} from "@/types/injection-keys";
@@ -24,12 +25,15 @@ import placementJs, { type Options } from "placement.js";
import { computed, inject, nextTick, provide, ref, useSlots } from "vue";
import { onClickOutside, unrefElement, whenever } from "@vueuse/core";
const props = defineProps<{
horizontal?: boolean;
shadow?: boolean;
disabled?: boolean;
placement?: Options["placement"];
}>();
const props = withDefaults(
defineProps<{
horizontal?: boolean;
shadow?: boolean;
disabled?: boolean;
placement?: Options["placement"];
}>(),
{ disabled: undefined }
);
defineOptions({
inheritAttrs: false,
@@ -46,10 +50,9 @@ provide(
IK_MENU_HORIZONTAL,
computed(() => props.horizontal ?? false)
);
provide(
IK_MENU_DISABLED,
computed(() => props.disabled ?? false)
);
useContext(DisabledContext, () => props.disabled);
let clearClickOutsideEvent: (() => void) | undefined;
const hasTrigger = useSlots().trigger !== undefined;

View File

@@ -36,33 +36,28 @@
import AppMenu from "@/components/menu/AppMenu.vue";
import MenuTrigger from "@/components/menu/MenuTrigger.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import {
IK_CLOSE_MENU,
IK_MENU_DISABLED,
IK_MENU_HORIZONTAL,
} from "@/types/injection-keys";
import { useContext } from "@/composables/context.composable";
import { DisabledContext } from "@/context";
import { IK_CLOSE_MENU, IK_MENU_HORIZONTAL } from "@/types/injection-keys";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faAngleDown, faAngleRight } from "@fortawesome/free-solid-svg-icons";
import { computed, inject, ref } from "vue";
const props = defineProps<{
icon?: IconDefinition;
onClick?: () => any;
disabled?: boolean;
busy?: boolean;
}>();
const props = withDefaults(
defineProps<{
icon?: IconDefinition;
onClick?: () => any;
disabled?: boolean;
busy?: boolean;
}>(),
{ disabled: undefined }
);
const isParentHorizontal = inject(
IK_MENU_HORIZONTAL,
computed(() => false)
);
const isMenuDisabled = inject(
IK_MENU_DISABLED,
computed(() => false)
);
const isDisabled = computed(
() => props.disabled === true || isMenuDisabled.value
);
const isDisabled = useContext(DisabledContext, () => props.disabled);
const submenuIcon = computed(() =>
isParentHorizontal.value ? faAngleDown : faAngleRight

View File

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

View File

@@ -33,7 +33,7 @@
<script lang="ts" setup>
import RouterTab from "@/components/RouterTab.vue";
import UiTabBar from "@/components/ui/UiTabBar.vue";
import { usePoolStore } from "@/stores/pool.store";
import { usePoolCollection } from "@/stores/xen-api/pool.store";
const { pool, isReady } = usePoolStore().subscribe();
const { pool, isReady } = usePoolCollection();
</script>

View File

@@ -0,0 +1,88 @@
<template>
<UiCard class="pool-dashboard-alarms">
<UiCardTitle>
{{ $t("alarms") }}
<template v-if="isReady && alarms.length > 0" #right>
<UiCounter :value="alarms.length" color="error" />
</template>
</UiCardTitle>
<div v-if="!isStarted" class="pre-start">
<div>
<p class="text">
{{ $t("click-to-display-alarms") }}
</p>
<UiButton @click="start">{{ $t("load-now") }}</UiButton>
</div>
<div>
<img alt="" src="@/assets/server-status.svg" />
</div>
</div>
<NoDataError v-else-if="hasError" />
<div v-else-if="!isReady">
<UiCardSpinner />
</div>
<div v-else-if="alarms.length === 0" class="no-alarm">
<div>
<img alt="" src="@/assets/server-status.svg" />
</div>
<p class="text">
{{ $t("all-good") }}<br />{{ $t("no-alarm-triggered") }}
</p>
</div>
<div v-else class="table-container">
<UiTable>
<tbody>
<AlarmRow v-for="alarm in alarms" :key="alarm.uuid" :alarm="alarm" />
</tbody>
</UiTable>
</div>
</UiCard>
</template>
<script lang="ts" setup>
import NoDataError from "@/components/NoDataError.vue";
import AlarmRow from "@/components/pool/dashboard/alarm/AlarmRow.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiCounter from "@/components/ui/UiCounter.vue";
import UiTable from "@/components/ui/UiTable.vue";
import { useAlarmCollection } from "@/stores/xen-api/alarm.store";
const {
records: alarms,
start,
isStarted,
isReady,
hasError,
} = useAlarmCollection({ defer: true });
</script>
<style lang="postcss" scoped>
.pool-dashboard-alarms {
min-width: 0;
}
.pre-start,
.no-alarm {
display: flex;
justify-content: center;
align-items: center;
gap: 3rem;
}
.text {
font-size: 2rem;
font-weight: 500;
.pre-start & {
margin-bottom: 2rem;
}
.no-alarm & {
color: var(--color-green-infra-base);
}
}
.table-container {
max-height: 24rem;
overflow: auto;
}
</style>

View File

@@ -37,33 +37,30 @@ import UiCard from "@/components/ui/UiCard.vue";
import UiCardFooter from "@/components/ui/UiCardFooter.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { useVmMetricsCollection } from "@/stores/xen-api/vm-metrics.store";
import { percent } from "@/libs/utils";
import { POWER_STATE } from "@/libs/xen-api";
import { useHostMetricsStore } from "@/stores/host-metrics.store";
import { useHostStore } from "@/stores/host.store";
import { useVmMetricsStore } from "@/stores/vm-metrics.store";
import { useVmStore } from "@/stores/vm.store";
import { VM_POWER_STATE } from "@/libs/xen-api/xen-api.enums";
import { logicAnd } from "@vueuse/math";
import { computed } from "vue";
const ACTIVE_STATES = new Set([POWER_STATE.RUNNING, POWER_STATE.PAUSED]);
const ACTIVE_STATES = new Set([VM_POWER_STATE.RUNNING, VM_POWER_STATE.PAUSED]);
const {
hasError: hostStoreHasError,
isReady: isHostStoreReady,
runningHosts,
} = useHostStore().subscribe({
hostMetricsSubscription: useHostMetricsStore().subscribe(),
});
} = useHostCollection();
const {
hasError: vmStoreHasError,
isReady: isVmStoreReady,
records: vms,
} = useVmStore().subscribe();
} = useVmCollection();
const { getByOpaqueRef: getVmMetrics, isReady: isVmMetricsStoreReady } =
useVmMetricsStore().subscribe();
useVmMetricsCollection();
const nPCpu = computed(() =>
runningHosts.value.reduce(

View File

@@ -11,20 +11,20 @@
</UiCard>
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { vTooltip } from "@/directives/tooltip.directive";
import HostsCpuUsage from "@/components/pool/dashboard/cpuUsage/HostsCpuUsage.vue";
import VmsCpuUsage from "@/components/pool/dashboard/cpuUsage/VmsCpuUsage.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostStore } from "@/stores/host.store";
import { useVmStore } from "@/stores/vm.store";
import { computed, inject, type ComputedRef } from "vue";
import type { Stat } from "@/composables/fetch-stats.composable";
import type { HostStats, VmStats } from "@/libs/xapi-stats";
import UiSpinner from "@/components/ui/UiSpinner.vue";
const { hasError: hasVmError } = useVmStore().subscribe();
const { hasError: hasHostError } = useHostStore().subscribe();
const { hasError: hasVmError } = useVmCollection();
const { hasError: hasHostError } = useHostCollection();
const vmStats = inject<ComputedRef<Stat<VmStats>[]>>(
"vmStats",

View File

@@ -0,0 +1,41 @@
<template>
<UiCard>
<UiCardTitle class="patches-title">
{{ $t("patches") }}
<template v-if="areAllLoaded" #right>
{{ $t("n-missing", { n: count }) }}
</template>
</UiCardTitle>
<div class="table-container">
<HostPatches
:are-all-loaded="areAllLoaded"
:are-some-loaded="areSomeLoaded"
:has-multiple-hosts="hosts.length > 1"
:patches="patches"
/>
</div>
</UiCard>
</template>
<script lang="ts" setup>
import HostPatches from "@/components/HostPatchesTable.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostPatches } from "@/composables/host-patches.composable";
import { useHostCollection } from "@/stores/xen-api/host.store";
const { records: hosts } = useHostCollection();
const { count, patches, areSomeLoaded, areAllLoaded } = useHostPatches(hosts);
</script>
<style lang="postcss" scoped>
.patches-title {
--section-title-right-color: var(--color-red-vates-base);
}
.table-container {
max-height: 40rem;
overflow: auto;
}
</style>

View File

@@ -12,21 +12,21 @@
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { vTooltip } from "@/directives/tooltip.directive";
import HostsRamUsage from "@/components/pool/dashboard/ramUsage/HostsRamUsage.vue";
import VmsRamUsage from "@/components/pool/dashboard/ramUsage/VmsRamUsage.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostStore } from "@/stores/host.store";
import { useVmStore } from "@/stores/vm.store";
import { computed, inject } from "vue";
import type { ComputedRef } from "vue";
import type { HostStats, VmStats } from "@/libs/xapi-stats";
import type { Stat } from "@/composables/fetch-stats.composable";
import UiSpinner from "@/components/ui/UiSpinner.vue";
const { hasError: hasVmError } = useVmStore().subscribe();
const { hasError: hasHostError } = useHostStore().subscribe();
const { hasError: hasVmError } = useVmCollection();
const { hasError: hasHostError } = useHostCollection();
const vmStats = inject<ComputedRef<Stat<VmStats>[]>>(
"vmStats",

View File

@@ -26,22 +26,21 @@ import UiCard from "@/components/ui/UiCard.vue";
import UiCardSpinner from "@/components/ui/UiCardSpinner.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UiSeparator from "@/components/ui/UiSeparator.vue";
import { useHostMetricsStore } from "@/stores/host-metrics.store";
import { useVmStore } from "@/stores/vm.store";
import { useHostMetricsCollection } from "@/stores/xen-api/host-metrics.store";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { computed } from "vue";
const {
isReady: isVmReady,
records: vms,
hasError: hasVmError,
runningVms,
} = useVmStore().subscribe();
} = useVmCollection();
const {
isReady: isHostMetricsReady,
records: hostMetrics,
hasError: hasHostMetricsError,
} = useHostMetricsStore().subscribe();
} = useHostMetricsCollection();
const hasError = computed(() => hasVmError.value || hasHostMetricsError.value);
@@ -55,5 +54,7 @@ const activeHostsCount = computed(
const totalVmsCount = computed(() => vms.value.length);
const activeVmsCount = computed(() => runningVms.value.length);
const activeVmsCount = computed(
() => vms.value.filter((vm) => vm.power_state === "Running").length
);
</script>

View File

@@ -23,11 +23,11 @@ import SizeStatsSummary from "@/components/ui/SizeStatsSummary.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useSrStore } from "@/stores/storage.store";
import { useSrCollection } from "@/stores/xen-api/sr.store";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed } from "vue";
const { records: srs, isReady, hasError } = useSrStore().subscribe();
const { records: srs, isReady, hasError } = useSrCollection();
const data = computed<{
result: { id: string; label: string; value: number }[];

View File

@@ -0,0 +1,17 @@
<template>
<UiCard>
<UiCardTitle :count="pendingTasks.length">{{ $t("tasks") }}</UiCardTitle>
<TasksTable :pending-tasks="pendingTasks" />
</UiCard>
</template>
<script lang="ts" setup>
import TasksTable from "@/components/tasks/TasksTable.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useTaskCollection } from "@/stores/xen-api/task.store";
const { pendingTasks } = useTaskCollection();
</script>
<style lang="postcss" scoped></style>

View File

@@ -0,0 +1,63 @@
<template>
<tr>
<th>
<div
v-tooltip="new Date(parseDateTime(alarm.timestamp)).toLocaleString()"
class="ellipsis time"
>
<RelativeTime :date="alarm.timestamp" />
</div>
</th>
<td>
<div
ref="descriptionElement"
v-tooltip="hasTooltip"
class="ellipsis description"
>
{{ $t(`alarm-type.${alarm.type}`, { n: alarm.triggerLevel * 100 }) }}
</div>
</td>
<td class="level">{{ alarm.level * 100 }}%</td>
<td class="on">{{ $t("on-object", { object: alarm.cls }) }}</td>
<td class="object">
<ObjectLink :type="rawTypeToType(alarm.cls)" :uuid="alarm.obj_uuid" />
</td>
</tr>
</template>
<script lang="ts" setup generic="T extends RawObjectType">
import ObjectLink from "@/components/ObjectLink.vue";
import RelativeTime from "@/components/RelativeTime.vue";
import { vTooltip } from "@/directives/tooltip.directive";
import { hasEllipsis, parseDateTime } from "@/libs/utils";
import type { RawObjectType } from "@/libs/xen-api/xen-api.types";
import { rawTypeToType } from "@/libs/xen-api/xen-api.utils";
import type { XenApiAlarm } from "@/types/xen-api";
import { computed, ref } from "vue";
defineProps<{
alarm: XenApiAlarm<T>;
}>();
const descriptionElement = ref<HTMLElement>();
const hasTooltip = computed(() => hasEllipsis(descriptionElement.value));
</script>
<style lang="postcss" scoped>
.ellipsis {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.level {
color: var(--color-red-vates-base);
font-size: 1.4rem;
font-weight: 700;
}
.on {
white-space: nowrap;
}
.object-link {
white-space: nowrap;
}
</style>

View File

@@ -12,13 +12,13 @@
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { getAvgCpuUsage } from "@/libs/utils";
import { useHostStore } from "@/stores/host.store";
import { IK_HOST_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
const { hasError } = useHostStore().subscribe();
const { hasError } = useHostCollection();
const stats = inject(
IK_HOST_STATS,

View File

@@ -12,9 +12,9 @@
</template>
<script lang="ts" setup>
import { useHostCollection } from "@/stores/xen-api/host.store";
import type { HostStats } from "@/libs/xapi-stats";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import { useHostStore } from "@/stores/host.store";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import { sumBy } from "lodash-es";
@@ -29,7 +29,7 @@ const { t } = useI18n();
const hostLastWeekStats = inject(IK_HOST_LAST_WEEK_STATS);
const { records: hosts } = useHostStore().subscribe();
const { records: hosts } = useHostCollection();
const customMaxValue = computed(
() => 100 * sumBy(hosts.value, (host) => +host.cpu_info.cpu_count)

View File

@@ -12,13 +12,13 @@
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { getAvgCpuUsage } from "@/libs/utils";
import { useVmStore } from "@/stores/vm.store";
import { IK_VM_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
const { hasError } = useVmStore().subscribe();
const { hasError } = useVmCollection();
const stats = inject(
IK_VM_STATS,

View File

@@ -10,15 +10,15 @@
<script lang="ts" setup>
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { IK_HOST_STATS } from "@/types/injection-keys";
import { type ComputedRef, computed, inject } from "vue";
import UsageBar from "@/components/UsageBar.vue";
import { formatSize, parseRamUsage } from "@/libs/utils";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import NoDataError from "@/components/NoDataError.vue";
import { useHostStore } from "@/stores/host.store";
const { hasError } = useHostStore().subscribe();
const { hasError } = useHostCollection();
const stats = inject(
IK_HOST_STATS,

View File

@@ -17,10 +17,10 @@
<script lang="ts" setup>
import SizeStatsSummary from "@/components/ui/SizeStatsSummary.vue";
import { formatSize, getHostMemory } from "@/libs/utils";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { useHostMetricsCollection } from "@/stores/xen-api/host-metrics.store";
import { formatSize } from "@/libs/utils";
import { RRD_STEP_FROM_STRING } from "@/libs/xapi-stats";
import { useHostMetricsStore } from "@/stores/host-metrics.store";
import { useHostStore } from "@/stores/host.store";
import type { LinearChartData, ValueFormatter } from "@/types/chart";
import { IK_HOST_LAST_WEEK_STATS } from "@/types/injection-keys";
import { sumBy } from "lodash-es";
@@ -31,27 +31,22 @@ const LinearChart = defineAsyncComponent(
() => import("@/components/charts/LinearChart.vue")
);
const hostMetricsSubscription = useHostMetricsStore().subscribe();
const hostStore = useHostStore();
const { runningHosts } = hostStore.subscribe({ hostMetricsSubscription });
const { runningHosts } = useHostCollection();
const { getHostMemory } = useHostMetricsCollection();
const { t } = useI18n();
const hostLastWeekStats = inject(IK_HOST_LAST_WEEK_STATS);
const customMaxValue = computed(() =>
sumBy(
runningHosts.value,
(host) => getHostMemory(host, hostMetricsSubscription)?.size ?? 0
)
sumBy(runningHosts.value, (host) => getHostMemory(host)?.size ?? 0)
);
const currentData = computed(() => {
let size = 0,
usage = 0;
runningHosts.value.forEach((host) => {
const hostMemory = getHostMemory(host, hostMetricsSubscription);
const hostMemory = getHostMemory(host);
size += hostMemory?.size ?? 0;
usage += hostMemory?.usage ?? 0;
});

View File

@@ -12,13 +12,13 @@
import NoDataError from "@/components/NoDataError.vue";
import UiCardTitle from "@/components/ui/UiCardTitle.vue";
import UsageBar from "@/components/UsageBar.vue";
import { useVmCollection } from "@/stores/xen-api/vm.store";
import { formatSize, parseRamUsage } from "@/libs/utils";
import { useVmStore } from "@/stores/vm.store";
import { IK_VM_STATS } from "@/types/injection-keys";
import { N_ITEMS } from "@/views/pool/PoolDashboardView.vue";
import { computed, type ComputedRef, inject } from "vue";
const { hasError } = useVmStore().subscribe();
const { hasError } = useVmCollection();
const stats = inject(
IK_VM_STATS,

View File

@@ -34,9 +34,9 @@
<script lang="ts" setup>
import RelativeTime from "@/components/RelativeTime.vue";
import UiProgressBar from "@/components/ui/progress/UiProgressBar.vue";
import { useHostCollection } from "@/stores/xen-api/host.store";
import { parseDateTime } from "@/libs/utils";
import type { XenApiTask } from "@/libs/xen-api";
import { useHostStore } from "@/stores/host.store";
import type { XenApiTask } from "@/libs/xen-api/xen-api.types";
import { computed } from "vue";
const props = defineProps<{
@@ -44,7 +44,7 @@ const props = defineProps<{
task: XenApiTask;
}>();
const { getByOpaqueRef: getHost } = useHostStore().subscribe();
const { getByOpaqueRef: getHost } = useHostCollection();
const createdAt = computed(() => parseDateTime(props.task.created));

View File

@@ -1,5 +1,5 @@
<template>
<UiTable class="tasks-table" :color="hasError ? 'error' : undefined">
<UiTable :color="hasError ? 'error' : undefined" class="tasks-table">
<thead>
<tr>
<th>{{ $t("name") }}</th>
@@ -20,6 +20,9 @@
<UiSpinner class="loader" />
</td>
</tr>
<tr v-else-if="!hasTasks">
<td class="no-tasks" colspan="5">{{ $t("no-tasks") }}</td>
</tr>
<template v-else>
<TaskRow
v-for="task in pendingTasks"
@@ -35,20 +38,35 @@
<script lang="ts" setup>
import TaskRow from "@/components/tasks/TaskRow.vue";
import UiTable from "@/components/ui/UiTable.vue";
import UiSpinner from "@/components/ui/UiSpinner.vue";
import { useTaskStore } from "@/stores/task.store";
import type { XenApiTask } from "@/libs/xen-api";
import UiTable from "@/components/ui/UiTable.vue";
import { useTaskCollection } from "@/stores/xen-api/task.store";
import type { XenApiTask } from "@/libs/xen-api/xen-api.types";
import { computed } from "vue";
defineProps<{
const props = defineProps<{
pendingTasks: XenApiTask[];
finishedTasks: XenApiTask[];
finishedTasks?: XenApiTask[];
}>();
const { hasError, isFetching } = useTaskStore().subscribe();
const { hasError, isFetching } = useTaskCollection();
const hasTasks = computed(
() => props.pendingTasks.length > 0 || (props.finishedTasks?.length ?? 0) > 0
);
</script>
<style lang="postcss" scoped>
.tasks-table {
width: 100%;
}
.no-tasks {
text-align: center;
color: var(--color-blue-scale-300);
font-style: italic;
}
td[colspan="5"] {
text-align: center;
}

View File

@@ -1,28 +1,25 @@
<template>
<button
:class="{
busy: isBusy,
busy: busy,
disabled: isDisabled,
active,
'has-icon': icon !== undefined,
}"
:disabled="isBusy || isDisabled"
type="button"
:disabled="busy || isDisabled"
class="ui-action-button"
type="button"
>
<UiIcon :busy="isBusy" :icon="icon" />
<UiIcon :busy="busy" :icon="icon" />
<slot />
</button>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import {
IK_BUTTON_GROUP_BUSY,
IK_BUTTON_GROUP_DISABLED,
} from "@/types/injection-keys";
import { useContext } from "@/composables/context.composable";
import { DisabledContext } from "@/context";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { computed, inject } from "vue";
const props = withDefaults(
defineProps<{
@@ -31,20 +28,10 @@ const props = withDefaults(
icon?: IconDefinition;
active?: boolean;
}>(),
{ busy: undefined, disabled: undefined }
{ disabled: undefined }
);
const isGroupBusy = inject(
IK_BUTTON_GROUP_BUSY,
computed(() => false)
);
const isBusy = computed(() => props.busy ?? isGroupBusy.value);
const isGroupDisabled = inject(
IK_BUTTON_GROUP_DISABLED,
computed(() => false)
);
const isDisabled = computed(() => props.disabled ?? isGroupDisabled.value);
const isDisabled = useContext(DisabledContext, () => props.disabled);
</script>
<style lang="postcss" scoped>

View File

@@ -1,11 +1,11 @@
<template>
<button
:class="className"
:disabled="isBusy || isDisabled"
:disabled="busy || isDisabled"
:type="type || 'button'"
class="ui-button"
>
<UiSpinner v-if="isBusy" />
<UiSpinner v-if="busy" />
<template v-else>
<UiIcon :icon="icon" class="icon" />
<slot />
@@ -15,10 +15,9 @@
<script lang="ts" setup>
import UiSpinner from "@/components/ui/UiSpinner.vue";
import { useContext } from "@/composables/context.composable";
import { ColorContext, DisabledContext } from "@/context";
import {
IK_BUTTON_GROUP_BUSY,
IK_BUTTON_GROUP_COLOR,
IK_BUTTON_GROUP_DISABLED,
IK_BUTTON_GROUP_OUTLINED,
IK_BUTTON_GROUP_TRANSPARENT,
} from "@/types/injection-keys";
@@ -39,45 +38,31 @@ const props = withDefaults(
active?: boolean;
}>(),
{
busy: undefined,
disabled: undefined,
outlined: undefined,
transparent: undefined,
}
);
const isGroupBusy = inject(
IK_BUTTON_GROUP_BUSY,
computed(() => false)
);
const isBusy = computed(() => props.busy ?? isGroupBusy.value);
const isGroupDisabled = inject(
IK_BUTTON_GROUP_DISABLED,
computed(() => false)
);
const isDisabled = computed(() => props.disabled ?? isGroupDisabled.value);
const isDisabled = useContext(DisabledContext, () => props.disabled);
const isGroupOutlined = inject(
IK_BUTTON_GROUP_OUTLINED,
computed(() => false)
);
const isGroupTransparent = inject(
IK_BUTTON_GROUP_TRANSPARENT,
computed(() => false)
);
const buttonGroupColor = inject(
IK_BUTTON_GROUP_COLOR,
computed(() => "info")
);
const buttonColor = computed(() => props.color ?? buttonGroupColor.value);
const { name: contextColor } = useContext(ColorContext, () => props.color);
const className = computed(() => {
return [
`color-${buttonColor.value}`,
`color-${contextColor.value}`,
{
busy: isBusy.value,
busy: props.busy,
active: props.active,
disabled: isDisabled.value,
outlined: props.outlined ?? isGroupOutlined.value,

View File

@@ -5,35 +5,25 @@
</template>
<script lang="ts" setup>
import { useContext } from "@/composables/context.composable";
import { ColorContext, DisabledContext } from "@/context";
import type { Color } from "@/types";
import {
IK_BUTTON_GROUP_BUSY,
IK_BUTTON_GROUP_COLOR,
IK_BUTTON_GROUP_DISABLED,
IK_BUTTON_GROUP_OUTLINED,
IK_BUTTON_GROUP_TRANSPARENT,
} from "@/types/injection-keys";
import { computed, provide } from "vue";
const props = defineProps<{
busy?: boolean;
disabled?: boolean;
color?: Color;
outlined?: boolean;
transparent?: boolean;
merge?: boolean;
}>();
provide(
IK_BUTTON_GROUP_BUSY,
computed(() => props.busy ?? false)
);
provide(
IK_BUTTON_GROUP_DISABLED,
computed(() => props.disabled ?? false)
);
provide(
IK_BUTTON_GROUP_COLOR,
computed(() => props.color ?? "info")
const props = withDefaults(
defineProps<{
busy?: boolean;
disabled?: boolean;
color?: Color;
outlined?: boolean;
transparent?: boolean;
merge?: boolean;
}>(),
{ disabled: undefined }
);
provide(
IK_BUTTON_GROUP_OUTLINED,
@@ -43,13 +33,16 @@ provide(
IK_BUTTON_GROUP_TRANSPARENT,
computed(() => props.transparent ?? false)
);
useContext(ColorContext, () => props.color);
useContext(DisabledContext, () => props.disabled);
</script>
<style lang="postcss" scoped>
.ui-button-group {
display: flex;
align-items: center;
justify-content: left;
justify-content: center;
gap: 1rem;
&.merge {

View File

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

View File

@@ -6,6 +6,7 @@
class="left"
>
<slot>{{ left }}</slot>
<UiCounter class="count" v-if="count > 0" :value="count" color="info" />
</component>
<component
:is="subtitle ? 'h6' : 'h5'"
@@ -18,11 +19,17 @@
</template>
<script lang="ts" setup>
defineProps<{
subtitle?: boolean;
left?: string;
right?: string;
}>();
import UiCounter from "@/components/ui/UiCounter.vue";
withDefaults(
defineProps<{
subtitle?: boolean;
left?: string;
right?: string;
count?: number;
}>(),
{ count: 0 }
);
</script>
<style lang="postcss" scoped>
@@ -55,6 +62,9 @@ defineProps<{
font-size: var(--section-title-left-size);
font-weight: var(--section-title-left-weight);
color: var(--section-title-left-color);
display: flex;
align-items: center;
gap: 2rem;
}
.right {
@@ -62,4 +72,8 @@ defineProps<{
font-weight: var(--section-title-right-weight);
color: var(--section-title-right-color);
}
.count {
font-size: 1.6rem;
}
</style>

View File

@@ -1,157 +0,0 @@
<template>
<Teleport to="body">
<form
:class="className"
class="ui-modal"
v-bind="$attrs"
@click.self="emit('close')"
>
<div class="container">
<span v-if="onClose" class="close-icon" @click="emit('close')">
<UiIcon :icon="faXmark" />
</span>
<div v-if="icon || $slots.icon" class="modal-icon">
<slot name="icon">
<UiIcon :icon="icon" />
</slot>
</div>
<UiTitle v-if="$slots.title" type="h4">
<slot name="title" />
</UiTitle>
<div v-if="$slots.subtitle" class="subtitle">
<slot name="subtitle" />
</div>
<div v-if="$slots.default" class="content">
<slot />
</div>
<UiButtonGroup :color="color">
<slot name="buttons" />
</UiButtonGroup>
</div>
</form>
</Teleport>
</template>
<script lang="ts" setup>
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiTitle from "@/components/ui/UiTitle.vue";
import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { useMagicKeys, whenever } from "@vueuse/core";
import { computed } from "vue";
const props = withDefaults(
defineProps<{
icon?: IconDefinition;
color?: "info" | "warning" | "error" | "success";
onClose?: () => void;
}>(),
{ color: "info" }
);
const emit = defineEmits<{
(event: "close"): void;
}>();
const { escape } = useMagicKeys();
whenever(escape, () => emit("close"));
const className = computed(() => {
return [`color-${props.color}`, { "has-icon": props.icon !== undefined }];
});
</script>
<style lang="postcss" scoped>
.ui-modal {
position: fixed;
z-index: 2;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
overflow: auto;
align-items: center;
justify-content: center;
background-color: #00000080;
}
.color-success {
--modal-color: var(--color-green-infra-base);
--modal-background-color: var(--background-color-green-infra);
}
.color-info {
--modal-color: var(--color-extra-blue-base);
--modal-background-color: var(--background-color-extra-blue);
}
.color-warning {
--modal-color: var(--color-orange-world-base);
--modal-background-color: var(--background-color-orange-world);
}
.color-error {
--modal-color: var(--color-red-vates-base);
--modal-background-color: var(--background-color-red-vates);
}
.container {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
min-width: 40rem;
padding: 4.2rem;
text-align: center;
border-radius: 1rem;
background-color: var(--modal-background-color);
box-shadow: var(--shadow-400);
}
.close-icon {
font-size: 2rem;
position: absolute;
top: 1.5rem;
right: 2rem;
padding: 0.2rem 0.5rem;
cursor: pointer;
color: var(--modal-color);
}
.container :deep(.accent) {
color: var(--modal-color);
}
.modal-icon {
font-size: 4.8rem;
margin: 2rem 0;
color: var(--modal-color);
}
.ui-title {
margin-top: 4rem;
.has-icon & {
margin-top: 0;
}
}
.subtitle {
font-size: 1.6rem;
font-weight: 400;
color: var(--color-blue-scale-200);
}
.content {
overflow: auto;
font-size: 1.6rem;
max-height: calc(100vh - 40rem);
margin-top: 2rem;
}
.ui-button-group {
margin-top: 4rem;
}
</style>

View File

@@ -9,22 +9,19 @@
</template>
<script lang="ts" setup>
import { IK_TAB_BAR_DISABLED } from "@/types/injection-keys";
import { computed, inject } from "vue";
import { useContext } from "@/composables/context.composable";
import { DisabledContext } from "@/context";
withDefaults(
const props = withDefaults(
defineProps<{
disabled?: boolean;
active?: boolean;
tag?: string;
}>(),
{ tag: "span" }
{ tag: "span", disabled: undefined }
);
const isTabBarDisabled = inject(
IK_TAB_BAR_DISABLED,
computed(() => false)
);
const isTabBarDisabled = useContext(DisabledContext, () => props.disabled);
</script>
<style lang="postcss" scoped>

View File

@@ -5,17 +5,17 @@
</template>
<script lang="ts" setup>
import { IK_TAB_BAR_DISABLED } from "@/types/injection-keys";
import { computed, provide } from "vue";
import { useContext } from "@/composables/context.composable";
import { DisabledContext } from "@/context";
const props = defineProps<{
disabled?: boolean;
}>();
provide(
IK_TAB_BAR_DISABLED,
computed(() => props.disabled ?? false)
const props = withDefaults(
defineProps<{
disabled?: boolean;
}>(),
{ disabled: undefined }
);
useContext(DisabledContext, () => props.disabled);
</script>
<style lang="postcss" scoped>

View File

@@ -0,0 +1,28 @@
<template>
<UiIcon
:class="textClass"
:icon="faXmark"
class="modal-close-icon"
@click="close"
/>
</template>
<script lang="ts" setup>
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useContext } from "@/composables/context.composable";
import { ColorContext } from "@/context";
import { IK_MODAL_CLOSE } from "@/types/injection-keys";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { inject } from "vue";
const { textClass } = useContext(ColorContext);
const close = inject(IK_MODAL_CLOSE, undefined);
</script>
<style lang="postcss" scoped>
.modal-close-icon {
font-size: 2rem;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<component
:is="tag"
:class="[backgroundClass, { nested: isNested }]"
class="modal-container"
>
<header v-if="$slots.header" class="modal-header">
<slot name="header" />
</header>
<main v-if="$slots.default" class="modal-content">
<slot name="default" />
</main>
<footer v-if="$slots.footer" class="modal-footer">
<slot name="footer" />
</footer>
</component>
</template>
<script lang="ts" setup>
import { useContext } from "@/composables/context.composable";
import { ColorContext } from "@/context";
import type { Color } from "@/types";
import { IK_MODAL_NESTED } from "@/types/injection-keys";
import { inject, provide } from "vue";
const props = withDefaults(
defineProps<{
tag?: string;
color?: Color;
}>(),
{ tag: "div" }
);
defineSlots<{
header: () => any;
default: () => any;
footer: () => any;
}>();
const { backgroundClass } = useContext(ColorContext, () => props.color);
const isNested = inject(IK_MODAL_NESTED, false);
provide(IK_MODAL_NESTED, true);
</script>
<style lang="postcss" scoped>
.modal-container {
display: grid;
grid-template-rows: 1fr auto 1fr;
max-width: calc(100vw - 2rem);
max-height: calc(100vh - 20rem);
padding: 2rem;
gap: 1rem;
border-radius: 1rem;
font-size: 1.6rem;
&:not(.nested) {
min-width: 40rem;
box-shadow: var(--shadow-400);
}
&.nested {
margin-top: 2rem;
}
}
.modal-header {
grid-row: 1;
}
.modal-content {
text-align: center;
grid-row: 2;
padding: 2rem;
max-height: 75vh;
overflow: auto;
}
.modal-footer {
grid-row: 3;
align-self: end;
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<Teleport to="body">
<div v-if="isOpen" class="ui-modal" @click.self="close">
<slot />
</div>
</Teleport>
</template>
<script lang="ts" setup>
import { useContext } from "@/composables/context.composable";
import { ColorContext } from "@/context";
import type { Color } from "@/types";
import { IK_MODAL_CLOSE } from "@/types/injection-keys";
import { useMagicKeys, useVModel, whenever } from "@vueuse/core/index";
import { provide } from "vue";
const props = defineProps<{
modelValue: boolean;
color?: Color;
}>();
const emit = defineEmits<{
(event: "update:modelValue", value: boolean): void;
}>();
const isOpen = useVModel(props, "modelValue", emit);
const close = () => (isOpen.value = false);
provide(IK_MODAL_CLOSE, close);
useContext(ColorContext, () => props.color);
const { escape } = useMagicKeys();
whenever(escape, () => close());
</script>
<style lang="postcss" scoped>
.ui-modal {
position: fixed;
z-index: 2;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
overflow: auto;
align-items: center;
justify-content: center;
background: rgba(26, 27, 56, 0.25);
flex-direction: column;
gap: 2rem;
font-size: 1.6rem;
font-weight: 400;
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<ModalContainer>
<template #header>
<ModalCloseIcon class="close-icon" />
</template>
<template #default>
<slot />
</template>
</ModalContainer>
</template>
<script lang="ts" setup>
import ModalCloseIcon from "@/components/ui/modals/ModalCloseIcon.vue";
import ModalContainer from "@/components/ui/modals/ModalContainer.vue";
defineSlots<{
default: () => void;
}>();
</script>
<style lang="postcss" scoped>
.close-icon {
float: right;
}
</style>

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