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

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
21 changes: 21 additions & 0 deletions @xen-orchestra/lite/src/components/host/HostHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<template>
<TitleBar :icon="faServer">
{{ name }}
</TitleBar>
</template>

<script lang="ts" setup>
import TitleBar from '@/components/TitleBar.vue'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import { useHostStore } from '@/stores/xen-api/host.store'
import { faServer } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue'
import { useRoute } from 'vue-router'

const { getByUuid: getHostByUuid } = useHostStore().subscribe()
const route = useRoute()

const host = computed(() => getHostByUuid(route.params.uuid as XenApiHost['uuid']))

const name = computed(() => host.value?.name_label)
</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
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
190 changes: 190 additions & 0 deletions @xen-orchestra/lite/src/views/host/HostConsoleView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<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">
<MenuList horizontal>
<MenuItem v-if="uiStore.hasUi" :icon="faArrowUpRightFromSquare" @click="openInNewTab">
{{ $t('open-console-in-new-tab') }}
</MenuItem>
<MenuItem
:icon="uiStore.hasUi ? faUpRightAndDownLeftFromCenter : faDownLeftAndUpRightToCenter"
@click="toggleFullScreen"
>
{{ $t(uiStore.hasUi ? 'fullscreen' : 'fullscreen-leave') }}
</MenuItem>
<MenuItem :disabled="!consoleElement" :icon="faKeyboard" @click="sendCtrlAltDel">
{{ $t('send-ctrl-alt-del') }}
</MenuItem>
</MenuList>
<RemoteConsole
ref="consoleElement"
:is-console-available="isConsoleAvailable"
:location="hostConsole.location"
class="remote-console"
/>
</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 { isVmOperationPending } from '@/libs/vm'
import { VM_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 MenuItem from '@core/components/menu/MenuItem.vue'
import MenuList from '@core/components/menu/MenuList.vue'
import { useUiStore } from '@core/stores/ui.store'
import {
faArrowUpRightFromSquare,
faDownLeftAndUpRightToCenter,
faKeyboard,
faUpRightAndDownLeftFromCenter,
} from '@fortawesome/free-solid-svg-icons'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'

const STOP_OPERATIONS = [
VM_OPERATION.SHUTDOWN,
VM_OPERATION.CLEAN_SHUTDOWN,
VM_OPERATION.HARD_SHUTDOWN,
VM_OPERATION.CLEAN_REBOOT,
VM_OPERATION.HARD_REBOOT,
VM_OPERATION.PAUSE,
VM_OPERATION.SUSPEND,
]

usePageTitleStore().setTitle(useI18n().t('console'))

const router = useRouter()
const route = useRoute()
const uiStore = useUiStore()

const {
isReady: isHostReady,
getByUuid: getHostByUuid,
hasError: hasHostError,
runningHosts: getRunningHosts,
} = useHostStore().subscribe()
const { records: getRecords } = 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 vm = computed(() => {
const controlDomainOpaqueRef = host.value?.control_domain
return controlDomainOpaqueRef ? getRecords.value.find(vm => vm.$ref === controlDomainOpaqueRef) : undefined
})

const hostConsole = computed(() => {
const consoleOpaqueRef = vm.value?.consoles[0]
return consoleOpaqueRef ? getConsoleByOpaqueRef(consoleOpaqueRef) : undefined
})

const isReady = computed(() => isHostReady.value && isConsoleReady.value && vm.value)

const isHostRunning = computed(() => {
return getRunningHosts.value.some(runningHost => runningHost.uuid === host.value?.uuid)
})

const isConsoleAvailable = computed(() =>
vm.value !== undefined ? !isVmOperationPending(vm.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('dashboard'))
</script>
7 changes: 6 additions & 1 deletion @xen-orchestra/lite/src/views/host/HostRootView.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
<template>
<ObjectNotFoundWrapper :is-ready="isReady" :uuid-checker="hasUuid">
<template v-if="uiStore.hasUi">
<HostHeader />
<HostTabBar :uuid="currentHost!.uuid" />
</template>
<RouterView />
</ObjectNotFoundWrapper>
</template>

<script lang="ts" setup>
import HostHeader from '@/components/host/HostHeader.vue'
import HostTabBar from '@/components/host/HostTabBar.vue'
import ObjectNotFoundWrapper from '@/components/ObjectNotFoundWrapper.vue'
import type { XenApiHost } from '@/libs/xen-api/xen-api.types'
import { usePageTitleStore } from '@/stores/page-title.store'
Expand All @@ -18,7 +24,6 @@ const route = useRoute()
const uiStore = useUiStore()

const currentHost = computed(() => getByUuid(route.params.uuid as XenApiHost['uuid']))

watchEffect(() => {
uiStore.currentHostOpaqueRef = currentHost.value?.$ref
})
Expand Down
11 changes: 11 additions & 0 deletions @xen-orchestra/lite/src/views/host/HostTasksView.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('dashboard'))
</script>
11 changes: 11 additions & 0 deletions @xen-orchestra/lite/src/views/host/HostVmsView.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('dashboard'))
</script>
Loading