1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-30 22:50:10 +03:00

F #5422: Reformat VM console styles (#1920)

This commit is contained in:
Sergio Betanzos 2022-04-07 18:55:32 +02:00 committed by GitHub
parent 226484619e
commit 4f6f250a52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 369 additions and 129 deletions

View File

@ -60,13 +60,13 @@ export const ENDPOINTS = [
},
{
label: 'Guacamole',
disabledSidebar: true,
disableLayout: true,
path: PATH.GUACAMOLE,
Component: Guacamole,
},
{
label: 'WebMKS',
disabledSidebar: true,
disableLayout: true,
path: PATH.WMKS,
Component: WebMKS,
},

View File

@ -74,7 +74,7 @@ const GuacamoleCtrlAltDelButton = memo(
variant="outlined"
color="error"
>
<Translate word={T.SendCtrlAltDel} />
<Translate word={T.CtrlAltDel} />
</Button>
)
}

View File

@ -24,7 +24,7 @@ import {
GuacamoleSession, // eslint-disable-line no-unused-vars
SOCKETS,
GUACAMOLE_STATES_STR,
THUMBNAIL_UPDATE_FREQUENCY,
// THUMBNAIL_UPDATE_FREQUENCY,
} from 'client/constants'
const {
@ -55,7 +55,7 @@ const GuacamoleClient = ({ id, display }) => {
const guac = useRef(createGuacamoleClient()).current
// Automatically update the client thumbnail
guac.client.onsync = () => handleUpdateThumbnail()
// guac.client.onsync = () => handleUpdateThumbnail()
const { enqueueError, enqueueInfo, enqueueSuccess } = useGeneralApi()
const { token, ...session } = useGuacamole(id)
@ -63,7 +63,7 @@ const GuacamoleClient = ({ id, display }) => {
setConnectionState,
setTunnelUnstable,
setMultiTouchSupport,
updateThumbnail,
// updateThumbnail,
} = useGuacamoleApi(id)
const handleConnect = (width, height, force = false) => {
@ -97,7 +97,7 @@ const GuacamoleClient = ({ id, display }) => {
* history under its associated ID. If the client is not connected, this
* function has no effect.
*/
const handleUpdateThumbnail = () => {
/* const handleUpdateThumbnail = () => {
const nowTimestamp = new Date().getTime()
const lastTimestamp = session?.thumbnail?.timestamp
@ -142,7 +142,7 @@ const GuacamoleClient = ({ id, display }) => {
const newThumbnail = { timestamp: nowTimestamp, canvas: url }
updateThumbnail({ thumbnail: newThumbnail })
}, 'image/webp')
}
} */
useEffect(() => {
guac.tunnel.onerror = (status) => {

View File

@ -13,19 +13,22 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, useEffect } from 'react'
import { ReactElement, useEffect, useMemo } from 'react'
import PropTypes from 'prop-types'
import { useHistory } from 'react-router'
import { Stack, Typography, Divider, Skeleton } from '@mui/material'
import { Stack, Typography, Divider, Skeleton, Avatar } from '@mui/material'
import { useGetVmQuery } from 'client/features/OneApi/vm'
import { useLazyGetServiceQuery } from 'client/features/OneApi/service'
import { useGeneralApi } from 'client/features/General'
import { StatusCircle } from 'client/components/Status'
import { StatusBadge } from 'client/components/Status'
import { OpenNebulaLogo } from 'client/components/Icons'
import MultipleTags from 'client/components/MultipleTags'
import { Translate } from 'client/components/HOC'
import { getIps, getState, isVCenter } from 'client/models/VirtualMachine'
import { timeFromMilliseconds } from 'client/models/Helper'
import { PATH } from 'client/apps/sunstone/routes'
import { VM_ACTIONS } from 'client/constants'
import { T, VM_ACTIONS, STATIC_FILES_URL } from 'client/constants'
/**
* @param {object} props - Props
@ -38,55 +41,102 @@ const HeaderVmInfo = ({ id, type }) => {
const { enqueueError } = useGeneralApi()
const { data: vm, isSuccess, isLoading, isError } = useGetVmQuery(id)
const [getService, { data: serviceFlow }] = useLazyGetServiceQuery()
const ips = getIps(vm)
const { color: stateColor, name: stateName } = getState(vm) ?? {}
const time = timeFromMilliseconds(+vm?.ETIME || +vm?.STIME)
const isVMRC = useMemo(() => type === VM_ACTIONS.VMRC, [type])
const serviceId = useMemo(() => vm?.USER_TEMPLATE?.SERVICE_ID, [vm])
const srcLogo = useMemo(() => vm?.USER_TEMPLATE?.LOGO?.toLowerCase(), [vm])
useEffect(() => {
serviceId !== undefined && getService({ id: serviceId })
}, [serviceId])
useEffect(() => {
isError && redirectTo(PATH.DASHBOARD)
}, [isError])
useEffect(() => {
if (type === VM_ACTIONS.VMRC && isSuccess && vm && !isVCenter(vm)) {
if (isVMRC && isSuccess && vm && !isVCenter(vm)) {
enqueueError(`${vm.ID} - ${vm.NAME} is not located on vCenter Host`)
redirectTo(PATH.DASHBOARD)
}
}, [isSuccess])
}, [isVMRC, isSuccess])
return (
<Stack direction="row" justifyContent="space-between" gap="1em" px={2}>
<Typography flexGrow={1} display="flex" alignItems="center" gap="0.5em">
<Stack
justifyContent="space-between"
flexGrow={1}
flexWrap="wrap"
gap="1em"
>
<Stack direction="row" alignItems="flex-end" gap="0.5em">
{isLoading ? (
<>
<Skeleton variant="circular" width={12} height={12} />
<Skeleton variant="text" width="60%" />
<Skeleton variant="circular" width={24} height={24} />
<Skeleton height={30} sx={{ width: { xs: '100%', sm: '60%' } }} />
</>
) : (
<>
<StatusCircle color={stateColor} tooltip={stateName} />
{`# ${vm?.ID} - ${vm?.NAME}`}
<StatusBadge title={stateName} stateColor={stateColor}>
{srcLogo ? (
<Avatar src={`${STATIC_FILES_URL}/${srcLogo}`} />
) : (
<OpenNebulaLogo width={38} height={38} disabledBetaText />
)}
</StatusBadge>
<Typography noWrap component="span" variant="h6">
{vm?.NAME}
</Typography>
{serviceFlow && (
<Typography noWrap component="span">
<Translate word={T.PartOf} />
{`: ${serviceFlow?.NAME}`}
</Typography>
)}
</>
)}
</Typography>
</Stack>
<Stack
flexGrow={1}
direction="row"
justifyContent="flex-end"
direction={{ xs: 'column', sm: 'row' }}
gap="0.5em"
alignItems="baseline"
divider={<Divider orientation="vertical" flexItem />}
gap="1em"
>
{isLoading ? (
<Skeleton variant="text" width="60%" />
<Skeleton
variant="text"
sx={{ width: { xs: '100%', sm: '50%', md: '25%' } }}
/>
) : (
<Typography>{`Started on: ${time.toFormat('ff')}`}</Typography>
<>
<Typography
noWrap
component="span"
variant="body1"
color="text.secondary"
>
{`# ${vm?.ID}`}
</Typography>
<Typography noWrap variant="body1">
<Translate
word={T.StartedOnTime}
values={[time.toFormat('ff')]}
/>
</Typography>
</>
)}
{isLoading ? (
<Skeleton variant="text" width="40%" />
<Skeleton
variant="text"
sx={{ width: { xs: '100%', sm: '50%', md: '25%' } }}
/>
) : (
!!ips?.length && (
<Typography>
<MultipleTags tags={ips} />
<MultipleTags tags={ips} clipboard />
</Typography>
)
)}

View File

@ -68,7 +68,7 @@ const WebMKSCtrlAltDelButton = memo((session) => {
variant="outlined"
color="error"
>
<Translate word={T.SendCtrlAltDel} />
<Translate word={T.CtrlAltDel} />
</Button>
)
})

View File

@ -23,6 +23,10 @@ import { SOCKETS } from 'client/constants'
const ERROR = 'Error'
const UNINITIALIZED = 'Uninitialized'
const CONNECTED = 'Connected'
const compareStrings = (a, b) =>
`${a}`.toLocaleLowerCase() === `${b}`.toLocaleLowerCase()
/**
* @param {object} options - Client options
@ -87,6 +91,9 @@ const WebMKSClient = ({ token }) => {
return {
wmks: wmks.current,
status,
isUninitialized: compareStrings(status, UNINITIALIZED),
isError: compareStrings(status, ERROR),
isConnected: compareStrings(status, CONNECTED),
displayElement: (
<Box
sx={{

View File

@ -80,7 +80,7 @@ const modificationTypeInput = (fieldName, { type: typeId }) => ({
validation: lazy((_, { context }) =>
string().default(() => {
const capacityUserInput = context.extra?.USER_INPUTS?.[fieldName]
const { type } = getUserInputParams(capacityUserInput) ?? {}
const { type } = getUserInputParams(capacityUserInput)
return type
})
@ -177,7 +177,7 @@ const modificationOptionsInput = (fieldName, { type, options: optionsId }) => ({
})
.default(() => {
const capacityUserInput = context.extra?.USER_INPUTS?.[fieldName]
const { options = [] } = getUserInputParams(capacityUserInput) ?? {}
const { options = [] } = getUserInputParams(capacityUserInput)
return options
})

View File

@ -93,7 +93,7 @@ export const LOGO = {
alt="logo"
imgProps={{ height: 25, width: 25, style: { marginRight: 10 } }}
// expected url for Ruby Sunstone compatibility
// => client/assets/images/logos/{value}.png
// => images/logos/{logo}.png
src={`${STATIC_FILES_URL}/${value}`}
/>
),

View File

@ -24,9 +24,9 @@ import { useGeneral, useGeneralApi } from 'client/features/General'
import Header from 'client/components/Header'
import Footer from 'client/components/Footer'
import internalStyles from 'client/components/HOC/InternalLayout/styles'
import { sidebar } from 'client/theme/defaults'
import { sidebar, footer } from 'client/theme/defaults'
const InternalLayout = ({ title, customHeader, disabledSidebar, children }) => {
const InternalLayout = ({ title, disableLayout, children }) => {
const classes = internalStyles()
const container = useRef()
const { isFixMenu } = useGeneral()
@ -37,30 +37,34 @@ const InternalLayout = ({ title, customHeader, disabledSidebar, children }) => {
changeTitle(typeof title === 'function' ? title(params) : title)
}, [title])
if (disableLayout) {
return (
<Box data-cy="main-layout" className={classes.root}>
<Box
component="main"
sx={{ height: '100vh', width: '100%', pb: `${footer.regular}px` }}
>
{children}
</Box>
<Footer />
</Box>
)
}
return (
<Box
data-cy="main-layout"
className={classes.root}
sx={useMemo(
() =>
disabledSidebar
? {}
: {
marginLeft: {
lg: isFixMenu
? `${sidebar.fixed}px`
: `${sidebar.minified}px`,
},
},
[isFixMenu, disabledSidebar]
() => ({
ml: {
lg: isFixMenu ? `${sidebar.fixed}px` : `${sidebar.minified}px`,
},
}),
[isFixMenu]
)}
>
{customHeader ?? (
<Header
disabledSidebar={disabledSidebar}
scrollContainer={container.current}
/>
)}
<Header scrollContainer={container.current} />
<Box component="main" className={classes.main}>
<CSSTransition
in
@ -93,8 +97,7 @@ const InternalLayout = ({ title, customHeader, disabledSidebar, children }) => {
InternalLayout.propTypes = {
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
customHeader: PropTypes.node,
disabledSidebar: PropTypes.bool,
disableLayout: PropTypes.bool,
children: PropTypes.any,
}

View File

@ -36,7 +36,7 @@ import Group from 'client/components/Header/Group'
import Zone from 'client/components/Header/Zone'
import { sentenceCase } from 'client/utils'
const Header = ({ disabledSidebar = false }) => {
const Header = () => {
const { isOneAdmin } = useAuth()
const { fixMenu } = useGeneralApi()
const { appTitle, title, isBeta, withGroupSwitcher } = useGeneral()
@ -45,17 +45,15 @@ const Header = ({ disabledSidebar = false }) => {
return (
<AppBar data-cy="header" elevation={0} position="absolute">
<Toolbar>
{!disabledSidebar && (
<IconButton
onClick={() => fixMenu(true)}
edge="start"
size="small"
variant="outlined"
sx={{ display: { lg: 'none' } }}
>
<MenuIcon />
</IconButton>
)}
<IconButton
onClick={() => fixMenu(true)}
edge="start"
size="small"
variant="outlined"
sx={{ display: { lg: 'none' } }}
>
<MenuIcon />
</IconButton>
<Box
flexGrow={1}
ml={2}
@ -117,7 +115,6 @@ const Header = ({ disabledSidebar = false }) => {
}
Header.propTypes = {
disabledSidebar: PropTypes.bool,
scrollContainer: PropTypes.object,
}

View File

@ -0,0 +1,43 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import { number, string, oneOfType } from 'prop-types'
const GuacamoleLogo = memo(({ width, height, viewBox, ...props }) => (
<svg viewBox={viewBox} width={width} height={height} {...props}>
<g id="logo__guacamole" fill="currentColor">
<path d="M21.104 10.951c.7-.371 1.351-.708 1.993-1.067.232-.13.333-.105.411.139a1.545 1.545 0 0 1-.259 1.436 5.464 5.464 0 0 1-2.568 1.767 17.565 17.565 0 0 1-5.285 1.1 20.533 20.533 0 0 1-7.063-.673 7.513 7.513 0 0 1-3.595-1.894 1.417 1.417 0 0 1-.135-2.181 4.557 4.557 0 0 0 .8-1.344.589.589 0 0 1 .382-.4 8.368 8.368 0 0 0 2.786-1.7 2.3 2.3 0 0 1 1.6-.411 4.551 4.551 0 0 0 2.317-.585 2.447 2.447 0 0 1 1.987-.157 6.926 6.926 0 0 0 2.749.39 3.46 3.46 0 0 1 2.094.893c.137.09.057.193.015.294-.165.4-.314.807-.5 1.2-.122.263-.041.417.219.528.108.047.207.113.316.151a.726.726 0 0 1 .524.492.951.951 0 0 0 .527.574.948.948 0 0 1 .586.885 4.96 4.96 0 0 0 .099.563Z" />
<path d="M13.441 19.101a20.933 20.933 0 0 1-9.4-2.292 9.579 9.579 0 0 1-1.658-1.086 1.138 1.138 0 0 1-.51-.865c-.009-.306-.082-.608-.116-.912a1.794 1.794 0 0 0-.221-.8 2.621 2.621 0 0 1-.219-1.109 3.184 3.184 0 0 1 1.389-3.129 11.594 11.594 0 0 1 1.439-.935c.066-.036.142-.1.211-.057.1.066.012.15-.013.228a4.076 4.076 0 0 1-.753 1.314 2.379 2.379 0 0 0 .2 3.378 7.775 7.775 0 0 0 3.238 1.961 20.178 20.178 0 0 0 7.241 1.084 18.943 18.943 0 0 0 7.1-1.36 6.923 6.923 0 0 0 2.529-1.671 2.516 2.516 0 0 0 .533-3.05c-.022-.044-.059-.092-.053-.132.04-.231.72-.482.906-.329a2.349 2.349 0 0 1 .995 1.911 6.01 6.01 0 0 1-.308 2.314 6.109 6.109 0 0 0-.258 1.4 1.325 1.325 0 0 1-.565.876 14.3 14.3 0 0 1-4.1 2.053 23.312 23.312 0 0 1-7.607 1.208ZM3.062 19.252c.108-.065.2-.007.3.064a15.464 15.464 0 0 0 6.193 2.427 21.51 21.51 0 0 0 5.692.356 18.984 18.984 0 0 0 8.085-2.186c.225-.126.437-.27.652-.41.056-.035.106-.084.17-.036s.035.119.007.169a4.059 4.059 0 0 1-.227.343 10.036 10.036 0 0 1-5.773 3.942 16.682 16.682 0 0 1-10.429-.569 8.917 8.917 0 0 1-4.469-3.713c-.039-.063-.084-.125-.119-.19s-.142-.09-.082-.197Z" />
<path d="M21.925 4.499a3.676 3.676 0 0 1 .388.1 7.965 7.965 0 0 1 2.894 1.755c.245.241.216.442 0 .695a11.193 11.193 0 0 1-3.085 2.207c-.159.093-.238.048-.3-.108a1.007 1.007 0 0 0-.547-.566.53.53 0 0 1-.292-.4.689.689 0 0 0-.469-.522c-.141-.045-.21-.1-.152-.264a12.325 12.325 0 0 1 1.191-2.636c.085-.133.17-.279.372-.261Z" />
</g>
</svg>
))
GuacamoleLogo.propTypes = {
width: oneOfType([number, string]).isRequired,
height: oneOfType([number, string]).isRequired,
viewBox: string,
}
GuacamoleLogo.defaultProps = {
width: 28,
height: 28,
viewBox: '0 0 28 28',
}
GuacamoleLogo.displayName = 'GuacamoleLogo'
export default GuacamoleLogo

View File

@ -26,15 +26,13 @@ const OpenNebulaLogo = memo(
height,
spinner,
withText,
viewBox,
viewBox = withText ? '0 0 120 45' : '0 0 35 45',
disabledBetaText,
...props
}) => {
const { isBeta } = useGeneral()
const {
palette: { mode },
} = useTheme()
const isDarkMode = mode === SCHEMES.DARK
const { palette } = useTheme()
const isDarkMode = palette.mode === SCHEMES.DARK
const cloudColor = useMemo(
() => ({
@ -247,7 +245,6 @@ OpenNebulaLogo.propTypes = {
OpenNebulaLogo.defaultProps = {
width: 360,
height: 360,
viewBox: '0 0 120 45',
spinner: false,
withText: false,
disabledBetaText: false,

View File

@ -0,0 +1,44 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import { number, string, oneOfType } from 'prop-types'
const WebMKSLogo = memo(({ width, height, viewBox, ...props }) => (
<svg viewBox={viewBox} width={width} height={height} {...props}>
<g id="logo__wmks" fill="currentColor">
<path
d="M2196.906-4110.052a2.112 2.112 0 0 0-2.166-2.151q-2.533-.006-5.066 0a2.105 2.105 0 0 0-2.172 2.176v3.638c0 .277 0 .278-.281.277h-7.94a1.547 1.547 0 0 0-1.108.428 1.894 1.894 0 0 0-.582 1.66 1.745 1.745 0 0 0 1.744 1.6c1.649-.031 3.3-.009 4.951-.01h.185c.014.022.029.044.042.067a1.3 1.3 0 0 0-.183.136q-3.471 3.465-6.936 6.932a1.6 1.6 0 0 0-.485 1.162 1.974 1.974 0 0 0 1.235 1.79 1.691 1.691 0 0 0 1.961-.445l6.774-6.77c.054-.055.115-.1.209-.184v5.023a1.722 1.722 0 0 0 .223.912 1.906 1.906 0 0 0 2.158.807 1.748 1.748 0 0 0 1.292-1.736c-.025-2.627-.007-5.254-.011-7.882 0-.179.045-.24.233-.239 1.257.008 2.514.006 3.77 0a2.153 2.153 0 0 0 .342-.021 2.1 2.1 0 0 0 1.809-2.16q.01-2.504.002-5.01Zm-2.5 3.951a.671.671 0 0 1-.764.756h-2.8a.741.741 0 0 1-.821-.826v-2.758a.709.709 0 0 1 .8-.792h2.816a.689.689 0 0 1 .76.752v1.445c.009.472.011.948.003 1.424Z"
transform="translate(-2172.382 4116.408)"
/>
</g>
</svg>
))
WebMKSLogo.propTypes = {
width: oneOfType([number, string]).isRequired,
height: oneOfType([number, string]).isRequired,
viewBox: string,
}
WebMKSLogo.defaultProps = {
width: 28,
height: 28,
viewBox: '0 0 28 28',
}
WebMKSLogo.displayName = 'WebMKSLogo'
export default WebMKSLogo

View File

@ -14,6 +14,8 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import DockerLogo from 'client/components/Icons/DockerIcon'
import GuacamoleLogo from 'client/components/Icons/GuacamoleIcon'
import OpenNebulaLogo from 'client/components/Icons/OpenNebulaIcon'
import WebMKSLogo from 'client/components/Icons/WebMKSIcon'
export { DockerLogo, OpenNebulaLogo }
export { DockerLogo, GuacamoleLogo, OpenNebulaLogo, WebMKSLogo }

View File

@ -15,10 +15,11 @@
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import PropTypes from 'prop-types'
import { Tooltip, Typography } from '@mui/material'
import { StatusChip } from 'client/components/Status'
import { Translate } from 'client/components/HOC'
import { T } from 'client/constants'
const MultipleTags = ({ tags, limitTags = 1, clipboard }) => {
if (tags?.length === 0) {
@ -62,7 +63,8 @@ const MultipleTags = ({ tags, limitTags = 1, clipboard }) => {
})}
>
<Typography component="span" variant="subtitle2" sx={{ ml: 1 }}>
{`+${more} more`}
{`+${more} `}
<Translate word={T.More} />
</Typography>
</Tooltip>
)}

View File

@ -76,7 +76,7 @@ const Sidebar = ({ endpoints }) => {
matchPath(pathname, { path, exact: true })
)
return endpoint?.disabledSidebar
return endpoint?.disableLayout
}, [pathname])
if (isDisabledSidebar) {

View File

@ -15,8 +15,8 @@
* ------------------------------------------------------------------------- */
import { ReactElement, useMemo, useRef, useEffect } from 'react'
import { useParams, useHistory } from 'react-router'
import { Box, Stack, Typography } from '@mui/material'
import { RESOURCE_NAMES } from 'client/constants'
import { Box, Stack, Container, Typography } from '@mui/material'
import { useViews } from 'client/features/Auth'
import { useGetGuacamoleSessionQuery } from 'client/features/OneApi/vm'
@ -32,51 +32,72 @@ import {
GuacamoleFullScreenButton,
GuacamoleScreenshotButton,
} from 'client/components/Consoles'
import { GuacamoleLogo } from 'client/components/Icons'
import { PATH } from 'client/apps/sunstone/routes'
import { Tr } from 'client/components/HOC'
import { sentenceCase } from 'client/utils'
import { RESOURCE_NAMES, T } from 'client/constants'
/** @returns {ReactElement} Guacamole container */
const Guacamole = () => {
const containerRef = useRef(null)
const headerRef = useRef(null)
const { id, type = '' } = useParams()
const { push: redirectTo } = useHistory()
const { view, [RESOURCE_NAMES.VM]: vmView } = useViews()
const isAvailableView = useMemo(
() => view && vmView?.actions?.[type] === true,
() => view && !!vmView?.actions?.[type] === true,
[view]
)
const { isError } = useGetGuacamoleSessionQuery(
const { isError: queryIsError } = useGetGuacamoleSessionQuery(
{ id, type },
{ refetchOnMountOrArgChange: false, skip: !isAvailableView }
)
useEffect(() => {
;(isError || !isAvailableView) && redirectTo(PATH.DASHBOARD)
}, [isError])
;(queryIsError || !isAvailableView) && redirectTo(PATH.DASHBOARD)
}, [queryIsError])
const containerRef = useRef(null)
const headerRef = useRef(null)
const {
token,
clientState,
displayElement,
isError,
isConnected,
...session
} = useGuacamoleSession(
useMemo(
() => ({
id: `${id}-${type}`,
container: containerRef.current,
header: headerRef.current,
}),
[
containerRef.current?.offsetWidth,
containerRef.current?.offsetHeight,
headerRef.current?.offsetWidth,
headerRef.current?.offsetHeight,
]
),
GuacamoleDisplay,
GuacamoleMouse,
GuacamoleKeyboard,
GuacamoleClipboard
)
const { token, clientState, displayElement, ...session } =
useGuacamoleSession(
useMemo(
() => ({
id: `${id}-${type}`,
container: containerRef.current,
header: headerRef.current,
}),
[
containerRef.current?.offsetWidth,
containerRef.current?.offsetHeight,
headerRef.current?.offsetWidth,
headerRef.current?.offsetHeight,
]
),
GuacamoleDisplay,
GuacamoleMouse,
GuacamoleKeyboard,
GuacamoleClipboard
)
const colorStatus = useMemo(
() =>
isError ? 'error.main' : isConnected ? 'success.main' : 'text.secondary',
[isError, isConnected]
)
const connectionState = useMemo(
() => sentenceCase(clientState?.connectionState ?? ''),
[clientState?.connectionState]
)
return (
<Box
@ -85,18 +106,45 @@ const Guacamole = () => {
height: '100%',
display: 'grid',
gridTemplateRows: 'auto 1fr',
gap: '1em',
}}
>
<Stack ref={headerRef}>
<Stack
ref={headerRef}
component={Container}
direction={{ sm: 'column', md: 'row' }}
alignItems="stretch"
justifyContent="space-between"
gap="1em"
padding="1em"
>
<HeaderVmInfo id={id} type={type} />
<Stack direction="row" alignItems="center" gap="1em" my="1em">
<GuacamoleCtrlAltDelButton {...session} />
<GuacamoleReconnectButton {...session} />
<GuacamoleScreenshotButton {...session} />
<GuacamoleFullScreenButton {...session} />
{clientState?.connectionState && (
<Typography>{`State: ${clientState?.connectionState}`}</Typography>
<Stack
direction={{ sm: 'row', md: 'column' }}
alignItems={{ sm: 'center', md: 'end' }}
flexGrow={{ sm: 1, md: 0 }}
flexWrap="wrap"
gap="1em"
>
{connectionState && (
<Stack
title={`${Tr(T.GuacamoleState)}: ${connectionState}`}
flexGrow={1}
direction={{ sm: 'row-reverse', md: 'row' }}
justifyContent="flex-end"
alignItems="flex-end"
gap="1em"
>
<Typography color={colorStatus}>{connectionState}</Typography>
<GuacamoleLogo />
</Stack>
)}
<Stack direction="row" alignItems="center" gap="1em">
<GuacamoleReconnectButton {...session} />
<GuacamoleScreenshotButton {...session} />
<GuacamoleFullScreenButton {...session} />
<GuacamoleCtrlAltDelButton {...session} />
</Stack>
</Stack>
</Stack>
{displayElement}

View File

@ -15,10 +15,9 @@
* ------------------------------------------------------------------------- */
import { ReactElement, useEffect, useMemo } from 'react'
import { useHistory, useParams } from 'react-router'
import { Box, Stack, Typography } from '@mui/material'
import { RESOURCE_NAMES, VM_ACTIONS } from 'client/constants'
import { useViews } from 'client/features/Auth'
import { Box, Stack, Typography, Container } from '@mui/material'
import { useViews } from 'client/features/Auth'
import { useGetVMRCSessionQuery } from 'client/features/OneApi/vcenter'
import {
HeaderVmInfo,
@ -27,8 +26,11 @@ import {
WebMKSCtrlAltDelButton,
WebMKSFullScreenButton,
} from 'client/components/Consoles'
import { WebMKSLogo } from 'client/components/Icons'
import { PATH } from 'client/apps/sunstone/routes'
import { Tr } from 'client/components/HOC'
import { sentenceCase } from 'client/utils'
import { RESOURCE_NAMES, T, VM_ACTIONS } from 'client/constants'
/** @returns {ReactElement} WebMKS container */
const WebMKS = () => {
@ -40,27 +42,69 @@ const WebMKS = () => {
[view]
)
const { data: ticket, isError } = useGetVMRCSessionQuery(
const { data: ticket, isError: queryIsError } = useGetVMRCSessionQuery(
{ id },
{ refetchOnMountOrArgChange: false, skip: !isAvailableView }
)
useEffect(() => {
;(isError || !isAvailableView) && redirectTo(PATH.DASHBOARD)
}, [isError])
;(queryIsError || !isAvailableView) && redirectTo(PATH.DASHBOARD)
}, [queryIsError])
const { ...session } = useWebMKSSession({ token: ticket })
const { status, displayElement } = session
const { status, isError, isConnected, displayElement } = session
const colorStatus = useMemo(
() =>
isError ? 'error.main' : isConnected ? 'success.main' : 'text.secondary',
[isError, isConnected]
)
const connectionState = useMemo(() => sentenceCase(status ?? ''), [status])
return (
<Box display="grid" gridTemplateRows="auto 1fr" width={1} height={1}>
<Stack>
<Box
sx={{
height: '100%',
display: 'grid',
gridTemplateRows: 'auto 1fr',
gap: '1em',
}}
>
<Stack
component={Container}
direction={{ sm: 'column', md: 'row' }}
alignItems="stretch"
justifyContent="space-between"
gap="1em"
padding="1em"
>
<HeaderVmInfo id={id} type={VM_ACTIONS.VMRC} />
<Stack direction="row" alignItems="center" gap="1em" my="1em">
<WebMKSCtrlAltDelButton {...session} />
<WebMKSFullScreenButton {...session} />
{/* <WebMKSKeyboard {...session} /> */}
<Typography>{sentenceCase(status)}</Typography>
<Stack
direction={{ sm: 'row', md: 'column' }}
alignItems={{ sm: 'center', md: 'end' }}
flexGrow={{ sm: 1, md: 0 }}
flexWrap="wrap"
gap="1em"
>
{connectionState && (
<Stack
title={`${Tr(T.VMRCState)}: ${connectionState}`}
flexGrow={1}
direction={{ sm: 'row-reverse', md: 'row' }}
justifyContent="flex-end"
alignItems="flex-end"
gap="1em"
>
<Typography color={colorStatus}>{connectionState}</Typography>
<WebMKSLogo />
</Stack>
)}
<Stack direction="row" alignItems="center" gap="1em">
<WebMKSFullScreenButton {...session} />
<WebMKSCtrlAltDelButton {...session} />
{/* <WebMKSKeyboard {...session} /> */}
</Stack>
</Stack>
</Stack>
{displayElement}

View File

@ -325,6 +325,8 @@ const OPTIONAL = 'O'
* @returns {UserInputObject} User input object
*/
export const getUserInputParams = (userInputString) => {
if (!userInputString) return {}
const params = String(userInputString).split(PARAMS_SEPARATOR)
const options = [

View File

@ -28,9 +28,9 @@ import {
import { ProtectedRoute, NoAuthRoute } from 'client/components/Route'
import { InternalLayout } from 'client/components/HOC'
const renderRoute = ({ Component, label, disabledSidebar, ...rest }, index) => (
const renderRoute = ({ Component, label, disableLayout, ...rest }, index) => (
<ProtectedRoute key={index} exact {...rest}>
<InternalLayout title={label} disabledSidebar={disabledSidebar}>
<InternalLayout title={label} disableLayout={disableLayout}>
<Component fallback={<LinearProgress color="secondary" />} />
</InternalLayout>
</ProtectedRoute>
@ -75,6 +75,7 @@ Router.propTypes = {
label: PropTypes.string,
path: PropTypes.string,
sidebar: PropTypes.bool,
disableLayout: PropTypes.bool,
routes: PropTypes.array,
})
),