Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(xo-lite): display host console #8136

Merged
merged 13 commits into from
Nov 26, 2024
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
Loading