Skip to content

Commit

Permalink
feat(xo-lite): display host console (#8136)
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastianCzechDev authored Nov 26, 2024
1 parent e80b39e commit 904273f
Show file tree
Hide file tree
Showing 15 changed files with 326 additions and 21 deletions.
1 change: 1 addition & 0 deletions @xen-orchestra/lite/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [Header] Update user menu button (PR [#8109](https://github.com/vatesfr/xen-orchestra/pull/8109))
- [VM/Console] Display _Console Clipboard_ and _Console Actions_ (PR [#8125](https://github.com/vatesfr/xen-orchestra/pull/8125))
- [i18n] Add Czech translation (contribution made by [@p-bo](https://github.com/p-bo)) (PR [#8098](https://github.com/vatesfr/xen-orchestra/pull/8098))
- [Host/Console] Display _Console_, _Console Clipboard_ and _Console Actions_ (PR [#8136](https://github.com/vatesfr/xen-orchestra/pull/8136))

## **0.5.0** (2024-10-31)

Expand Down
2 changes: 1 addition & 1 deletion @xen-orchestra/lite/src/components/ObjectLink.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const props = defineProps<{
}>()
const config: Config = {
host: { context: useHostStore().subscribe({ defer: true }), routeName: 'host.dashboard' },
host: { context: useHostStore().subscribe({ defer: true }), routeName: 'host.console' },
vm: { context: useVmStore().subscribe({ defer: true }), routeName: 'vm.console' },
sr: { context: useSrStore().subscribe({ defer: true }), routeName: undefined },
pool: { context: usePoolStore().subscribe({ defer: true }), routeName: 'pool.dashboard' },
Expand Down
27 changes: 27 additions & 0 deletions @xen-orchestra/lite/src/components/host/HostHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<template>
<UiHeadBar>
<template #icon>
<UiObjectIcon size="medium" type="host" :state="getHostPowerState as HostState" />
</template>
{{ host.name_label }}
</UiHeadBar>
</template>

<script lang="ts" setup>
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import { useHostStore } from '@/stores/xen-api/host.store'
import type { HostState } from '@core/types/object-icon.type'
import UiHeadBar from '@core/components/ui/head-bar/UiHeadBar.vue'
import UiObjectIcon from '@core/components/ui/object-icon/UiObjectIcon.vue'
import { computed } from 'vue'
const props = defineProps<{
host: XenApiHost
}>()
const { runningHosts } = useHostStore().subscribe()
const getHostPowerState = computed(() => {
const isHostRunning = runningHosts.value.some(runningHost => runningHost.uuid === props.host.uuid)
return isHostRunning ? 'running' : 'halted'
})
</script>
28 changes: 28 additions & 0 deletions @xen-orchestra/lite/src/components/host/HostTabBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<template>
<TabList>
<RouterTab :to="{ name: 'host.dashboard', params: { uuid } }" disabled>
{{ $t('dashboard') }}
</RouterTab>
<RouterTab :to="{ name: 'host.console', params: { uuid } }">
{{ $t('console') }}
</RouterTab>
<RouterTab :to="{ name: 'host.network', params: { uuid } }" disabled>
{{ $t('network') }}
</RouterTab>
<RouterTab :to="{ name: 'host.tasks', params: { uuid } }" disabled>
{{ $t('tasks') }}
</RouterTab>
<RouterTab :to="{ name: 'host.vms', params: { uuid } }" disabled>
{{ $t('vms') }}
</RouterTab>
</TabList>
</template>

<script lang="ts" setup>
import RouterTab from '@/components/RouterTab.vue'
import TabList from '@core/components/tab/TabList.vue'
defineProps<{
uuid: string
}>()
</script>
6 changes: 1 addition & 5 deletions @xen-orchestra/lite/src/components/infra/InfraHostItem.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
<template>
<VtsTreeItem v-if="host !== undefined" :expanded="isExpanded" class="infra-host-item">
<UiTreeItemLabel
:icon="faServer"
:route="{ name: 'host.dashboard', params: { uuid: host.uuid } }"
@toggle="toggle()"
>
<UiTreeItemLabel :icon="faServer" :route="{ name: 'host.console', params: { uuid: host.uuid } }" @toggle="toggle()">
{{ host.name_label || '(Host)' }}
<template #addons>
<UiIcon v-if="isPoolMaster" v-tooltip="$t('master')" :icon="faStar" accent="warning" />
Expand Down
8 changes: 8 additions & 0 deletions @xen-orchestra/lite/src/libs/host.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { HOST_OPERATION } from '@/libs/xen-api/xen-api.enums'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import { useXenApiStore } from '@/stores/xen-api.store'
import type { XenApiPatch } from '@/types/xen-api'
import { castArray } from 'lodash-es'

export async function fetchMissingHostPatches(hostRef: XenApiHost['$ref']): Promise<XenApiPatch[]> {
const xenApiStore = useXenApiStore()
Expand All @@ -16,3 +18,9 @@ export async function fetchMissingHostPatches(hostRef: XenApiHost['$ref']): Prom
$id: `${rawPatch.name}-${rawPatch.version}`,
}))
}

export const isHostOperationPending = (host: XenApiHost, operations: HOST_OPERATION[] | HOST_OPERATION) => {
const currentOperations = Object.values(host.current_operations)

return castArray(operations).some(operation => currentOperations.includes(operation))
}
4 changes: 4 additions & 0 deletions @xen-orchestra/lite/src/libs/xen-api/xen-api.enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ export enum ON_NORMAL_EXIT {
RESTART = 'restart',
}

export enum HOST_OPERATION {
SHUTDOWN = 'shutdown',
}

export enum VM_OPERATION {
ASSERT_OPERATION_VALID = 'assert_operation_valid',
AWAITING_MEMORY_LIVE = 'awaiting_memory_live',
Expand Down
2 changes: 2 additions & 0 deletions @xen-orchestra/lite/src/libs/xen-api/xen-api.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
ALLOCATION_ALGORITHM,
BOND_MODE,
DOMAIN_TYPE,
HOST_OPERATION,
IP_CONFIGURATION_MODE,
IPV6_CONFIGURATION_MODE,
NETWORK_DEFAULT_LOCKING_MODE,
Expand Down Expand Up @@ -111,6 +112,7 @@ export interface XenApiHost extends XenApiRecord<'host'> {
cpu_info: { cpu_count: string }
software_version: { product_version: string }
control_domain: XenApiVm['$ref']
current_operations: Record<string, HOST_OPERATION>
}

export interface XenApiSr extends XenApiRecord<'sr'> {
Expand Down
32 changes: 32 additions & 0 deletions @xen-orchestra/lite/src/router/host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export default {
path: '/host/:uuid',
component: () => import('@/views/host/HostRootView.vue'),
redirect: { name: 'host.console' },
children: [
{
path: 'dashboard',
name: 'host.dashboard',
component: () => import('@/views/host/HostDashboardView.vue'),
},
{
path: 'console',
name: 'host.console',
component: () => import('@/views/host/HostConsoleView.vue'),
},
{
path: 'network',
name: 'host.network',
component: () => import('@/views/host/HostNetworkView.vue'),
},
{
path: 'tasks',
name: 'host.tasks',
component: () => import('@/views/host/HostTasksView.vue'),
},
{
path: 'vms',
name: 'host.vms',
component: () => import('@/views/host/HostVmsView.vue'),
},
],
}
13 changes: 2 additions & 11 deletions @xen-orchestra/lite/src/router/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import host from '@/router/host'
import pool from '@/router/pool'
import story from '@/router/story'
import vm from '@/router/vm'
Expand Down Expand Up @@ -25,17 +26,7 @@ const router = createRouter({
story,
pool,
vm,
{
path: '/host/:uuid',
component: () => import('@/views/host/HostRootView.vue'),
children: [
{
path: '',
name: 'host.dashboard',
component: () => import('@/views/host/HostDashboardView.vue'),
},
],
},
host,
{
path: '/:pathMatch(.*)*',
name: 'not-found',
Expand Down
178 changes: 178 additions & 0 deletions @xen-orchestra/lite/src/views/host/HostConsoleView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<template>
<div :class="{ 'no-ui': !uiStore.hasUi }" class="host-console-view">
<div v-if="hasError">{{ $t('error-occurred') }}</div>
<UiSpinner v-else-if="!isReady" class="spinner" />
<UiStatusPanel v-else-if="!isHostRunning" :image-source="monitor" :title="$t('power-on-host-for-console')" />
<template v-else-if="host && hostConsole">
<VtsLayoutConsole>
<RemoteConsole
ref="consoleElement"
:is-console-available="isConsoleAvailable"
:location="hostConsole.location"
class="remote-console"
/>
<template #actions>
<VtsActionsConsole
:open-in-new-tab="openInNewTab"
:send-ctrl-alt-del="sendCtrlAltDel"
:toggle-full-screen="toggleFullScreen"
:is-fullscreen="!uiStore.hasUi"
/>
<VtsDivider type="stretch" />
<VtsClipboardConsole />
</template>
</VtsLayoutConsole>
</template>
</div>
</template>

<script lang="ts" setup>
import monitor from '@/assets/monitor.svg'
import RemoteConsole from '@/components/RemoteConsole.vue'
import UiSpinner from '@/components/ui/UiSpinner.vue'
import UiStatusPanel from '@/components/ui/UiStatusPanel.vue'
import { isHostOperationPending } from '@/libs/host'
import { HOST_OPERATION } from '@/libs/xen-api/xen-api.enums'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import { usePageTitleStore } from '@/stores/page-title.store'
import { useConsoleStore } from '@/stores/xen-api/console.store'
import { useControlDomainStore } from '@/stores/xen-api/control-domain.store'
import { useHostStore } from '@/stores/xen-api/host.store'
import VtsActionsConsole from '@core/components/console/VtsActionsConsole.vue'
import VtsClipboardConsole from '@core/components/console/VtsClipboardConsole.vue'
import VtsLayoutConsole from '@core/components/console/VtsLayoutConsole.vue'
import VtsDivider from '@core/components/divider/VtsDivider.vue'
import { useUiStore } from '@core/stores/ui.store'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
const STOP_OPERATIONS = [HOST_OPERATION.SHUTDOWN]
usePageTitleStore().setTitle(useI18n().t('console'))
const router = useRouter()
const route = useRoute()
const uiStore = useUiStore()
const {
isReady: isHostReady,
getByUuid: getHostByUuid,
hasError: hasHostError,
runningHosts,
} = useHostStore().subscribe()
const { records: controlDomains } = useControlDomainStore().subscribe()
const {
isReady: isConsoleReady,
getByOpaqueRef: getConsoleByOpaqueRef,
hasError: hasConsoleError,
} = useConsoleStore().subscribe()
const hasError = computed(() => hasHostError.value || hasConsoleError.value)
const host = computed(() => getHostByUuid(route.params.uuid as XenApiHost['uuid']))
const controlDomain = computed(() => {
const controlDomainOpaqueRef = host.value?.control_domain
return controlDomainOpaqueRef
? controlDomains.value.find(controlDomain => controlDomain.$ref === controlDomainOpaqueRef)
: undefined
})
const hostConsole = computed(() => {
const consoleOpaqueRef = controlDomain.value?.consoles[0]
return consoleOpaqueRef ? getConsoleByOpaqueRef(consoleOpaqueRef) : undefined
})
const isReady = computed(() => isHostReady.value && isConsoleReady.value && controlDomain.value)
const isHostRunning = computed(() => {
return runningHosts.value.some(runningHost => runningHost.uuid === host.value?.uuid)
})
const isConsoleAvailable = computed(() =>
controlDomain.value !== undefined ? !isHostOperationPending(host.value!, STOP_OPERATIONS) : false
)
const consoleElement = ref()
const sendCtrlAltDel = () => consoleElement.value?.sendCtrlAltDel()
const toggleFullScreen = () => {
uiStore.hasUi = !uiStore.hasUi
}
const openInNewTab = () => {
const routeData = router.resolve({ query: { ui: '0' } })
window.open(routeData.href, '_blank')
}
</script>

<style lang="postcss" scoped>
.host-console-view {
display: flex;
height: calc(100% - 14.5rem);
flex-direction: column;
&.no-ui {
height: 100%;
}
}
.spinner {
color: var(--color-info-txt-base);
display: flex;
margin: auto;
width: 10rem;
height: 10rem;
}
.remote-console {
flex: 1;
max-width: 100%;
height: 100%;
}
.not-available {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
text-align: center;
gap: 4rem;
color: var(--color-info-txt-base);
font-size: 3.6rem;
}
.open-in-new-window {
position: absolute;
top: 0;
right: 0;
overflow: hidden;
& > .link {
display: flex;
align-items: center;
gap: 1rem;
background-color: var(--color-info-txt-base);
color: var(--color-info-txt-item);
text-decoration: none;
padding: 1.5rem;
font-size: 1.6rem;
border-radius: 0 0 0 0.8rem;
white-space: nowrap;
transform: translateX(calc(100% - 4.5rem));
transition: transform 0.2s ease-in-out;
&:hover {
transform: translateX(0);
}
}
}
.host-console-view:deep(.menu-list) {
background-color: transparent;
align-self: center;
}
</style>
11 changes: 11 additions & 0 deletions @xen-orchestra/lite/src/views/host/HostNetworkView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<template>
<PageUnderConstruction />
</template>

<script lang="ts" setup>
import PageUnderConstruction from '@/components/PageUnderConstruction.vue'
import { usePageTitleStore } from '@/stores/page-title.store'
import { useI18n } from 'vue-i18n'
usePageTitleStore().setTitle(useI18n().t('network'))
</script>
Loading

0 comments on commit 904273f

Please sign in to comment.