mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-30 22:50:10 +03:00
parent
226484619e
commit
4f6f250a52
@ -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,
|
||||
},
|
||||
|
@ -74,7 +74,7 @@ const GuacamoleCtrlAltDelButton = memo(
|
||||
variant="outlined"
|
||||
color="error"
|
||||
>
|
||||
<Translate word={T.SendCtrlAltDel} />
|
||||
<Translate word={T.CtrlAltDel} />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
||||
)
|
||||
)}
|
||||
|
@ -68,7 +68,7 @@ const WebMKSCtrlAltDelButton = memo((session) => {
|
||||
variant="outlined"
|
||||
color="error"
|
||||
>
|
||||
<Translate word={T.SendCtrlAltDel} />
|
||||
<Translate word={T.CtrlAltDel} />
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
|
@ -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={{
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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}`}
|
||||
/>
|
||||
),
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
|
43
src/fireedge/src/client/components/Icons/GuacamoleIcon.js
Normal file
43
src/fireedge/src/client/components/Icons/GuacamoleIcon.js
Normal 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
|
@ -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,
|
||||
|
44
src/fireedge/src/client/components/Icons/WebMKSIcon.js
Normal file
44
src/fireedge/src/client/components/Icons/WebMKSIcon.js
Normal 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
|
@ -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 }
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -76,7 +76,7 @@ const Sidebar = ({ endpoints }) => {
|
||||
matchPath(pathname, { path, exact: true })
|
||||
)
|
||||
|
||||
return endpoint?.disabledSidebar
|
||||
return endpoint?.disableLayout
|
||||
}, [pathname])
|
||||
|
||||
if (isDisabledSidebar) {
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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 = [
|
||||
|
@ -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,
|
||||
})
|
||||
),
|
||||
|
Loading…
x
Reference in New Issue
Block a user