diff --git a/.prettierignore b/.prettierignore
index a50140d249..2c6de51e84 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -4,3 +4,4 @@
# Generated files
/@xen-orchestra/web/src/route-map.d.ts
+/@xen-orchestra/lite/src/route-map.d.ts
diff --git a/@xen-orchestra/lite/CHANGELOG.md b/@xen-orchestra/lite/CHANGELOG.md
index dcf3ae79f7..8230328084 100644
--- a/@xen-orchestra/lite/CHANGELOG.md
+++ b/@xen-orchestra/lite/CHANGELOG.md
@@ -7,6 +7,7 @@
- Update the UiCard component to use the one from web-core (PR [#9980](https://github.com/vatesfr/xen-orchestra/pull/9980))
- Update the UiCardTitle component to use the one from web-core (PR [#9982](https://github.com/vatesfr/xen-orchestra/pull/9982))
- Replacement of the UiSeparator component with VtsDivider from web-core (PR [#10017](https://github.com/vatesfr/xen-orchestra/pull/10017))
+- Update side panels (PR [#9836](https://github.com/vatesfr/xen-orchestra/pull/9836))
## **0.22.0** (2026-05-28)
diff --git a/@xen-orchestra/lite/src/components/host/network/HostPifSidePanel.vue b/@xen-orchestra/lite/src/components/host/network/HostPifSidePanel.vue
index f8b35821d6..8163f9c8de 100644
--- a/@xen-orchestra/lite/src/components/host/network/HostPifSidePanel.vue
+++ b/@xen-orchestra/lite/src/components/host/network/HostPifSidePanel.vue
@@ -1,41 +1,28 @@
-
-
-
-
-
-
- {{ t('action:edit') }}
-
-
- {{ t('action:delete') }}
-
-
-
+
+
+
+ {{ t('action:edit') }}
+
+
+ {{ t('action:delete') }}
+
-
+
{{ isBond ? t('bond') : t('pif') }}
@@ -282,7 +269,7 @@
-
+
diff --git a/@xen-orchestra/lite/src/components/pool/network/PoolNetworkSidePanel.vue b/@xen-orchestra/lite/src/components/pool/network/PoolNetworkSidePanel.vue
index 4ea347d0e1..7054bd5ca4 100644
--- a/@xen-orchestra/lite/src/components/pool/network/PoolNetworkSidePanel.vue
+++ b/@xen-orchestra/lite/src/components/pool/network/PoolNetworkSidePanel.vue
@@ -1,42 +1,28 @@
-
-
-
-
-
-
- {{ t('action:edit') }}
-
-
- {{ t('action:delete') }}
-
-
-
-
+
+
+
+ {{ t('action:edit') }}
+
+
+ {{ t('action:delete') }}
+
-
+
{{ network.name_label }}
@@ -113,7 +99,7 @@
-
+
diff --git a/@xen-orchestra/lite/src/pages/host/[uuid]/network.vue b/@xen-orchestra/lite/src/pages/host/[uuid]/network.vue
index 801ad8df57..d4a50b74ab 100644
--- a/@xen-orchestra/lite/src/pages/host/[uuid]/network.vue
+++ b/@xen-orchestra/lite/src/pages/host/[uuid]/network.vue
@@ -1,15 +1,10 @@
-
+
-
-
-
- {{ t('select-to-see-details') }}
-
-
-
+
+
diff --git a/@xen-orchestra/lite/src/stories/web-core/state-hero/vts-state-hero.story.vue b/@xen-orchestra/lite/src/stories/web-core/state-hero/vts-state-hero.story.vue
index 9d5bc7482d..a0a98a780f 100644
--- a/@xen-orchestra/lite/src/stories/web-core/state-hero/vts-state-hero.story.vue
+++ b/@xen-orchestra/lite/src/stories/web-core/state-hero/vts-state-hero.story.vue
@@ -7,17 +7,7 @@
prop('horizontal').bool().widget(),
prop('imageSize').enum('small', 'medium', 'large').preset('medium').widget(),
prop('type')
- .enum(
- 'no-result',
- 'under-construction',
- 'no-data',
- 'no-selection',
- 'error',
- 'not-found',
- 'offline',
- 'all-good',
- 'all-done'
- )
+ .enum(...STATE_HERO_TYPES)
.widget(),
prop('noBackground').bool().widget(),
slot(),
@@ -34,4 +24,5 @@
import ComponentStory from '@/components/component-story/ComponentStory.vue'
import { prop, setting, slot } from '@/libs/story/story-param'
import VtsStateHero from '@core/components/state-hero/VtsStateHero.vue'
+import { STATE_HERO_TYPES } from '@core/types/state-hero.type.ts'
diff --git a/@xen-orchestra/lite/src/stories/web-core/ui/panel/ui-panel.story.vue b/@xen-orchestra/lite/src/stories/web-core/ui/panel/ui-panel.story.vue
index 5613a6a192..c14449cdf1 100644
--- a/@xen-orchestra/lite/src/stories/web-core/ui/panel/ui-panel.story.vue
+++ b/@xen-orchestra/lite/src/stories/web-core/ui/panel/ui-panel.story.vue
@@ -2,54 +2,26 @@
-
-
- Toggle
- {{ settings.action1 }}
-
- {{ settings.action2 }}
-
-
-
-
- Content 1
- Content 1
- Content 1
+
+ {{ settings.headerContent }}
+
+ Card content
-
+
diff --git a/@xen-orchestra/web-core/lib/components/layout/VtsContentSidePanel.vue b/@xen-orchestra/web-core/lib/components/layout/VtsContentSidePanel.vue
new file mode 100644
index 0000000000..0374b49335
--- /dev/null
+++ b/@xen-orchestra/web-core/lib/components/layout/VtsContentSidePanel.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
diff --git a/@xen-orchestra/web-core/lib/components/operation-card/VtsOperationCard.vue b/@xen-orchestra/web-core/lib/components/operation-card/VtsOperationCard.vue
index f13cf9227f..776392642f 100644
--- a/@xen-orchestra/web-core/lib/components/operation-card/VtsOperationCard.vue
+++ b/@xen-orchestra/web-core/lib/components/operation-card/VtsOperationCard.vue
@@ -12,9 +12,10 @@
+
+
diff --git a/@xen-orchestra/web-core/lib/components/panel/VtsSidePanel.vue b/@xen-orchestra/web-core/lib/components/panel/VtsSidePanel.vue
new file mode 100644
index 0000000000..d0ef25b060
--- /dev/null
+++ b/@xen-orchestra/web-core/lib/components/panel/VtsSidePanel.vue
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/@xen-orchestra/web-core/lib/components/state-hero/VtsStateHero.vue b/@xen-orchestra/web-core/lib/components/state-hero/VtsStateHero.vue
index 253aa87125..9ab590cce0 100644
--- a/@xen-orchestra/web-core/lib/components/state-hero/VtsStateHero.vue
+++ b/@xen-orchestra/web-core/lib/components/state-hero/VtsStateHero.vue
@@ -9,32 +9,24 @@
{{ t('all-good!') }}
+
+
+
+
+
+
+
diff --git a/@xen-orchestra/web-core/lib/composables/table-state.composable.ts b/@xen-orchestra/web-core/lib/composables/table-state.composable.ts
index c591645a2d..89eebe129e 100644
--- a/@xen-orchestra/web-core/lib/composables/table-state.composable.ts
+++ b/@xen-orchestra/web-core/lib/composables/table-state.composable.ts
@@ -1,5 +1,5 @@
-import type { StateHeroType } from '@core/components/state-hero/VtsStateHero.vue'
import type { TableState } from '@core/components/table/VtsTable.vue'
+import type { StateHeroType } from '@core/types/state-hero.type.ts'
import { computed, toValue, type MaybeRefOrGetter } from 'vue'
import { useI18n } from 'vue-i18n'
diff --git a/@xen-orchestra/web-core/lib/locales/en.json b/@xen-orchestra/web-core/lib/locales/en.json
index bf5d61969e..0d0d2403b0 100644
--- a/@xen-orchestra/web-core/lib/locales/en.json
+++ b/@xen-orchestra/web-core/lib/locales/en.json
@@ -886,7 +886,7 @@
"select-compression": "Select a compression",
"select-destination-host": "Select a destination host",
"select-host": "Select host",
- "select-to-see-details": "Select an element to see details",
+ "select-to-see-details": "Click on {icon} to see details",
"selected-vms-in-execution": "Some selected VMs are running",
"self-signed-certificates": "Self-signed certificates",
"send-us-feedback": "Send us feedback",
diff --git a/@xen-orchestra/web-core/lib/locales/fr.json b/@xen-orchestra/web-core/lib/locales/fr.json
index e5015d9bf1..1c000216f8 100644
--- a/@xen-orchestra/web-core/lib/locales/fr.json
+++ b/@xen-orchestra/web-core/lib/locales/fr.json
@@ -886,7 +886,7 @@
"select-compression": "Sélectionnez une compression",
"select-destination-host": "Sélectionnez un hôte de destination",
"select-host": "Sélectionnez un hôte",
- "select-to-see-details": "Sélectionnez un élément pour voir les détails",
+ "select-to-see-details": "Cliquez sur {icon} pour voir les détails",
"selected-vms-in-execution": "Certaines VMs sélectionnées sont en cours d'exécution",
"self-signed-certificates": "Certificats auto-signés",
"send-us-feedback": "Envoyez-nous vos commentaires",
diff --git a/@xen-orchestra/web-core/lib/stores/panel.store.ts b/@xen-orchestra/web-core/lib/stores/panel.store.ts
index 4f6e7eaa2d..6f7a48baa0 100644
--- a/@xen-orchestra/web-core/lib/stores/panel.store.ts
+++ b/@xen-orchestra/web-core/lib/stores/panel.store.ts
@@ -1,12 +1,63 @@
import { useUiStore } from '@core/stores/ui.store'
+import { useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
-import { computed, ref } from 'vue'
+import { computed, watch } from 'vue'
export const usePanelStore = defineStore('panel', () => {
const uiStore = useUiStore()
- const isExpanded = ref(false)
- const open = () => (isExpanded.value = true)
- const close = () => (isExpanded.value = false)
+ const isExpanded = useLocalStorage('panel.expanded', true)
+ const isLocked = useLocalStorage('panel.locked', true)
- return { open, close, isExpanded: computed(() => !uiStore.isSmall || isExpanded.value) }
+ const cssHorizontalOffset = computed(() => (isExpanded.value ? 0 : '100%'))
+ /* Panel closes automatically when it has no content (true on small UI or when not locked) */
+ const syncsOpenStateWithSelection = computed(() => uiStore.isSmall || !isLocked.value)
+
+ function expand() {
+ isExpanded.value = true
+ }
+
+ function collapse() {
+ isExpanded.value = false
+ }
+
+ function syncWithSelection(hasSelection: boolean) {
+ if (!syncsOpenStateWithSelection.value) {
+ return
+ }
+ if (!hasSelection && isExpanded.value) {
+ collapse()
+ } else if (hasSelection && !isExpanded.value) {
+ expand()
+ }
+ }
+
+ function toggleLock() {
+ const next = !isLocked.value
+ isLocked.value = next
+ if (next && !uiStore.isSmall && !isExpanded.value) {
+ expand()
+ }
+ }
+
+ watch(
+ () => uiStore.isSmall,
+ isSmall => {
+ /* Expand when coming back from small to large UI, if locked and not expanded */
+ if (!isSmall && isLocked.value && !isExpanded.value) {
+ expand()
+ }
+ },
+ { immediate: true }
+ )
+
+ return {
+ isExpanded,
+ isLocked,
+ syncsOpenStateWithSelection,
+ expand,
+ collapse,
+ toggleLock,
+ syncWithSelection,
+ cssHorizontalOffset,
+ }
})
diff --git a/@xen-orchestra/web-core/lib/types/state-hero.type.ts b/@xen-orchestra/web-core/lib/types/state-hero.type.ts
new file mode 100644
index 0000000000..3bd0550507
--- /dev/null
+++ b/@xen-orchestra/web-core/lib/types/state-hero.type.ts
@@ -0,0 +1,19 @@
+export const STATE_HERO_TYPES = [
+ 'busy',
+ 'no-result',
+ 'under-construction',
+ 'no-data',
+ 'no-selection',
+ 'error',
+ 'not-found',
+ 'offline',
+ 'all-good',
+ 'all-done',
+ 'creating',
+] as const
+
+export type StateHeroType = (typeof STATE_HERO_TYPES)[number]
+
+export type StateHeroFormat = 'page' | 'card' | 'panel' | 'table'
+
+export type StateHeroSize = 'extra-small' | 'small' | 'medium' | 'large'
diff --git a/@xen-orchestra/web/src/modules/backup/components/jobs/panel/BackupJobSidePanel.vue b/@xen-orchestra/web/src/modules/backup/components/jobs/panel/BackupJobSidePanel.vue
index 010227b0ca..05d6b6b68c 100644
--- a/@xen-orchestra/web/src/modules/backup/components/jobs/panel/BackupJobSidePanel.vue
+++ b/@xen-orchestra/web/src/modules/backup/components/jobs/panel/BackupJobSidePanel.vue
@@ -1,18 +1,6 @@
-
-
-
-
-
-
-
+
+
@@ -22,7 +10,7 @@
-
+
-
-
+
+ const settings =
+ backupJob.type === 'backup'
+ ? getVmBackupJobSettings(backupJob)
+ : backupJob.type === 'metadataBackup'
+ ? getMetadataBackupJobSettings(backupJob)
+ : getMirrorBackupJobSettings(backupJob)
+
+ return Object.values(settings).some(value => value !== undefined)
+})
+
diff --git a/@xen-orchestra/web/src/modules/backup/components/logs/panel/BackupLogSidePanel.vue b/@xen-orchestra/web/src/modules/backup/components/logs/panel/BackupLogSidePanel.vue
index f0c79b7ea6..31a1a2be77 100644
--- a/@xen-orchestra/web/src/modules/backup/components/logs/panel/BackupLogSidePanel.vue
+++ b/@xen-orchestra/web/src/modules/backup/components/logs/panel/BackupLogSidePanel.vue
@@ -1,18 +1,6 @@
-
-
-
-
-
-
-
+
+
@@ -23,7 +11,7 @@
-
+
-
-
diff --git a/@xen-orchestra/web/src/modules/host/components/list/panel/HostSidePanel.vue b/@xen-orchestra/web/src/modules/host/components/list/panel/HostSidePanel.vue
index a119b45392..c22f34eda3 100644
--- a/@xen-orchestra/web/src/modules/host/components/list/panel/HostSidePanel.vue
+++ b/@xen-orchestra/web/src/modules/host/components/list/panel/HostSidePanel.vue
@@ -1,18 +1,6 @@
-
-
-
-
-
-
-
+
+
@@ -20,7 +8,7 @@
-
+
-
-
diff --git a/@xen-orchestra/web/src/modules/network/components/panel/NetworkSidePanel.vue b/@xen-orchestra/web/src/modules/network/components/panel/NetworkSidePanel.vue
index 04112a4fe8..993aa5d6ed 100644
--- a/@xen-orchestra/web/src/modules/network/components/panel/NetworkSidePanel.vue
+++ b/@xen-orchestra/web/src/modules/network/components/panel/NetworkSidePanel.vue
@@ -1,19 +1,9 @@
-
-
-
-
-
-
+
+
+
-
+
{{ network.name_label }}
@@ -68,7 +58,7 @@
-
+
diff --git a/@xen-orchestra/web/src/modules/pif/components/list/panel/PifSidePanel.vue b/@xen-orchestra/web/src/modules/pif/components/list/panel/PifSidePanel.vue
index 417e434a0b..a1204b6aaf 100644
--- a/@xen-orchestra/web/src/modules/pif/components/list/panel/PifSidePanel.vue
+++ b/@xen-orchestra/web/src/modules/pif/components/list/panel/PifSidePanel.vue
@@ -1,18 +1,6 @@
-
-
-
-
-
-
-
+
+
{{ pif.isBondMaster ? t('bond') : t('pif') }}
@@ -247,7 +235,7 @@
-
+
-
-
diff --git a/@xen-orchestra/web/src/modules/snapshot/composables/use-vm-snapshot-revert-modal.composable.ts b/@xen-orchestra/web/src/modules/snapshot/composables/use-vm-snapshot-revert-modal.composable.ts
index 549f5612f3..e22cc3e637 100644
--- a/@xen-orchestra/web/src/modules/snapshot/composables/use-vm-snapshot-revert-modal.composable.ts
+++ b/@xen-orchestra/web/src/modules/snapshot/composables/use-vm-snapshot-revert-modal.composable.ts
@@ -4,7 +4,7 @@ import { useModal } from '@core/packages/modal/use-modal.ts'
import { toComputed } from '@core/utils/to-computed.util.ts'
import { type MaybeRefOrGetter, ref } from 'vue'
-export function useVmSnapshotRevertModal(rawSnapshot: MaybeRefOrGetter) {
+export function useVmSnapshotRevertModal(rawSnapshot: MaybeRefOrGetter) {
const snapshot = toComputed(rawSnapshot)
const snapshotBefore = ref(true)
diff --git a/@xen-orchestra/web/src/modules/storage-repository/components/list/panel/StorageRepositorySidePanel.vue b/@xen-orchestra/web/src/modules/storage-repository/components/list/panel/StorageRepositorySidePanel.vue
index 3599c4e51b..e3d8f8f019 100644
--- a/@xen-orchestra/web/src/modules/storage-repository/components/list/panel/StorageRepositorySidePanel.vue
+++ b/@xen-orchestra/web/src/modules/storage-repository/components/list/panel/StorageRepositorySidePanel.vue
@@ -1,38 +1,27 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+
-
-
diff --git a/@xen-orchestra/web/src/modules/storage-repository/composables/xo-sr-utils.composable.ts b/@xen-orchestra/web/src/modules/storage-repository/composables/xo-sr-utils.composable.ts
index a718c69d5b..052d60580c 100644
--- a/@xen-orchestra/web/src/modules/storage-repository/composables/xo-sr-utils.composable.ts
+++ b/@xen-orchestra/web/src/modules/storage-repository/composables/xo-sr-utils.composable.ts
@@ -31,7 +31,11 @@ export function useGetPbdsInScope() {
return getPbdsInScope(sr, scope).filter(pbd => !pbd.attached)
}
- function getSrPbdsSignature(sr: FrontXoSr, scope: SrScope) {
+ function getSrPbdsSignature(sr: FrontXoSr | undefined, scope: SrScope) {
+ if (sr === undefined) {
+ return scope.type === 'host' ? `host:${scope.hostId}` : 'pool'
+ }
+
const scopedPbds = getPbdsInScope(sr, scope)
return scopedPbds.map(pbd => `${pbd.id}:${pbd.attached}`).join('|') || sr.id
diff --git a/@xen-orchestra/web/src/modules/task/components/list/panel/TaskSidePanel.vue b/@xen-orchestra/web/src/modules/task/components/list/panel/TaskSidePanel.vue
index 4b3ac751bb..78f919b271 100644
--- a/@xen-orchestra/web/src/modules/task/components/list/panel/TaskSidePanel.vue
+++ b/@xen-orchestra/web/src/modules/task/components/list/panel/TaskSidePanel.vue
@@ -1,18 +1,6 @@
-
-
-
-
-
-
-
+
+
@@ -20,7 +8,7 @@
-
+
-
-
diff --git a/@xen-orchestra/web/src/modules/task/composables/xo-task-properties-utils.composable.ts b/@xen-orchestra/web/src/modules/task/composables/xo-task-properties-utils.composable.ts
index 89c18738c9..2749786db1 100644
--- a/@xen-orchestra/web/src/modules/task/composables/xo-task-properties-utils.composable.ts
+++ b/@xen-orchestra/web/src/modules/task/composables/xo-task-properties-utils.composable.ts
@@ -2,15 +2,17 @@ import type { FrontXoTask } from '@/modules/task/remote-resources/use-xo-task-co
import { toComputed } from '@core/utils/to-computed.util.ts'
import { computed, type MaybeRefOrGetter } from 'vue'
-export function useXoTaskPropertiesUtils(rawTask: MaybeRefOrGetter) {
+export function useXoTaskPropertiesUtils(rawTask: MaybeRefOrGetter) {
const task = toComputed(rawTask)
const properties = computed(() => {
- if (!task.value.properties) {
+ const taskProperties = task.value?.properties
+
+ if (!taskProperties) {
return {}
}
- const { method, name, type, objectId, progress, userId, ...other } = task.value.properties
+ const { method, name, type, objectId, progress, userId, ...other } = taskProperties
return {
method,
diff --git a/@xen-orchestra/web/src/modules/traffic-rules/components/list/panel/TrafficRulesSidePanel.vue b/@xen-orchestra/web/src/modules/traffic-rules/components/list/panel/TrafficRulesSidePanel.vue
index d860476f72..a7dcf1f594 100644
--- a/@xen-orchestra/web/src/modules/traffic-rules/components/list/panel/TrafficRulesSidePanel.vue
+++ b/@xen-orchestra/web/src/modules/traffic-rules/components/list/panel/TrafficRulesSidePanel.vue
@@ -1,36 +1,26 @@
-
-
-
-
+
+
-
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
+
-
+
-
-
diff --git a/@xen-orchestra/web/src/modules/vdi/components/list/panel/VdiSidePanel.vue b/@xen-orchestra/web/src/modules/vdi/components/list/panel/VdiSidePanel.vue
index f86951ead6..a13e0a02b3 100644
--- a/@xen-orchestra/web/src/modules/vdi/components/list/panel/VdiSidePanel.vue
+++ b/@xen-orchestra/web/src/modules/vdi/components/list/panel/VdiSidePanel.vue
@@ -1,35 +1,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
+
+
+
+
-
+
-
-
diff --git a/@xen-orchestra/web/src/modules/vif/components/panel/VifSidePanel.vue b/@xen-orchestra/web/src/modules/vif/components/panel/VifSidePanel.vue
index 56173244b9..ec4eaa1b05 100644
--- a/@xen-orchestra/web/src/modules/vif/components/panel/VifSidePanel.vue
+++ b/@xen-orchestra/web/src/modules/vif/components/panel/VifSidePanel.vue
@@ -1,28 +1,13 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
+
+
+
+
{{ t('vif') }}
@@ -143,7 +128,7 @@
-
+
diff --git a/@xen-orchestra/web/src/modules/vm/components/list/panel/VmSidePanel.vue b/@xen-orchestra/web/src/modules/vm/components/list/panel/VmSidePanel.vue
index e6a71b8007..5403340709 100644
--- a/@xen-orchestra/web/src/modules/vm/components/list/panel/VmSidePanel.vue
+++ b/@xen-orchestra/web/src/modules/vm/components/list/panel/VmSidePanel.vue
@@ -1,23 +1,11 @@
-
-
-
-
-
-
-
+
+
-
+
-
-
diff --git a/@xen-orchestra/web/src/pages/(site)/backups.vue b/@xen-orchestra/web/src/pages/(site)/backups.vue
index 8fa585daf2..2cfd4c125f 100644
--- a/@xen-orchestra/web/src/pages/(site)/backups.vue
+++ b/@xen-orchestra/web/src/pages/(site)/backups.vue
@@ -1,19 +1,10 @@
-
+
-
-
-
- {{ t('select-to-see-details') }}
-
-
-
+
+