mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-22 18:50:08 +03:00
(cherry picked from commit 1154d568c34a089b79af3314ec94fe0c5fcbe1e8)
This commit is contained in:
parent
710ea8080d
commit
5695b00d2b
src/fireedge
package-lock.jsonpackage.json
src
client
apps/sunstone
components
Buttons
Cards
Consoles
Guacamole
index.jsForms/VmTemplate/CreateForm/Steps/ExtraConfiguration/inputOutput
HOC
Header
Sidebar
Tables/Vms
Tabs
constants
containers/Guacamole
features
models
router
store
utils
server
888
src/fireedge/package-lock.json
generated
888
src/fireedge/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -81,6 +81,7 @@
|
||||
"fast-xml-parser": "3.19.0",
|
||||
"fs-extra": "9.0.1",
|
||||
"fuse.js": "6.4.1",
|
||||
"guacamole-common-js": "1.3.1",
|
||||
"helmet": "4.1.1",
|
||||
"http": "0.0.1-security",
|
||||
"http-proxy-middleware": "1.0.5",
|
||||
|
@ -62,8 +62,8 @@ const Sunstone = ({ store = {}, location = '' }) => (
|
||||
|
||||
Sunstone.propTypes = {
|
||||
location: PropTypes.string,
|
||||
context: PropTypes.shape({}),
|
||||
store: PropTypes.shape({}),
|
||||
context: PropTypes.object,
|
||||
store: PropTypes.object,
|
||||
}
|
||||
|
||||
Sunstone.displayName = 'SunstoneApp'
|
||||
|
@ -27,10 +27,14 @@ const Dashboard = loadable(
|
||||
const Settings = loadable(() => import('client/containers/Settings'), {
|
||||
ssr: false,
|
||||
})
|
||||
const Guacamole = loadable(() => import('client/containers/Guacamole'), {
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
export const PATH = {
|
||||
DASHBOARD: '/dashboard',
|
||||
SETTINGS: '/settings',
|
||||
GUACAMOLE: '/guacamole/:id/:type',
|
||||
}
|
||||
|
||||
export const ENDPOINTS = [
|
||||
@ -50,6 +54,12 @@ export const ENDPOINTS = [
|
||||
position: -1,
|
||||
Component: Settings,
|
||||
},
|
||||
{
|
||||
label: 'Guacamole',
|
||||
disabledSidebar: true,
|
||||
path: PATH.GUACAMOLE,
|
||||
Component: Guacamole,
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
|
108
src/fireedge/src/client/components/Buttons/ConsoleAction.js
Normal file
108
src/fireedge/src/client/components/Buttons/ConsoleAction.js
Normal file
@ -0,0 +1,108 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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, useMemo, useCallback, ReactElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useHistory, generatePath } from 'react-router-dom'
|
||||
|
||||
import {
|
||||
AppleImac2021 as VncIcon,
|
||||
TerminalOutline as SshIcon,
|
||||
Windows as RdpIcon,
|
||||
} from 'iconoir-react'
|
||||
import { SubmitButton } from 'client/components/FormControl'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import { useLazyGetGuacamoleSessionQuery } from 'client/features/OneApi/vm'
|
||||
import {
|
||||
nicsIncludesTheConnectionType,
|
||||
isAvailableAction,
|
||||
} from 'client/models/VirtualMachine'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T, VM, RESOURCE_NAMES, VM_ACTIONS } from 'client/constants'
|
||||
import { PATH } from 'client/apps/sunstone/routes'
|
||||
|
||||
const GUACAMOLE_BUTTON = {
|
||||
vnc: { tooltip: T.Vnc, icon: <VncIcon /> },
|
||||
ssh: { tooltip: T.Ssh, icon: <SshIcon /> },
|
||||
rdp: { tooltip: T.Rdp, icon: <RdpIcon /> },
|
||||
}
|
||||
|
||||
const GuacamoleButton = memo(
|
||||
/**
|
||||
* @param {object} options - Options
|
||||
* @param {VM} options.vm - Virtual machine
|
||||
* @param {'vnc'|'ssh'|'rdp'} options.connectionType - Connection type
|
||||
* @param {Function} [options.onClick] - Handle click for button
|
||||
* @returns {ReactElement} - Guacamole button
|
||||
*/
|
||||
({ vm, connectionType, onClick }) => {
|
||||
const history = useHistory()
|
||||
const { view, [RESOURCE_NAMES.VM]: vmView } = useViews()
|
||||
const [getSession, { isLoading }] = useLazyGetGuacamoleSessionQuery()
|
||||
|
||||
const isDisabled = useMemo(() => {
|
||||
const noAction = vmView?.actions?.[connectionType] !== true
|
||||
const noAvailable = isAvailableAction(connectionType)(vm)
|
||||
|
||||
return noAction || noAvailable
|
||||
}, [view, vm])
|
||||
|
||||
const { tooltip, icon } = GUACAMOLE_BUTTON[connectionType]
|
||||
|
||||
const goToConsole =
|
||||
onClick ??
|
||||
useCallback(
|
||||
async (evt) => {
|
||||
try {
|
||||
evt.stopPropagation()
|
||||
|
||||
const params = { id: vm?.ID, type: connectionType }
|
||||
await getSession(params)
|
||||
history.push(generatePath(PATH.GUACAMOLE, params))
|
||||
} catch {}
|
||||
},
|
||||
[vm?.ID, connectionType, history]
|
||||
)
|
||||
|
||||
if (
|
||||
isDisabled ||
|
||||
(connectionType !== VM_ACTIONS.VNC &&
|
||||
!nicsIncludesTheConnectionType(vm, connectionType))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SubmitButton
|
||||
data-cy={`${vm?.ID}-${connectionType}`}
|
||||
icon={icon}
|
||||
tooltip={<Translate word={tooltip} />}
|
||||
isSubmitting={isLoading}
|
||||
onClick={goToConsole}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
GuacamoleButton.propTypes = {
|
||||
vm: PropTypes.object,
|
||||
connectionType: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
}
|
||||
|
||||
GuacamoleButton.displayName = 'GuacamoleButton'
|
||||
|
||||
export { GuacamoleButton }
|
@ -15,3 +15,4 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export * from 'client/components/Buttons/ScheduleAction'
|
||||
export * from 'client/components/Buttons/ConsoleAction'
|
||||
|
@ -36,9 +36,10 @@ const VirtualMachineCard = memo(
|
||||
* @param {object} props - Props
|
||||
* @param {VM} props.vm - Virtual machine resource
|
||||
* @param {object} props.rootProps - Props to root component
|
||||
* @param {ReactElement} [props.actions] - Actions
|
||||
* @returns {ReactElement} - Card
|
||||
*/
|
||||
({ vm, rootProps }) => {
|
||||
({ vm, rootProps, actions }) => {
|
||||
const classes = rowStyles()
|
||||
const { ID, NAME, UNAME, GNAME, IPS, STIME, ETIME, LOCK } = vm
|
||||
|
||||
@ -89,6 +90,7 @@ const VirtualMachineCard = memo(
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
{actions && <div className={classes.actions}>{actions}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -99,6 +101,7 @@ VirtualMachineCard.propTypes = {
|
||||
rootProps: PropTypes.shape({
|
||||
className: PropTypes.string,
|
||||
}),
|
||||
actions: PropTypes.any,
|
||||
}
|
||||
|
||||
VirtualMachineCard.displayName = 'VirtualMachineCard'
|
||||
|
213
src/fireedge/src/client/components/Consoles/Guacamole/buttons.js
Normal file
213
src/fireedge/src/client/components/Consoles/Guacamole/buttons.js
Normal file
@ -0,0 +1,213 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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, useCallback, useState, ReactElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Refresh, Maximize, Camera } from 'iconoir-react'
|
||||
import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
Button,
|
||||
IconButton,
|
||||
CircularProgress,
|
||||
} from '@mui/material'
|
||||
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { downloadFile } from 'client/utils'
|
||||
import { T, GuacamoleSession } from 'client/constants'
|
||||
|
||||
const GuacamoleCtrlAltDelButton = memo(
|
||||
/**
|
||||
* @param {GuacamoleSession} session - Guacamole session
|
||||
* @returns {ReactElement} Guacamole mouse plugin
|
||||
*/
|
||||
(session) => {
|
||||
const { id, client } = session
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!client) return
|
||||
|
||||
const ctrlKey = 65507
|
||||
const altKey = 65513
|
||||
const delKey = 65535
|
||||
|
||||
client?.sendKeyEvent(1, ctrlKey)
|
||||
client?.sendKeyEvent(1, altKey)
|
||||
client?.sendKeyEvent(1, delKey)
|
||||
client?.sendKeyEvent(0, delKey)
|
||||
client?.sendKeyEvent(0, altKey)
|
||||
client?.sendKeyEvent(0, ctrlKey)
|
||||
}, [client])
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-cy={`${id}-ctrl-alt-del-button`}
|
||||
onClick={handleClick}
|
||||
disableElevation
|
||||
variant="outlined"
|
||||
color="error"
|
||||
>
|
||||
<Translate word={T.SendCtrlAltDel} />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* @param {GuacamoleSession} session - Guacamole session
|
||||
* @returns {ReactElement} Guacamole mouse plugin
|
||||
*/
|
||||
const GuacamoleReconnectButton = (session) => {
|
||||
const { id, isLoading, handleReconnect } = session
|
||||
const [reconnecting, setReconnecting] = useState(false)
|
||||
|
||||
const handleReconnectSession = async () => {
|
||||
if (isLoading) return
|
||||
|
||||
setReconnecting(true)
|
||||
await handleReconnect()
|
||||
setReconnecting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
arrow
|
||||
placement="bottom"
|
||||
title={
|
||||
<Typography variant="subtitle2">
|
||||
<Translate word={T.Reconnect} />
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
data-cy={`${id}-reconnect-button`}
|
||||
onClick={handleReconnectSession}
|
||||
>
|
||||
{reconnecting || isLoading ? (
|
||||
<CircularProgress color="secondary" size={20} />
|
||||
) : (
|
||||
<Refresh />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const GuacamoleFullScreenButton = memo(
|
||||
/**
|
||||
* @param {GuacamoleSession} session - Guacamole session
|
||||
* @returns {ReactElement} Guacamole mouse plugin
|
||||
*/
|
||||
(session) => {
|
||||
const { id, viewport } = session
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
// If the document is not in full screen mode make the video full screen
|
||||
if (!document.fullscreenElement && document.fullscreenEnabled) {
|
||||
viewport?.requestFullscreen?.()
|
||||
} else if (document.exitFullscreen) {
|
||||
document.exitFullscreen()
|
||||
}
|
||||
}, [viewport])
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
arrow
|
||||
placement="bottom"
|
||||
title={
|
||||
<Typography variant="subtitle2">
|
||||
<Translate word={T.FullScreen} />
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<IconButton data-cy={`${id}-fullscreen-button`} onClick={handleClick}>
|
||||
<Maximize />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const GuacamoleScreenshotButton = memo(
|
||||
/**
|
||||
* @param {GuacamoleSession} session - Guacamole session
|
||||
* @returns {ReactElement} Guacamole mouse plugin
|
||||
*/
|
||||
(session) => {
|
||||
const { id, client } = session
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!client) return
|
||||
|
||||
const canvas = client.getDisplay().getDefaultLayer().getCanvas()
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
downloadFile(new File([blob], 'screenshot.png'))
|
||||
}, 'image/png')
|
||||
}, [client])
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
arrow
|
||||
placement="bottom"
|
||||
title={
|
||||
<Typography variant="subtitle2">
|
||||
<Translate word={T.Screenshot} />
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<IconButton data-cy={`${id}-screenshot-button`} onClick={handleClick}>
|
||||
<Camera />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const ButtonPropTypes = {
|
||||
client: PropTypes.object,
|
||||
viewport: PropTypes.object,
|
||||
}
|
||||
|
||||
GuacamoleCtrlAltDelButton.displayName = 'GuacamoleCtrlAltDelButton'
|
||||
GuacamoleCtrlAltDelButton.propTypes = ButtonPropTypes
|
||||
GuacamoleReconnectButton.displayName = 'GuacamoleReconnectButton'
|
||||
GuacamoleReconnectButton.propTypes = ButtonPropTypes
|
||||
GuacamoleFullScreenButton.displayName = 'GuacamoleFullScreenButton'
|
||||
GuacamoleFullScreenButton.propTypes = ButtonPropTypes
|
||||
GuacamoleScreenshotButton.displayName = 'GuacamoleScreenshotButton'
|
||||
GuacamoleScreenshotButton.propTypes = ButtonPropTypes
|
||||
|
||||
export {
|
||||
GuacamoleCtrlAltDelButton,
|
||||
GuacamoleReconnectButton,
|
||||
GuacamoleFullScreenButton,
|
||||
GuacamoleScreenshotButton,
|
||||
}
|
217
src/fireedge/src/client/components/Consoles/Guacamole/client.js
Normal file
217
src/fireedge/src/client/components/Consoles/Guacamole/client.js
Normal file
@ -0,0 +1,217 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { useRef, useEffect, RefObject } from 'react'
|
||||
import { WebSocketTunnel, Tunnel, Client } from 'guacamole-common-js'
|
||||
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { useGuacamole, useGuacamoleApi } from 'client/features/Guacamole'
|
||||
import { getConnectString, clientStateToString } from 'client/models/Guacamole'
|
||||
import { fakeDelay, isDevelopment } from 'client/utils'
|
||||
import {
|
||||
GuacamoleSession, // eslint-disable-line no-unused-vars
|
||||
SOCKETS,
|
||||
GUACAMOLE_STATES_STR,
|
||||
THUMBNAIL_UPDATE_FREQUENCY,
|
||||
} from 'client/constants'
|
||||
|
||||
const {
|
||||
CONNECTING,
|
||||
CONNECTED,
|
||||
DISCONNECTING,
|
||||
DISCONNECTED,
|
||||
CLIENT_ERROR,
|
||||
TUNNEL_ERROR,
|
||||
} = GUACAMOLE_STATES_STR
|
||||
|
||||
// eslint-disable-next-line jsdoc/valid-types
|
||||
/**
|
||||
* @typedef {GuacamoleSession & {
|
||||
* handleConnect: Function,
|
||||
* handleDisconnect: Function,
|
||||
* handleReconnect: function():Promise,
|
||||
* }} GuacamoleClientType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {object} options - Client options
|
||||
* @param {string} options.id - Session includes type and VM id. Eg: '6-vnc'
|
||||
* @param {RefObject} options.display - Session display. Only exists if display plugins is enabled
|
||||
* @returns {GuacamoleClientType} Guacamole client props
|
||||
*/
|
||||
const GuacamoleClient = ({ id, display }) => {
|
||||
const guac = useRef(createGuacamoleClient()).current
|
||||
|
||||
// Automatically update the client thumbnail
|
||||
guac.client.onsync = () => handleUpdateThumbnail()
|
||||
|
||||
const { enqueueError, enqueueInfo, enqueueSuccess } = useGeneralApi()
|
||||
const { token, ...session } = useGuacamole(id)
|
||||
const {
|
||||
setConnectionState,
|
||||
setTunnelUnstable,
|
||||
setMultiTouchSupport,
|
||||
updateThumbnail,
|
||||
} = useGuacamoleApi(id)
|
||||
|
||||
const handleConnect = (width, height, force = false) => {
|
||||
if (!session?.isUninitialized && !session.isDisconnected && !force) return
|
||||
|
||||
isDevelopment() && console.log(`connect ${id} 🔵`)
|
||||
|
||||
const options = { token, display, width, height }
|
||||
const connectString = getConnectString(options)
|
||||
|
||||
guac.client.connect(connectString)
|
||||
}
|
||||
|
||||
const handleDisconnect = () => {
|
||||
try {
|
||||
isDevelopment() && console.log(`disconnect ${id} 🔴`)
|
||||
guac.client?.disconnect()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleReconnect = async (width, height) => {
|
||||
session?.isConnected && handleDisconnect()
|
||||
|
||||
// sleep to avoid quick reconnection
|
||||
await fakeDelay(1500)
|
||||
handleConnect(width, height, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the thumbnail of the given managed client within the connection
|
||||
* history under its associated ID. If the client is not connected, this
|
||||
* function has no effect.
|
||||
*/
|
||||
const handleUpdateThumbnail = () => {
|
||||
const nowTimestamp = new Date().getTime()
|
||||
const lastTimestamp = session?.thumbnail?.timestamp
|
||||
|
||||
if (
|
||||
lastTimestamp &&
|
||||
nowTimestamp - lastTimestamp < THUMBNAIL_UPDATE_FREQUENCY
|
||||
)
|
||||
return
|
||||
|
||||
const clientDisplay = guac.client?.getDisplay()
|
||||
|
||||
if (clientDisplay?.getWidth() <= 0 || clientDisplay?.getHeight() <= 0)
|
||||
return
|
||||
|
||||
// Get screenshot
|
||||
const canvas = clientDisplay.flatten()
|
||||
|
||||
// Calculate scale of thumbnail (max 320x240, max zoom 100%)
|
||||
const scale = Math.min(320 / canvas.width, 240 / canvas.height, 1)
|
||||
|
||||
// Create thumbnail canvas
|
||||
const thumbnail = document.createElement('canvas')
|
||||
thumbnail.width = canvas.width * scale
|
||||
thumbnail.height = canvas.height * scale
|
||||
|
||||
// Scale screenshot to thumbnail
|
||||
const context = thumbnail.getContext('2d')
|
||||
context.drawImage(
|
||||
canvas,
|
||||
0,
|
||||
0,
|
||||
canvas.width,
|
||||
canvas.height,
|
||||
0,
|
||||
0,
|
||||
thumbnail.width,
|
||||
thumbnail.height
|
||||
)
|
||||
|
||||
thumbnail.toBlob((blob) => {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const newThumbnail = { timestamp: nowTimestamp, canvas: url }
|
||||
updateThumbnail({ thumbnail: newThumbnail })
|
||||
}, 'image/webp')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
guac.tunnel.onerror = (status) => {
|
||||
setConnectionState({ state: TUNNEL_ERROR, statusCode: status.code })
|
||||
}
|
||||
|
||||
guac.tunnel.onstatechange = (state) => {
|
||||
;({
|
||||
[Tunnel.State.CONNECTING]: () => {
|
||||
setConnectionState({ state: CONNECTING })
|
||||
},
|
||||
[Tunnel.State.OPEN]: () => {
|
||||
setTunnelUnstable({ unstable: false })
|
||||
},
|
||||
[Tunnel.State.UNSTABLE]: () => {
|
||||
setTunnelUnstable({ unstable: true })
|
||||
},
|
||||
[Tunnel.State.CLOSED]: () => {
|
||||
setConnectionState({ state: DISCONNECTED })
|
||||
},
|
||||
}[state]?.())
|
||||
}
|
||||
|
||||
guac.client.onstatechange = (state) => {
|
||||
const stateString = clientStateToString(state)
|
||||
const isDisconnect = [DISCONNECTING, DISCONNECTED].includes(stateString)
|
||||
const isDisconnected = DISCONNECTED === stateString
|
||||
const isConnected = CONNECTED === stateString
|
||||
|
||||
isConnected && enqueueSuccess('Connection established')
|
||||
isDisconnected && enqueueInfo('Disconnected')
|
||||
|
||||
!isDisconnect && setConnectionState({ state: stateString })
|
||||
}
|
||||
|
||||
guac.client.onerror = (status) => {
|
||||
enqueueError(status.message)
|
||||
setConnectionState({ state: CLIENT_ERROR, statusCode: status.code })
|
||||
}
|
||||
|
||||
guac.client.onmultitouch = (layer, touches) => {
|
||||
setMultiTouchSupport({ touches })
|
||||
}
|
||||
|
||||
return () => {
|
||||
handleDisconnect()
|
||||
}
|
||||
}, [id])
|
||||
|
||||
useEffect(() => {
|
||||
session?.isError && handleDisconnect()
|
||||
}, [session?.isError])
|
||||
|
||||
useEffect(() => {
|
||||
!session.isConnected && handleConnect()
|
||||
}, [token])
|
||||
|
||||
return { token, ...session, ...guac, handleReconnect }
|
||||
}
|
||||
|
||||
const createGuacamoleClient = () => {
|
||||
const { protocol, host } = window.location
|
||||
const websocketProtocol = protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const guacamoleWs = `${websocketProtocol}//${host}/fireedge/${SOCKETS.GUACAMOLE}`
|
||||
|
||||
const tunnel = new WebSocketTunnel(guacamoleWs)
|
||||
const client = new Client(tunnel)
|
||||
|
||||
return { client, tunnel }
|
||||
}
|
||||
|
||||
export default GuacamoleClient
|
@ -0,0 +1,63 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable jsdoc/valid-types */
|
||||
import { useRef } from 'react'
|
||||
|
||||
import {
|
||||
useGetLatest,
|
||||
reducePlugin,
|
||||
} from 'client/components/Consoles/Guacamole/utils'
|
||||
import GuacamoleClient, {
|
||||
GuacamoleClientType,
|
||||
} from 'client/components/Consoles/Guacamole/client'
|
||||
import {
|
||||
GuacamoleDisplayPlugin,
|
||||
GuacamoleKeyboardPlugin,
|
||||
GuacamoleMousePlugin,
|
||||
} from 'client/components/Consoles/Guacamole/plugins'
|
||||
|
||||
/**
|
||||
* Creates guacamole session.
|
||||
*
|
||||
* @param {object} options - Options
|
||||
* @param {string} options.id - Session includes type and VM id. Eg: '6-vnc'
|
||||
* @param {...any} [plugins] - Plugins
|
||||
* @returns {GuacamoleClientType &
|
||||
* GuacamoleDisplayPlugin &
|
||||
* GuacamoleKeyboardPlugin &
|
||||
* GuacamoleMousePlugin} session
|
||||
*/
|
||||
const useGuacamoleSession = (options, ...plugins) => {
|
||||
// Create the guacamole instance
|
||||
const instanceRef = useRef({})
|
||||
const getInstance = useGetLatest(instanceRef.current)
|
||||
|
||||
// Assign the options to the instance
|
||||
Object.assign(getInstance(), { ...options })
|
||||
|
||||
// Assign the session and plugins to the instance
|
||||
Object.assign(
|
||||
getInstance(),
|
||||
[GuacamoleClient, ...plugins].reduce(reducePlugin, getInstance())
|
||||
)
|
||||
|
||||
return getInstance()
|
||||
}
|
||||
|
||||
export * from 'client/components/Consoles/Guacamole/plugins'
|
||||
export * from 'client/components/Consoles/Guacamole/buttons'
|
||||
export { useGuacamoleSession }
|
@ -0,0 +1,158 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { useEffect, useState } from 'react'
|
||||
import {
|
||||
StringReader,
|
||||
StringWriter,
|
||||
BlobReader,
|
||||
BlobWriter,
|
||||
} from 'guacamole-common-js'
|
||||
|
||||
import { GuacamoleSession } from 'client/constants'
|
||||
import { isDevelopment } from 'client/utils'
|
||||
|
||||
const createClipboardData = ({ id, type, data } = {}) => ({
|
||||
source: id,
|
||||
/**
|
||||
* The mimetype of the data currently stored within the clipboard.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
type: type || 'text/plain',
|
||||
/**
|
||||
* The data currently stored within the clipboard.
|
||||
*
|
||||
* @type {string|Blob|File}
|
||||
*/
|
||||
data: data ?? '',
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {GuacamoleSession} session - Current session
|
||||
* @returns {null} null
|
||||
*/
|
||||
const GuacamoleClipboard = (session) => {
|
||||
const { id, client, isConnected } = session ?? {}
|
||||
|
||||
const [pendingRead, setPendingRead] = useState(() => false)
|
||||
const [storedClipboard, storeClipboard] = useState(() =>
|
||||
createClipboardData({ id })
|
||||
)
|
||||
|
||||
const getLocalClipboard = async () => {
|
||||
try {
|
||||
if (pendingRead) return
|
||||
|
||||
const text = await navigator.clipboard.readText()
|
||||
storeClipboard((prev) => ({ ...prev, data: text, type: 'text/plain' }))
|
||||
|
||||
return text
|
||||
} finally {
|
||||
setPendingRead(false)
|
||||
}
|
||||
}
|
||||
|
||||
const setLocalClipboard = async ({ data, type }) => {
|
||||
if (type !== 'text/plain') return
|
||||
|
||||
await navigator.clipboard.writeText(data)
|
||||
storeClipboard((prev) => ({ ...prev, data, type }))
|
||||
}
|
||||
|
||||
const setClientClipboard = ({ data, type = 'text/plain' } = {}) => {
|
||||
// Create stream with proper mimetype
|
||||
const stream = client.createClipboardStream(type)
|
||||
|
||||
// Send data as a string if it is stored as a string
|
||||
if (typeof data === 'string') {
|
||||
const writer = new StringWriter(stream)
|
||||
writer.sendText(data)
|
||||
writer.sendEnd()
|
||||
}
|
||||
// Otherwise, assume the data is a File/Blob
|
||||
else {
|
||||
// Write File/Blob asynchronously
|
||||
const writer = new BlobWriter(stream)
|
||||
writer.oncomplete = () => {
|
||||
writer.sendEnd()
|
||||
}
|
||||
|
||||
// Begin sending data
|
||||
writer.sendBlob(data)
|
||||
}
|
||||
}
|
||||
|
||||
const resyncClipboard = async () => {
|
||||
try {
|
||||
const localClipboard = await getLocalClipboard()
|
||||
setClientClipboard({ data: localClipboard })
|
||||
} catch (e) {
|
||||
isDevelopment() && console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
const focusGained = (evt) => {
|
||||
// Only recheck clipboard if it's the window itself that gained focus
|
||||
evt.target === window && resyncClipboard()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected) return
|
||||
;(async () => await resyncClipboard())()
|
||||
|
||||
window.addEventListener('load', resyncClipboard, true)
|
||||
window.addEventListener('copy', resyncClipboard)
|
||||
window.addEventListener('cut', resyncClipboard)
|
||||
window.addEventListener('focus', focusGained, true)
|
||||
|
||||
client.onclipboard = (stream, mimetype) => {
|
||||
// If the received data is text, read it as a simple string
|
||||
if (/^text\//.exec(mimetype)) {
|
||||
const reader = new StringReader(stream)
|
||||
|
||||
// Assemble received data into a single string
|
||||
let data = ''
|
||||
reader.ontext = (text) => {
|
||||
data += text
|
||||
}
|
||||
|
||||
// Set clipboard contents once stream is finished
|
||||
reader.onend = () => {
|
||||
setLocalClipboard({ data, type: mimetype })
|
||||
}
|
||||
}
|
||||
// Otherwise read the clipboard data as a Blob
|
||||
else {
|
||||
const reader = new BlobReader(stream, mimetype)
|
||||
|
||||
reader.onend = () => {
|
||||
setLocalClipboard({ data: reader.getBlob(), type: mimetype })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('load', resyncClipboard, true)
|
||||
window.removeEventListener('copy', resyncClipboard)
|
||||
window.removeEventListener('cut', resyncClipboard)
|
||||
window.removeEventListener('focus', focusGained, true)
|
||||
}
|
||||
}, [isConnected])
|
||||
|
||||
return { storedClipboard }
|
||||
}
|
||||
|
||||
export { GuacamoleClipboard }
|
@ -0,0 +1,147 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
MutableRefObject, // eslint-disable-line no-unused-vars
|
||||
ReactElement, // eslint-disable-line no-unused-vars
|
||||
} from 'react'
|
||||
import { styled } from '@mui/material'
|
||||
|
||||
import { GuacamoleSession } from 'client/constants'
|
||||
|
||||
/**
|
||||
* @typedef GuacamoleDisplayPlugin
|
||||
* @property {MutableRefObject} [display] - Display object
|
||||
* @property {MutableRefObject} [viewport] - Viewport object is the wrapper of display
|
||||
* @property {ReactElement} [displayElement] - Display element
|
||||
*/
|
||||
|
||||
const Viewport = styled('div')({
|
||||
backgroundColor: '#222431',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
placeContent: 'center',
|
||||
})
|
||||
|
||||
const Display = styled('div')({
|
||||
zIndex: 1,
|
||||
overflow: 'hidden',
|
||||
'& > *': { cursor: 'none' },
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {GuacamoleSession} session - Guacamole session
|
||||
* @returns {GuacamoleDisplayPlugin} Guacamole display plugin
|
||||
*/
|
||||
const GuacamoleDisplay = (session) => {
|
||||
const { id, container, header, client, isConnected } = session ?? {}
|
||||
const isSSH = useMemo(() => id.includes('ssh'), [id])
|
||||
|
||||
const viewportRef = useRef(null)
|
||||
const displayRef = useRef(null)
|
||||
|
||||
const containerResized = () => {
|
||||
if (!client || !container) return
|
||||
|
||||
const clientDisplay = client.getDisplay()
|
||||
const pixelDensity = window.devicePixelRatio || 1
|
||||
const headerHeight = header?.offsetHeight ?? 0
|
||||
|
||||
const width = document.fullscreenElement
|
||||
? window.innerWidth * pixelDensity
|
||||
: container.offsetWidth * pixelDensity
|
||||
|
||||
const height = document.fullscreenElement
|
||||
? window.innerHeight * pixelDensity
|
||||
: (container.offsetHeight - headerHeight) * pixelDensity
|
||||
|
||||
if (
|
||||
clientDisplay.getWidth() !== width ||
|
||||
clientDisplay.getHeight() !== height
|
||||
) {
|
||||
client.sendSize(width, height)
|
||||
}
|
||||
|
||||
// when type connection is SSH, display doesn't need scale
|
||||
id.includes('vnc') && updateDisplayScale()
|
||||
}
|
||||
|
||||
const updateDisplayScale = () => {
|
||||
if (!client) return
|
||||
|
||||
const clientDisplay = client.getDisplay()
|
||||
// Get screen resolution.
|
||||
const origHeight = Math.max(clientDisplay.getHeight(), 1)
|
||||
const origWidth = Math.max(clientDisplay.getWidth(), 1)
|
||||
|
||||
const headerHeight = header?.offsetHeight ?? 0
|
||||
|
||||
const containerWidth = document.fullscreenElement
|
||||
? window.innerWidth
|
||||
: container.offsetWidth
|
||||
|
||||
const containerHeight = document.fullscreenElement
|
||||
? window.innerHeight
|
||||
: container.offsetHeight - headerHeight
|
||||
|
||||
const xScale = containerWidth / origWidth
|
||||
const yScale = containerHeight / origHeight
|
||||
|
||||
// This is done to handle both X and Y axis
|
||||
let scale = Math.min(yScale, xScale)
|
||||
|
||||
// Limit to 1
|
||||
scale = Math.min(scale, 1)
|
||||
|
||||
scale !== 0 && clientDisplay.scale(scale)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected) return
|
||||
|
||||
const display = displayRef.current
|
||||
const clientDisplay = client.getDisplay()
|
||||
|
||||
display?.appendChild(clientDisplay.getElement())
|
||||
|
||||
const pollResize = setInterval(containerResized, 10)
|
||||
|
||||
return () => {
|
||||
display?.childNodes.forEach((node) => display?.removeChild(node))
|
||||
clearInterval(pollResize)
|
||||
}
|
||||
}, [isConnected])
|
||||
|
||||
return {
|
||||
display: displayRef.current,
|
||||
viewport: viewportRef.current,
|
||||
displayElement: (
|
||||
<Viewport ref={viewportRef}>
|
||||
<Display
|
||||
ref={displayRef}
|
||||
sx={isSSH ? { width: '100%', height: '100%' } : {}}
|
||||
/>
|
||||
</Viewport>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export { GuacamoleDisplay }
|
@ -0,0 +1,20 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export * from 'client/components/Consoles/Guacamole/plugins/clipboard'
|
||||
export * from 'client/components/Consoles/Guacamole/plugins/display'
|
||||
export * from 'client/components/Consoles/Guacamole/plugins/keyboard'
|
||||
export * from 'client/components/Consoles/Guacamole/plugins/mouse'
|
@ -0,0 +1,55 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Keyboard } from 'guacamole-common-js'
|
||||
|
||||
import { GuacamoleSession } from 'client/constants'
|
||||
|
||||
/**
|
||||
* @typedef GuacamoleKeyboardPlugin
|
||||
* @property {Keyboard} [keyboard] - Guacamole keyboard
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {GuacamoleSession} session - Guacamole session
|
||||
* @returns {GuacamoleKeyboardPlugin} Guacamole keyboard plugin
|
||||
*/
|
||||
const GuacamoleKeyboard = (session) => {
|
||||
const { client, isConnected } = session ?? {}
|
||||
|
||||
const keyboardRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected) return
|
||||
|
||||
keyboardRef.current = new Keyboard(document)
|
||||
|
||||
keyboardRef.current.onkeydown = (keySym) => client?.sendKeyEvent(1, keySym)
|
||||
keyboardRef.current.onkeyup = (keySym) => client?.sendKeyEvent(0, keySym)
|
||||
|
||||
// Release all keys when window loses focus
|
||||
window.addEventListener('blur', keyboardRef.current?.reset)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('blur', keyboardRef.current?.reset)
|
||||
}
|
||||
}, [isConnected])
|
||||
|
||||
return { keyboard: keyboardRef.current }
|
||||
}
|
||||
|
||||
export { GuacamoleKeyboard }
|
@ -0,0 +1,78 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Mouse } from 'guacamole-common-js'
|
||||
|
||||
import { GuacamoleSession } from 'client/constants'
|
||||
|
||||
/**
|
||||
* @typedef GuacamoleMousePlugin
|
||||
* @property {Mouse} [mouse] - Guacamole mouse
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {GuacamoleSession} session - Guacamole session
|
||||
* @returns {GuacamoleMousePlugin} Guacamole mouse plugin
|
||||
*/
|
||||
const GuacamoleMouse = (session) => {
|
||||
const { client, display, isConnected } = session ?? {}
|
||||
|
||||
const mouseRef = useRef(null)
|
||||
|
||||
const handleMouseState = (mouseState, scaleMouse = false) => {
|
||||
// Do not attempt to handle mouse state changes if the client
|
||||
// or display are not yet available
|
||||
if (!client) return
|
||||
|
||||
if (scaleMouse) {
|
||||
const clientScale = client.getDisplay().getScale()
|
||||
mouseState.y = mouseState.y / clientScale
|
||||
mouseState.x = mouseState.x / clientScale
|
||||
}
|
||||
|
||||
// Send mouse state, show cursor if necessary
|
||||
// client.display?.showCursor(!localCursor)
|
||||
client.sendMouseState(mouseState)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected) return
|
||||
|
||||
const mouse = new Mouse(client.getDisplay().getElement())
|
||||
mouseRef.current = mouse
|
||||
|
||||
mouse.onmousedown = mouse.onmouseup = (mouseState) => {
|
||||
// Ensure focus is regained via mousedown before forwarding event
|
||||
display?.focus()
|
||||
handleMouseState(mouseState)
|
||||
}
|
||||
|
||||
// Forward mousemove events untouched
|
||||
mouse.onmousemove = (mouseState) => {
|
||||
handleMouseState(mouseState, true)
|
||||
}
|
||||
|
||||
// Hide software cursor when mouse leaves display
|
||||
mouse.onmouseout = () => {
|
||||
// client?.display?.showCursor(false)
|
||||
}
|
||||
}, [isConnected])
|
||||
|
||||
return { mouse: mouseRef.current }
|
||||
}
|
||||
|
||||
export { GuacamoleMouse }
|
@ -0,0 +1,41 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { useCallback, useRef } from 'react'
|
||||
|
||||
/**
|
||||
* Helps avoid a lot of potential memory leaks.
|
||||
*
|
||||
* @param {object} obj - Instance object
|
||||
* @returns {function():object} - Returns the last object
|
||||
*/
|
||||
export const useGetLatest = (obj) => {
|
||||
const ref = useRef()
|
||||
ref.current = obj
|
||||
|
||||
return useCallback(() => ref.current, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign the plugin state to the previous state.
|
||||
*
|
||||
* @param {object} prevState - Previous state
|
||||
* @param {function():object} plugin - Plugin
|
||||
* @returns {object} Returns the new state
|
||||
*/
|
||||
export const reducePlugin = (prevState, plugin) => ({
|
||||
...prevState,
|
||||
...plugin(prevState),
|
||||
})
|
16
src/fireedge/src/client/components/Consoles/index.js
Normal file
16
src/fireedge/src/client/components/Consoles/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
export * from 'client/components/Consoles/Guacamole'
|
@ -32,9 +32,9 @@ const TYPE = {
|
||||
dependOf: '$general.HYPERVISOR',
|
||||
values: (hypervisor = kvm) => {
|
||||
const types = {
|
||||
[vcenter]: [T.VMRC],
|
||||
[lxc]: [T.VNC],
|
||||
}[hypervisor] ?? [T.VNC, T.SDL, T.SPICE]
|
||||
[vcenter]: [T.Vmrc],
|
||||
[lxc]: [T.Vnc],
|
||||
}[hypervisor] ?? [T.Vnc, T.Sdl, T.Spice]
|
||||
|
||||
return arrayToOptions(types)
|
||||
},
|
||||
|
@ -19,6 +19,7 @@ import PropTypes from 'prop-types'
|
||||
|
||||
import { useAuth, useAuthApi } from 'client/features/Auth'
|
||||
import { authApi } from 'client/features/AuthApi'
|
||||
import { oneApi } from 'client/features/OneApi'
|
||||
import groupApi from 'client/features/OneApi/group'
|
||||
import FullscreenProgress from 'client/components/LoadingScreen'
|
||||
import { findStorageData } from 'client/utils'
|
||||
@ -46,6 +47,8 @@ const AuthLayout = ({ subscriptions = [], children }) => {
|
||||
|
||||
return () => {
|
||||
authSubscription.unsubscribe()
|
||||
dispatch(authApi.util.resetApiState())
|
||||
dispatch(oneApi.util.resetApiState())
|
||||
}
|
||||
}, [dispatch, jwt])
|
||||
|
||||
|
@ -14,10 +14,9 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useRef, useEffect } from 'react'
|
||||
import { useRef, useEffect, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import clsx from 'clsx'
|
||||
import { Box, Container } from '@mui/material'
|
||||
import { CSSTransition } from 'react-transition-group'
|
||||
|
||||
@ -25,8 +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'
|
||||
|
||||
const InternalLayout = ({ title, children }) => {
|
||||
const InternalLayout = ({ title, customHeader, disabledSidebar, children }) => {
|
||||
const classes = internalStyles()
|
||||
const container = useRef()
|
||||
const { isFixMenu } = useGeneral()
|
||||
@ -40,9 +40,27 @@ const InternalLayout = ({ title, children }) => {
|
||||
return (
|
||||
<Box
|
||||
data-cy="main-layout"
|
||||
className={clsx(classes.root, { [classes.isDrawerFixed]: isFixMenu })}
|
||||
className={classes.root}
|
||||
sx={useMemo(
|
||||
() =>
|
||||
disabledSidebar
|
||||
? {}
|
||||
: {
|
||||
marginLeft: {
|
||||
lg: isFixMenu
|
||||
? `${sidebar.fixed}px`
|
||||
: `${sidebar.minified}px`,
|
||||
},
|
||||
},
|
||||
[isFixMenu, disabledSidebar]
|
||||
)}
|
||||
>
|
||||
<Header scrollContainer={container.current} />
|
||||
{customHeader ?? (
|
||||
<Header
|
||||
disabledSidebar={disabledSidebar}
|
||||
scrollContainer={container.current}
|
||||
/>
|
||||
)}
|
||||
<Box component="main" className={classes.main}>
|
||||
<CSSTransition
|
||||
in
|
||||
@ -75,6 +93,8 @@ const InternalLayout = ({ title, children }) => {
|
||||
|
||||
InternalLayout.propTypes = {
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
customHeader: PropTypes.node,
|
||||
disabledSidebar: PropTypes.bool,
|
||||
children: PropTypes.any,
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { sidebar, toolbar, footer } from 'client/theme/defaults'
|
||||
import { toolbar, footer } from 'client/theme/defaults'
|
||||
|
||||
export default makeStyles((theme) => ({
|
||||
root: {
|
||||
@ -27,14 +27,6 @@ export default makeStyles((theme) => ({
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
marginLeft: sidebar.minified,
|
||||
},
|
||||
},
|
||||
isDrawerFixed: {
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
marginLeft: sidebar.fixed,
|
||||
},
|
||||
},
|
||||
main: {
|
||||
height: '100vh',
|
||||
@ -49,7 +41,6 @@ export default makeStyles((theme) => ({
|
||||
},
|
||||
},
|
||||
scrollable: {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(2),
|
||||
height: '100%',
|
||||
|
@ -38,6 +38,7 @@ const HeaderPopover = memo(
|
||||
icon,
|
||||
buttonLabel,
|
||||
buttonProps,
|
||||
onMouseHover,
|
||||
headerTitle,
|
||||
popperProps,
|
||||
children,
|
||||
@ -78,7 +79,9 @@ const HeaderPopover = memo(
|
||||
aria-haspopup
|
||||
aria-describedby={hasId}
|
||||
aria-expanded={open ? 'true' : 'false'}
|
||||
onClick={handleClick}
|
||||
{...(onMouseHover
|
||||
? { onMouseEnter: handleClick, onMouseLeave: handleClose }
|
||||
: { onClick: handleClick })}
|
||||
size="small"
|
||||
endIcon={<CaretIcon />}
|
||||
startIcon={icon}
|
||||
@ -147,6 +150,7 @@ HeaderPopover.propTypes = {
|
||||
buttonProps: PropTypes.object,
|
||||
tooltip: PropTypes.any,
|
||||
headerTitle: PropTypes.any,
|
||||
onMouseHover: PropTypes.bool,
|
||||
disablePadding: PropTypes.bool,
|
||||
popperProps: PropTypes.object,
|
||||
children: PropTypes.func,
|
||||
@ -160,6 +164,7 @@ HeaderPopover.defaultProps = {
|
||||
buttonProps: {},
|
||||
headerTitle: undefined,
|
||||
disablePadding: false,
|
||||
onMouseHover: false,
|
||||
popperProps: {},
|
||||
children: () => undefined,
|
||||
}
|
||||
|
@ -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 = () => {
|
||||
const Header = ({ disabledSidebar = false }) => {
|
||||
const { isOneAdmin } = useAuth()
|
||||
const { fixMenu } = useGeneralApi()
|
||||
const { appTitle, title, isBeta, withGroupSwitcher } = useGeneral()
|
||||
@ -45,15 +45,17 @@ const Header = () => {
|
||||
return (
|
||||
<AppBar data-cy="header" elevation={0} position="absolute">
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
onClick={() => fixMenu(true)}
|
||||
edge="start"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ display: { lg: 'none' } }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
{!disabledSidebar && (
|
||||
<IconButton
|
||||
onClick={() => fixMenu(true)}
|
||||
edge="start"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ display: { lg: 'none' } }}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<Box
|
||||
flexGrow={1}
|
||||
ml={2}
|
||||
@ -82,20 +84,22 @@ const Header = () => {
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
data-cy="header-description"
|
||||
sx={{
|
||||
display: { xs: 'none', xl: 'block' },
|
||||
'&::before': {
|
||||
content: '"|"',
|
||||
margin: '0.5em',
|
||||
color: 'primary.contrastText',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{title && (
|
||||
<Typography
|
||||
variant="h6"
|
||||
data-cy="header-description"
|
||||
sx={{
|
||||
display: { xs: 'none', xl: 'block' },
|
||||
'&::before': {
|
||||
content: '"|"',
|
||||
margin: '0.5em',
|
||||
color: 'primary.contrastText',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
@ -113,11 +117,8 @@ const Header = () => {
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
disabledSidebar: PropTypes.bool,
|
||||
scrollContainer: PropTypes.object,
|
||||
}
|
||||
|
||||
Header.defaultProps = {
|
||||
scrollContainer: null,
|
||||
}
|
||||
|
||||
export default Header
|
||||
|
@ -16,6 +16,7 @@
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useLocation, matchPath } from 'react-router'
|
||||
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
@ -41,6 +42,7 @@ import SidebarLink from 'client/components/Sidebar/SidebarLink'
|
||||
import SidebarCollapseItem from 'client/components/Sidebar/SidebarCollapseItem'
|
||||
|
||||
const Sidebar = ({ endpoints }) => {
|
||||
const { pathname } = useLocation()
|
||||
const classes = sidebarStyles()
|
||||
const isUpLg = useMediaQuery((theme) => theme.breakpoints.up('lg'), {
|
||||
noSsr: true,
|
||||
@ -66,6 +68,18 @@ const Sidebar = ({ endpoints }) => {
|
||||
[endpoints]
|
||||
)
|
||||
|
||||
const isDisabledSidebar = useMemo(() => {
|
||||
const endpoint = endpoints.find(({ path }) =>
|
||||
matchPath(pathname, { path, exact: true })
|
||||
)
|
||||
|
||||
return endpoint?.disabledSidebar
|
||||
}, [pathname])
|
||||
|
||||
if (isDisabledSidebar) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
|
@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useState, useEffect, useMemo, ReactElement } from 'react'
|
||||
import { useMemo, ReactElement } from 'react'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import { useGetVmsQuery } from 'client/features/OneApi/vm'
|
||||
@ -23,15 +23,6 @@ import VmColumns from 'client/components/Tables/Vms/columns'
|
||||
import VmRow from 'client/components/Tables/Vms/row'
|
||||
import { RESOURCE_NAMES } from 'client/constants'
|
||||
|
||||
const INITIAL_ELEMENT = 0
|
||||
const INTERVAL_ON_FIRST_RENDER = 2_000
|
||||
|
||||
const INITIAL_ARGS = {
|
||||
start: INITIAL_ELEMENT,
|
||||
end: -INTERVAL_ON_FIRST_RENDER,
|
||||
state: -1,
|
||||
}
|
||||
|
||||
const DEFAULT_DATA_CY = 'vms'
|
||||
|
||||
/**
|
||||
@ -54,10 +45,11 @@ const VmsTable = (props) => {
|
||||
)
|
||||
|
||||
const { view, getResourceView } = useViews()
|
||||
const [totalData, setTotalData] = useState(() => [])
|
||||
const [args, setArgs] = useState(() => INITIAL_ARGS)
|
||||
const { data, isSuccess, refetch, isFetching } = useGetVmsQuery(args, {
|
||||
refetchOnMountOrArgChange: true,
|
||||
const { data, refetch, isFetching } = useGetVmsQuery(undefined, {
|
||||
selectFromResult: (result) => ({
|
||||
...result,
|
||||
data: result?.data?.filter(({ STATE }) => STATE !== '6') ?? [],
|
||||
}),
|
||||
})
|
||||
|
||||
const columns = useMemo(
|
||||
@ -69,41 +61,13 @@ const VmsTable = (props) => {
|
||||
[view]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFetching && isSuccess && data?.length >= +INTERVAL_ON_FIRST_RENDER) {
|
||||
setArgs((prev) => ({
|
||||
...prev,
|
||||
start: prev.start + INTERVAL_ON_FIRST_RENDER,
|
||||
}))
|
||||
}
|
||||
}, [isFetching])
|
||||
|
||||
useEffect(() => {
|
||||
isSuccess &&
|
||||
data &&
|
||||
setTotalData((prev) => {
|
||||
const notDuplicatedData = data.filter(
|
||||
({ ID }) => !prev.find((vm) => vm.ID === ID)
|
||||
)
|
||||
|
||||
return prev.concat(notDuplicatedData).sort((a, b) => b.ID - a.ID)
|
||||
})
|
||||
}, [isSuccess])
|
||||
|
||||
return (
|
||||
<EnhancedTable
|
||||
columns={columns}
|
||||
data={useMemo(
|
||||
() => totalData?.filter(({ STATE }) => STATE !== '6'),
|
||||
[totalData]
|
||||
)}
|
||||
data={useMemo(() => data, [data])}
|
||||
rootProps={rootProps}
|
||||
searchProps={searchProps}
|
||||
refetch={() => {
|
||||
totalData?.length >= +INTERVAL_ON_FIRST_RENDER
|
||||
? setArgs(INITIAL_ARGS)
|
||||
: refetch()
|
||||
}}
|
||||
refetch={refetch}
|
||||
isLoading={isFetching}
|
||||
getRowId={(row) => String(row.ID)}
|
||||
RowComponent={VmRow}
|
||||
|
@ -13,18 +13,42 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo } from 'react'
|
||||
import { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import vmApi from 'client/features/OneApi/vm'
|
||||
import { VirtualMachineCard } from 'client/components/Cards'
|
||||
import { GuacamoleButton } from 'client/components/Buttons'
|
||||
import { VM_ACTIONS } from 'client/constants'
|
||||
|
||||
const { VNC, RDP, SSH } = VM_ACTIONS
|
||||
|
||||
const Row = memo(
|
||||
({ original, ...props }) => {
|
||||
const detail = vmApi.endpoints.getVm.useQueryState(original.ID, {
|
||||
selectFromResult: ({ data }) => data,
|
||||
const state = vmApi.endpoints.getVms.useQueryState(undefined, {
|
||||
selectFromResult: ({ data = [] }) =>
|
||||
data.find((vm) => +vm.ID === +original.ID),
|
||||
})
|
||||
|
||||
return <VirtualMachineCard vm={detail ?? original} rootProps={props} />
|
||||
const memoVm = useMemo(() => state ?? original, [state, original])
|
||||
|
||||
return (
|
||||
<VirtualMachineCard
|
||||
vm={memoVm}
|
||||
rootProps={props}
|
||||
actions={
|
||||
<>
|
||||
{[VNC, RDP, SSH].map((connectionType) => (
|
||||
<GuacamoleButton
|
||||
key={`${memoVm}-${connectionType}`}
|
||||
connectionType={connectionType}
|
||||
vm={memoVm}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
},
|
||||
(prev, next) => prev.className === next.className
|
||||
)
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { LinearProgress } from '@mui/material'
|
||||
import { Alert, LinearProgress } from '@mui/material'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import { useGetClusterQuery } from 'client/features/OneApi/cluster'
|
||||
@ -32,7 +32,7 @@ const getTabComponent = (tabName) =>
|
||||
|
||||
const ClusterTabs = memo(({ id }) => {
|
||||
const { view, getResourceView } = useViews()
|
||||
const { isLoading } = useGetClusterQuery({ id })
|
||||
const { isLoading, isError, error } = useGetClusterQuery({ id })
|
||||
|
||||
const tabsAvailable = useMemo(() => {
|
||||
const resource = RESOURCE_NAMES.CLUSTER
|
||||
@ -41,6 +41,14 @@ const ClusterTabs = memo(({ id }) => {
|
||||
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
|
||||
}, [view])
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Alert severity="error" variant="outlined">
|
||||
{error.data}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { LinearProgress } from '@mui/material'
|
||||
import { Alert, LinearProgress } from '@mui/material'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import { useGetDatastoreQuery } from 'client/features/OneApi/datastore'
|
||||
@ -32,7 +32,7 @@ const getTabComponent = (tabName) =>
|
||||
|
||||
const DatastoreTabs = memo(({ id }) => {
|
||||
const { view, getResourceView } = useViews()
|
||||
const { isLoading } = useGetDatastoreQuery({ id })
|
||||
const { isLoading, isError, error } = useGetDatastoreQuery({ id })
|
||||
|
||||
const tabsAvailable = useMemo(() => {
|
||||
const resource = RESOURCE_NAMES.DATASTORE
|
||||
@ -41,6 +41,14 @@ const DatastoreTabs = memo(({ id }) => {
|
||||
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
|
||||
}, [view])
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Alert severity="error" variant="outlined">
|
||||
{error.data}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { LinearProgress } from '@mui/material'
|
||||
import { Alert, LinearProgress } from '@mui/material'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import { useGetGroupQuery } from 'client/features/OneApi/group'
|
||||
@ -32,7 +32,7 @@ const getTabComponent = (tabName) =>
|
||||
|
||||
const GroupTabs = memo(({ id }) => {
|
||||
const { view, getResourceView } = useViews()
|
||||
const { isLoading } = useGetGroupQuery(id)
|
||||
const { isLoading, isError, error } = useGetGroupQuery(id)
|
||||
|
||||
const tabsAvailable = useMemo(() => {
|
||||
const resource = RESOURCE_NAMES.GROUP
|
||||
@ -41,6 +41,14 @@ const GroupTabs = memo(({ id }) => {
|
||||
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
|
||||
}, [view])
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Alert severity="error" variant="outlined">
|
||||
{error.data}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { LinearProgress } from '@mui/material'
|
||||
import { Alert, LinearProgress } from '@mui/material'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { useGetHostQuery } from 'client/features/OneApi/host'
|
||||
@ -32,7 +32,7 @@ const getTabComponent = (tabName) =>
|
||||
|
||||
const HostTabs = memo(({ id }) => {
|
||||
const { view, getResourceView } = useAuth()
|
||||
const { isLoading } = useGetHostQuery(id)
|
||||
const { isLoading, isError, error } = useGetHostQuery(id)
|
||||
|
||||
const tabsAvailable = useMemo(() => {
|
||||
const resource = RESOURCE_NAMES.HOST
|
||||
@ -41,6 +41,14 @@ const HostTabs = memo(({ id }) => {
|
||||
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
|
||||
}, [view])
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Alert severity="error" variant="outlined">
|
||||
{error.data}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { LinearProgress } from '@mui/material'
|
||||
import { Alert, LinearProgress } from '@mui/material'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import { useGetImageQuery } from 'client/features/OneApi/image'
|
||||
@ -32,7 +32,7 @@ const getTabComponent = (tabName) =>
|
||||
|
||||
const ImageTabs = memo(({ id }) => {
|
||||
const { view, getResourceView } = useViews()
|
||||
const { isLoading } = useGetImageQuery({ id })
|
||||
const { isLoading, isError, error } = useGetImageQuery({ id })
|
||||
|
||||
const tabsAvailable = useMemo(() => {
|
||||
const resource = RESOURCE_NAMES.IMAGE
|
||||
@ -41,6 +41,14 @@ const ImageTabs = memo(({ id }) => {
|
||||
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
|
||||
}, [view])
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Alert severity="error" variant="outlined">
|
||||
{error.data}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { LinearProgress } from '@mui/material'
|
||||
import { Alert, LinearProgress } from '@mui/material'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import { useGetMarketplaceQuery } from 'client/features/OneApi/marketplace'
|
||||
@ -32,7 +32,7 @@ const getTabComponent = (tabName) =>
|
||||
|
||||
const MarketplaceTabs = memo(({ id }) => {
|
||||
const { view, getResourceView } = useViews()
|
||||
const { isLoading } = useGetMarketplaceQuery({ id })
|
||||
const { isLoading, isError, error } = useGetMarketplaceQuery({ id })
|
||||
|
||||
const tabsAvailable = useMemo(() => {
|
||||
const resource = RESOURCE_NAMES.MARKETPLACE
|
||||
@ -41,6 +41,14 @@ const MarketplaceTabs = memo(({ id }) => {
|
||||
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
|
||||
}, [view])
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Alert severity="error" variant="outlined">
|
||||
{error.data}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { LinearProgress } from '@mui/material'
|
||||
import { Alert, LinearProgress } from '@mui/material'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import { useGetMarketplaceAppQuery } from 'client/features/OneApi/marketplaceApp'
|
||||
@ -34,7 +34,7 @@ const getTabComponent = (tabName) =>
|
||||
|
||||
const MarketplaceAppTabs = memo(({ id }) => {
|
||||
const { view, getResourceView } = useViews()
|
||||
const { isLoading } = useGetMarketplaceAppQuery(id)
|
||||
const { isLoading, isError, error } = useGetMarketplaceAppQuery(id)
|
||||
|
||||
const tabsAvailable = useMemo(() => {
|
||||
const resource = RESOURCE_NAMES.APP
|
||||
@ -43,6 +43,14 @@ const MarketplaceAppTabs = memo(({ id }) => {
|
||||
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
|
||||
}, [view])
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Alert severity="error" variant="outlined">
|
||||
{error.data}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { LinearProgress } from '@mui/material'
|
||||
import { Alert, LinearProgress } from '@mui/material'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import { useGetUserQuery } from 'client/features/OneApi/user'
|
||||
@ -32,7 +32,7 @@ const getTabComponent = (tabName) =>
|
||||
|
||||
const UserTabs = memo(({ id }) => {
|
||||
const { view, getResourceView } = useViews()
|
||||
const { isLoading } = useGetUserQuery(id)
|
||||
const { isLoading, isError, error } = useGetUserQuery(id)
|
||||
|
||||
const tabsAvailable = useMemo(() => {
|
||||
const resource = RESOURCE_NAMES.USER
|
||||
@ -41,6 +41,14 @@ const UserTabs = memo(({ id }) => {
|
||||
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
|
||||
}, [view])
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Alert severity="error" variant="outlined">
|
||||
{error.data}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { LinearProgress } from '@mui/material'
|
||||
import { Alert, LinearProgress } from '@mui/material'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import { useGetVNetworkQuery } from 'client/features/OneApi/network'
|
||||
@ -32,7 +32,7 @@ const getTabComponent = (tabName) =>
|
||||
|
||||
const VNetworkTabs = memo(({ id }) => {
|
||||
const { view, getResourceView } = useViews()
|
||||
const { isLoading } = useGetVNetworkQuery({ id })
|
||||
const { isLoading, isError, error } = useGetVNetworkQuery({ id })
|
||||
|
||||
const tabsAvailable = useMemo(() => {
|
||||
const resource = RESOURCE_NAMES.VNET
|
||||
@ -41,6 +41,14 @@ const VNetworkTabs = memo(({ id }) => {
|
||||
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
|
||||
}, [view])
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Alert severity="error" variant="outlined">
|
||||
{error.data}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { LinearProgress } from '@mui/material'
|
||||
import { Alert, LinearProgress } from '@mui/material'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import { useGetVNTemplateQuery } from 'client/features/OneApi/networkTemplate'
|
||||
@ -32,7 +32,7 @@ const getTabComponent = (tabName) =>
|
||||
|
||||
const VNetTemplateTabs = memo(({ id }) => {
|
||||
const { view, getResourceView } = useViews()
|
||||
const { isLoading } = useGetVNTemplateQuery({ id })
|
||||
const { isLoading, isError, error } = useGetVNTemplateQuery({ id })
|
||||
|
||||
const tabsAvailable = useMemo(() => {
|
||||
const resource = RESOURCE_NAMES.VN_TEMPLATE
|
||||
@ -41,6 +41,14 @@ const VNetTemplateTabs = memo(({ id }) => {
|
||||
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
|
||||
}, [view])
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Alert severity="error" variant="outlined">
|
||||
{error.data}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { LinearProgress } from '@mui/material'
|
||||
import { Alert, LinearProgress } from '@mui/material'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import { useGetVmQuery } from 'client/features/OneApi/vm'
|
||||
@ -46,7 +46,7 @@ const getTabComponent = (tabName) =>
|
||||
|
||||
const VmTabs = memo(({ id }) => {
|
||||
const { view, getResourceView } = useViews()
|
||||
const { isLoading } = useGetVmQuery(id, {
|
||||
const { isLoading, isError, error } = useGetVmQuery(id, {
|
||||
refetchOnMountOrArgChange: 10,
|
||||
})
|
||||
|
||||
@ -57,6 +57,14 @@ const VmTabs = memo(({ id }) => {
|
||||
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
|
||||
}, [view])
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Alert severity="error" variant="outlined">
|
||||
{error.data}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { LinearProgress } from '@mui/material'
|
||||
import { Alert, LinearProgress } from '@mui/material'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import { useGetTemplateQuery } from 'client/features/OneApi/vmTemplate'
|
||||
@ -34,7 +34,7 @@ const getTabComponent = (tabName) =>
|
||||
|
||||
const VmTemplateTabs = memo(({ id }) => {
|
||||
const { view, getResourceView } = useViews()
|
||||
const { isLoading } = useGetTemplateQuery({ id })
|
||||
const { isLoading, isError, error } = useGetTemplateQuery({ id })
|
||||
|
||||
const tabsAvailable = useMemo(() => {
|
||||
const resource = RESOURCE_NAMES.VM_TEMPLATE
|
||||
@ -43,6 +43,14 @@ const VmTemplateTabs = memo(({ id }) => {
|
||||
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
|
||||
}, [view])
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Alert severity="error" variant="outlined">
|
||||
{error.data}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { LinearProgress } from '@mui/material'
|
||||
import { Alert, LinearProgress } from '@mui/material'
|
||||
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import { useGetZoneQuery } from 'client/features/OneApi/zone'
|
||||
@ -32,7 +32,7 @@ const getTabComponent = (tabName) =>
|
||||
|
||||
const ZoneTabs = memo(({ id }) => {
|
||||
const { view, getResourceView } = useViews()
|
||||
const { isLoading } = useGetZoneQuery(id)
|
||||
const { isLoading, isError, error } = useGetZoneQuery(id)
|
||||
|
||||
const tabsAvailable = useMemo(() => {
|
||||
const resource = RESOURCE_NAMES.ZONE
|
||||
@ -41,6 +41,14 @@ const ZoneTabs = memo(({ id }) => {
|
||||
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
|
||||
}, [view])
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Alert severity="error" variant="outlined">
|
||||
{error.data}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return isLoading ? (
|
||||
<LinearProgress color="secondary" sx={{ width: '100%' }} />
|
||||
) : (
|
||||
|
101
src/fireedge/src/client/constants/guacamole.js
Normal file
101
src/fireedge/src/client/constants/guacamole.js
Normal file
@ -0,0 +1,101 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Client, WebSocketTunnel, Status } from 'guacamole-common-js'
|
||||
|
||||
/**
|
||||
* @typedef GuacamoleSessionThumbnail
|
||||
* @property {number} timestamp - The time that this thumbnail was generated
|
||||
* @property {HTMLCanvasElement} canvas - The thumbnail of the Guacamole client display
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef GuacamoleSessionProperties
|
||||
* @property {boolean} autoFit - Whether the display should be scaled automatically
|
||||
* @property {number} scale - The current scale.
|
||||
* If autoFit is true, the effect of setting this value is undefined
|
||||
* @property {number} minScale - The minimum scale value
|
||||
* @property {number} maxScale - The maximum scale value
|
||||
* @property {boolean} keyboardEnabled - Whether or not the client should listen to keyboard events
|
||||
* @property {number} emulateAbsoluteMouse - Whether translation of touch to mouse events should
|
||||
* emulate an absolute pointer device, or a relative pointer device
|
||||
* @property {number} scrollTop - The relative Y coordinate of the scroll offset of the display
|
||||
* @property {number} scrollLeft - The relative X coordinate of the scroll offset of the display
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef GuacamoleSessionState
|
||||
* @property {GUACAMOLE_CLIENT_STATES} connectionState - The current connection state
|
||||
* @property {Status.Code} statusCode - The status code of the current error condition
|
||||
* @property {boolean} tunnelUnstable - Whether the network connection used by the tunnel seems unstable
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef GuacamoleSession
|
||||
* @property {string} token - The token of the connection associated with this client
|
||||
* @property {string} name - The name returned associated with the connection or connection group in use
|
||||
* @property {string} title - The title which should be displayed as the page title for this client
|
||||
* @property {Client} client - The actual underlying Guacamole client
|
||||
* @property {WebSocketTunnel} tunnel - The tunnel being used by the underlying Guacamole client
|
||||
* @property {GuacamoleSessionState} clientState - The current state of the Guacamole client
|
||||
* @property {boolean} isUninitialized - When true, indicates that the session hasn't been fired yet
|
||||
* @property {boolean} isLoading - When true, indicates that the session is awaiting a response
|
||||
* @property {boolean} isConnected - When true, indicates that the last session was connected successfully
|
||||
* @property {boolean} isDisconnected - When true, indicates that the last session was disconnected
|
||||
* @property {boolean} isError - When true, indicates that the last session has an error state
|
||||
* @property {GuacamoleSessionProperties} clientProperties - The current state of the Guacamole client
|
||||
* @property {GuacamoleSessionThumbnail} thumbnail - The most recently-generated thumbnail for this connection
|
||||
* @property {number} multiTouchSupport - The number of simultaneous touch contacts supported
|
||||
*/
|
||||
|
||||
/** @enum {string} Guacamole client state strings */
|
||||
export const GUACAMOLE_STATES_STR = {
|
||||
IDLE: 'IDLE',
|
||||
CONNECTING: 'CONNECTING',
|
||||
WAITING: 'WAITING',
|
||||
CONNECTED: 'CONNECTED',
|
||||
DISCONNECTING: 'DISCONNECTING',
|
||||
DISCONNECTED: 'DISCONNECTED',
|
||||
CLIENT_ERROR: 'CLIENT_ERROR',
|
||||
TUNNEL_ERROR: 'TUNNEL_ERROR',
|
||||
}
|
||||
|
||||
/** @enum {string} Guacamole client states */
|
||||
export const GUACAMOLE_CLIENT_STATES = [
|
||||
GUACAMOLE_STATES_STR.IDLE,
|
||||
GUACAMOLE_STATES_STR.CONNECTING,
|
||||
GUACAMOLE_STATES_STR.WAITING,
|
||||
GUACAMOLE_STATES_STR.CONNECTED,
|
||||
GUACAMOLE_STATES_STR.DISCONNECTING,
|
||||
GUACAMOLE_STATES_STR.DISCONNECTED,
|
||||
]
|
||||
|
||||
/**
|
||||
* The mimetype of audio data to be sent along the Guacamole
|
||||
* connection if audio input is supported.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const AUDIO_INPUT_MIMETYPE = 'audio/L16;rate=44100,channels=2'
|
||||
|
||||
/**
|
||||
* The minimum amount of time to wait between updates to
|
||||
* the client thumbnail, in milliseconds.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export const THUMBNAIL_UPDATE_FREQUENCY = 5000
|
@ -27,7 +27,7 @@ export const BY = {
|
||||
url: 'https://opennebula.io/',
|
||||
}
|
||||
|
||||
export const _APPS = defaultApps
|
||||
export const _APPS = { ...defaultApps }
|
||||
export const APPS = Object.keys(defaultApps)
|
||||
export const APPS_IN_BETA = [_APPS.sunstone.name]
|
||||
export const APPS_WITH_SWITCHER = [_APPS.sunstone.name]
|
||||
@ -90,6 +90,7 @@ export const SOCKETS = {
|
||||
DISCONNECT: 'disconnect',
|
||||
HOOKS: 'hooks',
|
||||
PROVISION: 'provision',
|
||||
GUACAMOLE: 'guacamole',
|
||||
}
|
||||
|
||||
/** @enum {string} Names of resource */
|
||||
@ -114,23 +115,24 @@ export const RESOURCE_NAMES = {
|
||||
export * as T from 'client/constants/translates'
|
||||
export * as ACTIONS from 'client/constants/actions'
|
||||
export * as STATES from 'client/constants/states'
|
||||
export * from 'client/constants/common'
|
||||
export * from 'client/constants/quota'
|
||||
export * from 'client/constants/scheduler'
|
||||
export * from 'client/constants/userInput'
|
||||
export * from 'client/constants/flow'
|
||||
export * from 'client/constants/provision'
|
||||
export * from 'client/constants/user'
|
||||
export * from 'client/constants/group'
|
||||
export * from 'client/constants/cluster'
|
||||
export * from 'client/constants/vm'
|
||||
export * from 'client/constants/vmTemplate'
|
||||
export * from 'client/constants/network'
|
||||
export * from 'client/constants/networkTemplate'
|
||||
export * from 'client/constants/common'
|
||||
export * from 'client/constants/datastore'
|
||||
export * from 'client/constants/flow'
|
||||
export * from 'client/constants/group'
|
||||
export * from 'client/constants/guacamole'
|
||||
export * from 'client/constants/host'
|
||||
export * from 'client/constants/image'
|
||||
export * from 'client/constants/marketplace'
|
||||
export * from 'client/constants/marketplaceApp'
|
||||
export * from 'client/constants/datastore'
|
||||
export * from 'client/constants/network'
|
||||
export * from 'client/constants/networkTemplate'
|
||||
export * from 'client/constants/provision'
|
||||
export * from 'client/constants/quota'
|
||||
export * from 'client/constants/scheduler'
|
||||
export * from 'client/constants/securityGroup'
|
||||
export * from 'client/constants/user'
|
||||
export * from 'client/constants/userInput'
|
||||
export * from 'client/constants/vm'
|
||||
export * from 'client/constants/vmTemplate'
|
||||
export * from 'client/constants/zone'
|
||||
|
@ -361,6 +361,19 @@ module.exports = {
|
||||
ReadyStatusGate: 'Ready status gate',
|
||||
|
||||
/* VM schema */
|
||||
/* VM schema - remote access */
|
||||
Vnc: 'VNC',
|
||||
Ssh: 'SSH',
|
||||
Rdp: 'RDP',
|
||||
SshConnection: 'SSH connection',
|
||||
RdpConnection: 'RDP connection',
|
||||
Vmrc: 'VMRC',
|
||||
Sdl: 'SDL',
|
||||
Spice: 'SPICE',
|
||||
SendCtrlAltDel: 'Send Ctrl-Alt-Del',
|
||||
Reconnect: 'Reconnect',
|
||||
FullScreen: 'Full screen',
|
||||
Screenshot: 'Screenshot',
|
||||
/* VM schema - info */
|
||||
UserTemplate: 'User Template',
|
||||
Template: 'Template',
|
||||
@ -389,8 +402,6 @@ module.exports = {
|
||||
NIC: 'NIC',
|
||||
Alias: 'Alias',
|
||||
AsAnAlias: 'Attach as an alias',
|
||||
RdpConnection: 'RDP connection',
|
||||
SshConnection: 'SSH connection',
|
||||
External: 'External',
|
||||
ExternalConcept: 'The NIC will be attached as an external alias of the VM',
|
||||
OverrideNetworkValuesIPv4: 'Override Network Values IPv4',
|
||||
@ -568,10 +579,6 @@ module.exports = {
|
||||
Class: 'Class',
|
||||
/* VM Template schema - Input/Output - graphics */
|
||||
Graphics: 'Graphics',
|
||||
VMRC: 'VMRC',
|
||||
VNC: 'VNC',
|
||||
SDL: 'SDL',
|
||||
SPICE: 'SPICE',
|
||||
ListenOnIp: 'Listen on IP',
|
||||
ServerPort: 'Server port',
|
||||
ServerPortConcept: 'Port for the VNC/SPICE server',
|
||||
|
@ -873,13 +873,12 @@ export const VM_ACTIONS_BY_STATE = {
|
||||
[VM_ACTIONS.UNRESCHED]: [STATES.RUNNING, STATES.UNKNOWN],
|
||||
|
||||
// REMOTE
|
||||
[VM_ACTIONS.VMRC]: [],
|
||||
[VM_ACTIONS.SPICE]: [],
|
||||
[VM_ACTIONS.VNC]: [],
|
||||
[VM_ACTIONS.SSH]: [],
|
||||
[VM_ACTIONS.RDP]: [],
|
||||
[VM_ACTIONS.FILE_RDP]: [],
|
||||
[VM_ACTIONS.FILE_VIRT_VIEWER]: [],
|
||||
[VM_ACTIONS.VMRC]: [STATES.RUNNING],
|
||||
[VM_ACTIONS.VNC]: [STATES.RUNNING],
|
||||
[VM_ACTIONS.SSH]: [STATES.RUNNING],
|
||||
[VM_ACTIONS.RDP]: [STATES.RUNNING],
|
||||
[VM_ACTIONS.FILE_RDP]: [STATES.RUNNING],
|
||||
[VM_ACTIONS.FILE_VIRT_VIEWER]: [STATES.RUNNING],
|
||||
|
||||
// INFORMATION
|
||||
[VM_ACTIONS.RENAME]: [],
|
||||
|
152
src/fireedge/src/client/containers/Guacamole/index.js
Normal file
152
src/fireedge/src/client/containers/Guacamole/index.js
Normal file
@ -0,0 +1,152 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { ReactElement, useMemo, useRef, useEffect } from 'react'
|
||||
import { useParams, useHistory } from 'react-router'
|
||||
import { Box, Stack, Typography, Divider, Skeleton } from '@mui/material'
|
||||
|
||||
import { useGetVmQuery } from 'client/features/OneApi/vm'
|
||||
import {
|
||||
useGuacamoleSession,
|
||||
GuacamoleDisplay,
|
||||
GuacamoleKeyboard,
|
||||
GuacamoleMouse,
|
||||
GuacamoleClipboard,
|
||||
GuacamoleCtrlAltDelButton,
|
||||
GuacamoleReconnectButton,
|
||||
GuacamoleFullScreenButton,
|
||||
GuacamoleScreenshotButton,
|
||||
} from 'client/components/Consoles'
|
||||
import { useViews } from 'client/features/Auth'
|
||||
import { StatusCircle } from 'client/components/Status'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
import { getIps, getState } from 'client/models/VirtualMachine'
|
||||
import { timeFromMilliseconds } from 'client/models/Helper'
|
||||
import { PATH } from 'client/apps/sunstone/routes'
|
||||
import { RESOURCE_NAMES } from 'client/constants'
|
||||
|
||||
/** @returns {ReactElement} Guacamole container */
|
||||
const Guacamole = () => {
|
||||
const { id, type = '' } = useParams()
|
||||
const { push: redirectTo } = useHistory()
|
||||
const { view, [RESOURCE_NAMES.VM]: vmView } = useViews()
|
||||
|
||||
const containerRef = useRef(null)
|
||||
const headerRef = useRef(null)
|
||||
|
||||
const { data: vm, isLoading, isError } = useGetVmQuery(id)
|
||||
|
||||
const ips = getIps(vm)
|
||||
const { color: stateColor, name: stateName } = getState(vm) ?? {}
|
||||
const time = timeFromMilliseconds(+vm?.ETIME || +vm?.STIME)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const noAction = vmView?.actions?.[type] !== true
|
||||
|
||||
// token should be saved after click on console button from datatable
|
||||
if (noAction || !token || isError) {
|
||||
redirectTo(PATH.DASHBOARD)
|
||||
}
|
||||
}, [view, token])
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={containerRef}
|
||||
sx={{
|
||||
height: '100%',
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'auto 1fr',
|
||||
}}
|
||||
>
|
||||
<Stack ref={headerRef}>
|
||||
<Stack direction="row" justifyContent="space-between" gap="1em" px={2}>
|
||||
<Typography
|
||||
flexGrow={1}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap="0.5em"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Skeleton variant="circular" width={12} height={12} />
|
||||
<Skeleton variant="text" width="60%" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<StatusCircle color={stateColor} tooltip={stateName} />
|
||||
{`# ${vm?.ID} - ${vm?.NAME}`}
|
||||
</>
|
||||
)}
|
||||
</Typography>
|
||||
<Stack
|
||||
flexGrow={1}
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
divider={<Divider orientation="vertical" flexItem />}
|
||||
gap="1em"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Skeleton variant="text" width="60%" />
|
||||
) : (
|
||||
<Typography>{`Started on: ${time.toFormat('ff')}`}</Typography>
|
||||
)}
|
||||
{isLoading ? (
|
||||
<Skeleton variant="text" width="40%" />
|
||||
) : (
|
||||
!!ips?.length && (
|
||||
<Typography>
|
||||
<MultipleTags tags={ips} />
|
||||
</Typography>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
<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>
|
||||
</Stack>
|
||||
{displayElement}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default Guacamole
|
@ -112,5 +112,19 @@ export const useViews = () => {
|
||||
[view]
|
||||
)
|
||||
|
||||
return useMemo(() => ({ getResourceView, views, view }), [views, view])
|
||||
return useMemo(
|
||||
() => ({
|
||||
...Object.values(RESOURCE_NAMES).reduce(
|
||||
(listOfResourceViews, resourceName) => ({
|
||||
...listOfResourceViews,
|
||||
[resourceName]: getResourceView(resourceName),
|
||||
}),
|
||||
{}
|
||||
),
|
||||
getResourceView,
|
||||
views,
|
||||
view,
|
||||
}),
|
||||
[views, view]
|
||||
)
|
||||
}
|
||||
|
73
src/fireedge/src/client/features/Guacamole/hooks.js
Normal file
73
src/fireedge/src/client/features/Guacamole/hooks.js
Normal file
@ -0,0 +1,73 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { useCallback, useMemo } from 'react'
|
||||
import { useDispatch, useSelector, shallowEqual } from 'react-redux'
|
||||
|
||||
import { name as guacSlice, actions } from 'client/features/Guacamole/slice'
|
||||
import { GuacamoleSession } from 'client/constants'
|
||||
|
||||
const {
|
||||
addGuacamoleSession,
|
||||
removeGuacamoleSession,
|
||||
updateThumbnail,
|
||||
setConnectionState,
|
||||
setTunnelUnstable,
|
||||
setMultiTouchSupport,
|
||||
} = actions
|
||||
|
||||
// --------------------------------------------------------------
|
||||
// Guacamole Hooks
|
||||
// --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Hook to get the state of Guacamole sessions.
|
||||
*
|
||||
* @param {string} [id] - Session id to subscribe
|
||||
* @returns {object|GuacamoleSession} Return Guacamole session by id or global state
|
||||
*/
|
||||
export const useGuacamole = (id) => {
|
||||
const guac = useSelector(
|
||||
(state) => (id ? state[guacSlice][id] : state[guacSlice]),
|
||||
shallowEqual
|
||||
)
|
||||
|
||||
return useMemo(() => ({ ...guac }), [guac])
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to manage Guacamole sessions.
|
||||
*
|
||||
* @param {string} [id] - Session id to operate
|
||||
* @returns {object} Return management actions
|
||||
*/
|
||||
export const useGuacamoleApi = (id) => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const commonDispatch = useCallback(
|
||||
(action) => (data) => dispatch(action({ id, ...data })),
|
||||
[dispatch, id]
|
||||
)
|
||||
|
||||
return {
|
||||
addSession: commonDispatch(addGuacamoleSession),
|
||||
removeSession: commonDispatch(removeGuacamoleSession),
|
||||
updateThumbnail: commonDispatch(updateThumbnail),
|
||||
setConnectionState: commonDispatch(setConnectionState),
|
||||
setTunnelUnstable: commonDispatch(setTunnelUnstable),
|
||||
setMultiTouchSupport: commonDispatch(setMultiTouchSupport),
|
||||
}
|
||||
}
|
17
src/fireedge/src/client/features/Guacamole/index.js
Normal file
17
src/fireedge/src/client/features/Guacamole/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
export * from 'client/features/Guacamole/slice'
|
||||
export * from 'client/features/Guacamole/hooks'
|
126
src/fireedge/src/client/features/Guacamole/slice.js
Normal file
126
src/fireedge/src/client/features/Guacamole/slice.js
Normal file
@ -0,0 +1,126 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { createSlice } from '@reduxjs/toolkit'
|
||||
import { Status } from 'guacamole-common-js'
|
||||
|
||||
import { actions as authActions } from 'client/features/Auth/slice'
|
||||
import { GUACAMOLE_STATES_STR } from 'client/constants'
|
||||
|
||||
const {
|
||||
IDLE,
|
||||
CONNECTING,
|
||||
WAITING,
|
||||
CONNECTED,
|
||||
DISCONNECTING,
|
||||
DISCONNECTED,
|
||||
CLIENT_ERROR,
|
||||
TUNNEL_ERROR,
|
||||
} = GUACAMOLE_STATES_STR
|
||||
|
||||
const getIdentifiedFromPayload = ({ id, type } = {}) =>
|
||||
id?.includes('-') ? id : `${id}-${type}`
|
||||
|
||||
const INITIAL_SESSION = {
|
||||
thumbnail: null,
|
||||
multiTouchSupport: 0,
|
||||
clientState: {
|
||||
connectionState: IDLE,
|
||||
tunnelUnstable: false,
|
||||
statusCode: Status.Code.SUCCESS,
|
||||
},
|
||||
isUninitialized: true,
|
||||
isLoading: false,
|
||||
isConnected: false,
|
||||
isDisconnected: false,
|
||||
isError: false,
|
||||
clientProperties: {
|
||||
autoFit: true,
|
||||
scale: 1,
|
||||
minScale: 1,
|
||||
maxScale: 3,
|
||||
focused: false,
|
||||
scrollTop: 0,
|
||||
scrollLeft: 0,
|
||||
},
|
||||
}
|
||||
|
||||
const slice = createSlice({
|
||||
name: 'guacamole',
|
||||
initialState: {},
|
||||
reducers: {
|
||||
addGuacamoleSession: (state, { payload }) => {
|
||||
const id = getIdentifiedFromPayload(payload)
|
||||
state[id] = { ...INITIAL_SESSION, token: payload?.token }
|
||||
},
|
||||
removeGuacamoleSession: (state, { payload }) => {
|
||||
const id = getIdentifiedFromPayload(payload)
|
||||
const { [id]: _, ...rest } = state
|
||||
|
||||
return { ...rest }
|
||||
},
|
||||
updateGuacamoleSession: (state, { payload }) => {
|
||||
const id = getIdentifiedFromPayload(payload)
|
||||
const { [id]: session = {} } = state
|
||||
|
||||
state[id] = { ...session, ...payload?.session }
|
||||
},
|
||||
setConnectionState: (state, { payload = {} }) => {
|
||||
const { state: cState, statusCode } = payload
|
||||
const id = getIdentifiedFromPayload(payload)
|
||||
const { [id]: session = {} } = state
|
||||
|
||||
if (
|
||||
!session ||
|
||||
session?.clientState.connectionState === TUNNEL_ERROR ||
|
||||
session?.clientState.connectionState === CLIENT_ERROR
|
||||
)
|
||||
return state
|
||||
|
||||
statusCode && (session.clientState.statusCode = statusCode)
|
||||
session.clientState.connectionState = cState
|
||||
session.clientState.tunnelUnstable = false
|
||||
|
||||
session.isUninitialized = cState === IDLE
|
||||
session.isLoading = [WAITING, CONNECTING, DISCONNECTING].includes(cState)
|
||||
session.isConnected = cState === CONNECTED
|
||||
session.isDisconnected = cState === DISCONNECTED
|
||||
session.isError = [CLIENT_ERROR, TUNNEL_ERROR].includes(cState)
|
||||
},
|
||||
setTunnelUnstable: (state, { payload = {} }) => {
|
||||
const id = getIdentifiedFromPayload(payload)
|
||||
const { [id]: session = {} } = state
|
||||
|
||||
session.clientState.tunnelUnstable = payload.unstable
|
||||
},
|
||||
setMultiTouchSupport: (state, { payload = {} }) => {
|
||||
const id = getIdentifiedFromPayload(payload)
|
||||
const { [id]: session = {} } = state
|
||||
|
||||
state[id] = { ...session, multiTouchSupport: payload?.touches }
|
||||
},
|
||||
updateThumbnail: (state, { payload = {} }) => {
|
||||
const id = getIdentifiedFromPayload(payload)
|
||||
const { [id]: session = {} } = state
|
||||
|
||||
state[id] = { ...session, thumbnail: payload?.thumbnail }
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(authActions.logout, () => ({}))
|
||||
},
|
||||
})
|
||||
|
||||
export const { name, reducer, actions } = slice
|
@ -14,11 +14,16 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { Actions, Commands } from 'server/utils/constants/commands/vm'
|
||||
import {
|
||||
Actions as ExtraActions,
|
||||
Commands as ExtraCommands,
|
||||
} from 'server/routes/api/vm/routes'
|
||||
import {
|
||||
oneApi,
|
||||
ONE_RESOURCES,
|
||||
ONE_RESOURCES_POOL,
|
||||
} from 'client/features/OneApi'
|
||||
import { actions as guacamoleActions } from 'client/features/Guacamole/slice'
|
||||
import { UpdateFromSocket } from 'client/features/OneApi/socket'
|
||||
import http from 'client/utils/rest'
|
||||
import {
|
||||
@ -101,7 +106,13 @@ const vmApi = oneApi.injectEndpoints({
|
||||
index !== -1 && (draft[index] = queryVm)
|
||||
})
|
||||
)
|
||||
} catch {}
|
||||
} catch {
|
||||
dispatch(
|
||||
vmApi.util.updateQueryData('getVms', undefined, (draft) =>
|
||||
draft.filter(({ ID }) => +ID !== +id)
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
onCacheEntryAdded: UpdateFromSocket({
|
||||
updateQueryData: (updateFn) =>
|
||||
@ -109,6 +120,29 @@ const vmApi = oneApi.injectEndpoints({
|
||||
resource: VM.toLowerCase(),
|
||||
}),
|
||||
}),
|
||||
getGuacamoleSession: builder.query({
|
||||
/**
|
||||
* Returns a Guacamole session.
|
||||
*
|
||||
* @param {object} params - Request parameters
|
||||
* @param {string} params.id - Virtual machine id
|
||||
* @param {'vnc'|'ssh'|'rdp'} params.type - Connection type
|
||||
* @returns {string} The session token
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
query: (params) => {
|
||||
const name = ExtraActions.GUACAMOLE
|
||||
const command = { name, ...ExtraCommands[name] }
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
async onQueryStarted({ id, type }, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const { data: token } = await queryFulfilled
|
||||
dispatch(guacamoleActions.addGuacamoleSession({ id, type, token }))
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
getMonitoring: builder.query({
|
||||
/**
|
||||
* Returns the virtual machine monitoring records.
|
||||
@ -535,6 +569,34 @@ const vmApi = oneApi.injectEndpoints({
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, { id }) => [{ type: VM, id }],
|
||||
async onQueryStarted(
|
||||
{ id, ...permissions },
|
||||
{ dispatch, queryFulfilled }
|
||||
) {
|
||||
const patchResult = dispatch(
|
||||
vmApi.util.updateQueryData('getVm', id, (draft) => {
|
||||
Object.entries(permissions)
|
||||
.filter(([_, value]) => value !== '-1')
|
||||
.forEach(([name, value]) => {
|
||||
const ensuredName = {
|
||||
ownerUse: 'OWNER_U',
|
||||
ownerManage: 'OWNER_M',
|
||||
ownerAdmin: 'OWNER_A',
|
||||
groupUse: 'GROUP_U',
|
||||
groupManage: 'GROUP_M',
|
||||
groupAdmin: 'GROUP_A',
|
||||
otherUse: 'OTHER_U',
|
||||
otherManage: 'OTHER_M',
|
||||
otherAdmin: 'OTHER_A',
|
||||
}[name]
|
||||
|
||||
draft.PERMISSIONS[ensuredName] = value
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
queryFulfilled.catch(patchResult.undo)
|
||||
},
|
||||
}),
|
||||
changeVmOwnership: builder.mutation({
|
||||
/**
|
||||
@ -844,6 +906,8 @@ export const {
|
||||
useLazyGetVmsQuery,
|
||||
useGetVmQuery,
|
||||
useLazyGetVmQuery,
|
||||
useGetGuacamoleSessionQuery,
|
||||
useLazyGetGuacamoleSessionQuery,
|
||||
useGetMonitoringQuery,
|
||||
useLazyGetMonitoringQuery,
|
||||
useGetMonitoringPoolQuery,
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { isRejectedWithValue, Middleware, Dispatch } from '@reduxjs/toolkit'
|
||||
|
||||
import * as Auth from 'client/features/Auth/slice'
|
||||
import { name as authName, logout } from 'client/features/Auth/slice'
|
||||
import { T, ONEADMIN_GROUP_ID } from 'client/constants'
|
||||
|
||||
/**
|
||||
@ -27,7 +27,7 @@ export const unauthenticatedMiddleware =
|
||||
(next) =>
|
||||
(action) => {
|
||||
if (isRejectedWithValue(action) && action.payload.status === 401) {
|
||||
dispatch(Auth.actions.logout(T.SessionExpired))
|
||||
dispatch(logout(T.SessionExpired))
|
||||
}
|
||||
|
||||
return next(action)
|
||||
@ -41,13 +41,13 @@ export const onlyForOneadminMiddleware =
|
||||
({ dispatch, getState }) =>
|
||||
(next) =>
|
||||
(action) => {
|
||||
const groups = getState()?.[Auth.name]?.user?.GROUPS?.ID
|
||||
const groups = getState()?.[authName]?.user?.GROUPS?.ID
|
||||
|
||||
if (!Auth.actions.logout.match(action) && groups) {
|
||||
if (!logout.match(action) && !!groups?.length) {
|
||||
const ensuredGroups = [groups].flat()
|
||||
|
||||
!ensuredGroups.includes(ONEADMIN_GROUP_ID) &&
|
||||
dispatch(Auth.actions.logout(T.OnlyForOneadminGroup))
|
||||
dispatch(logout(T.OnlyForOneadminGroup))
|
||||
}
|
||||
|
||||
return next(action)
|
||||
|
56
src/fireedge/src/client/models/Guacamole.js
Normal file
56
src/fireedge/src/client/models/Guacamole.js
Normal file
@ -0,0 +1,56 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { GUACAMOLE_CLIENT_STATES } from 'client/constants'
|
||||
|
||||
const isWindow = (display) => display instanceof Window
|
||||
|
||||
/**
|
||||
* @param {number} clientState - Guacamole client state
|
||||
* @returns {string} State information from resource
|
||||
*/
|
||||
export const clientStateToString = (clientState) =>
|
||||
GUACAMOLE_CLIENT_STATES[+clientState]
|
||||
|
||||
/**
|
||||
* Returns the string of connection parameters
|
||||
* to be passed to the Guacamole client.
|
||||
*
|
||||
* @param {object} options - Connection parameters
|
||||
* @param {HTMLElement} options.display - Element where the connection will be displayed
|
||||
* @param {number} options.width - Forced width connection
|
||||
* @param {number} options.height - Forced height connection
|
||||
* @returns {string} A string of connection parameters
|
||||
*/
|
||||
export const getConnectString = (options = {}) => {
|
||||
const { token, display = window, dpi, width, height } = options
|
||||
|
||||
// Calculate optimal width/height for display
|
||||
const pixelDensity = window.devicePixelRatio || 1
|
||||
const optimalDpi = dpi || pixelDensity * 96
|
||||
|
||||
const displayWidth =
|
||||
width || (isWindow(display) ? display?.innerWidth : display?.offsetWidth)
|
||||
|
||||
const displayHeight =
|
||||
height || (isWindow(display) ? display?.innerHeight : display?.offsetHeight)
|
||||
|
||||
return [
|
||||
`token=${encodeURIComponent(token)}`,
|
||||
`width=${Math.floor(displayWidth * pixelDensity)}`,
|
||||
`height=${Math.floor(displayHeight * pixelDensity)}`,
|
||||
`dpi=${Math.floor(optimalDpi)}`,
|
||||
].join('&')
|
||||
}
|
@ -18,6 +18,7 @@ import {
|
||||
prettySecurityGroup,
|
||||
} from 'client/models/SecurityGroup'
|
||||
import { isRelative } from 'client/models/Scheduler'
|
||||
import { stringToBoolean } from 'client/models/Helper'
|
||||
|
||||
import {
|
||||
STATES,
|
||||
@ -286,3 +287,16 @@ export const isAvailableAction =
|
||||
(state) => !VM_ACTIONS_BY_STATE[action]?.includes(state)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {VM} vm - Virtual machine
|
||||
* @param {'ssh'|'rdp'} type - Connection type
|
||||
* @returns {boolean} - Returns connection type is available
|
||||
*/
|
||||
export const nicsIncludesTheConnectionType = (vm, type) => {
|
||||
const ensuredConnection = String(type).toUpperCase()
|
||||
|
||||
if (!['SSH', 'RDP'].includes(ensuredConnection)) return false
|
||||
|
||||
return getNics(vm).some((nic) => stringToBoolean(nic[ensuredConnection]))
|
||||
}
|
||||
|
@ -28,9 +28,9 @@ import {
|
||||
import { ProtectedRoute, NoAuthRoute } from 'client/components/Route'
|
||||
import { InternalLayout } from 'client/components/HOC'
|
||||
|
||||
const renderRoute = ({ Component, label, ...rest }, index) => (
|
||||
const renderRoute = ({ Component, label, disabledSidebar, ...rest }, index) => (
|
||||
<ProtectedRoute key={index} exact {...rest}>
|
||||
<InternalLayout title={label}>
|
||||
<InternalLayout title={label} disabledSidebar={disabledSidebar}>
|
||||
<Component fallback={<LinearProgress color="secondary" />} />
|
||||
</InternalLayout>
|
||||
</ProtectedRoute>
|
||||
@ -72,7 +72,7 @@ Router.propTypes = {
|
||||
PropTypes.shape({
|
||||
Component: PropTypes.object,
|
||||
icon: PropTypes.object,
|
||||
label: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
path: PropTypes.string,
|
||||
sidebar: PropTypes.bool,
|
||||
routes: PropTypes.array,
|
||||
|
@ -20,6 +20,7 @@ import { isDevelopment } from 'client/utils'
|
||||
|
||||
import * as Auth from 'client/features/Auth/slice'
|
||||
import * as General from 'client/features/General/slice'
|
||||
import * as Guacamole from 'client/features/Guacamole/slice'
|
||||
import { authApi } from 'client/features/AuthApi'
|
||||
import { oneApi } from 'client/features/OneApi'
|
||||
import { unauthenticatedMiddleware } from 'client/features/middleware'
|
||||
@ -35,6 +36,7 @@ export const createStore = ({ initState = {}, extraMiddleware = [] }) => {
|
||||
reducer: {
|
||||
[Auth.name]: Auth.reducer,
|
||||
[General.name]: General.reducer,
|
||||
[Guacamole.name]: Guacamole.reducer,
|
||||
[authApi.reducerPath]: authApi.reducer,
|
||||
[oneApi.reducerPath]: oneApi.reducer,
|
||||
},
|
||||
|
@ -90,6 +90,32 @@ export const encodeBase64 = (string, defaultValue = '') => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a link to download the file, then remove it.
|
||||
*
|
||||
* @param {File} file - File
|
||||
*/
|
||||
export const downloadFile = (file) => {
|
||||
try {
|
||||
// Create a link and set the URL using `createObjectURL`
|
||||
const link = document.createElement('a')
|
||||
link.style.display = 'none'
|
||||
link.href = URL.createObjectURL(file)
|
||||
link.download = file.name
|
||||
|
||||
// It needs to be added to the DOM so it can be clicked
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
|
||||
// To make this work on Firefox we need to wait
|
||||
// a little while before removing it
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(link.href)
|
||||
link.parentNode.removeChild(link)
|
||||
}, 0)
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a long string of units into a readable format e.g KB, MB, GB, TB, YB.
|
||||
*
|
||||
|
@ -129,7 +129,7 @@ frontApps.forEach((frontApp) => {
|
||||
app.get(`${basename}/${frontApp}`, entrypointApp)
|
||||
app.get(`${basename}/${frontApp}/*`, entrypointApp)
|
||||
})
|
||||
app.get('/*', (req, res) => res.redirect(`/${defaultAppName}/provision`))
|
||||
app.get('/*', (req, res) => res.redirect(`/${defaultAppName}/sunstone`))
|
||||
// 404 - public
|
||||
app.get('*', entrypoint404)
|
||||
|
||||
|
@ -406,14 +406,19 @@ const setZones = () => {
|
||||
* Create token server admin.
|
||||
*
|
||||
* @param {object} config - config create token serveradmin
|
||||
* @param {string} config.serverAdmin - serverAdmin username
|
||||
* @param {string} config.username - user name
|
||||
* @param {string} config.key - serverAdmin key
|
||||
* @param {string} config.iv - serverAdmin iv
|
||||
* @param {string} config.serverAdmin - serverAdmin username
|
||||
* @returns {object|undefined} data encrypted serveradmin
|
||||
*/
|
||||
const createTokenServerAdmin = ({ serverAdmin, username, key, iv }) => {
|
||||
if (serverAdmin && username && key && iv) {
|
||||
const createTokenServerAdmin = ({
|
||||
username,
|
||||
key,
|
||||
iv,
|
||||
serverAdmin = username,
|
||||
}) => {
|
||||
if (username && key && iv) {
|
||||
!(expireTime && typeof expireTime.toSeconds === 'function') && setDates()
|
||||
const expire = parseInt(expireTime.toSeconds(), 10)
|
||||
|
||||
|
@ -13,14 +13,25 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
const { randomBytes, createCipheriv } = require('crypto')
|
||||
|
||||
const { defaults, httpCodes } = require('server/utils/constants')
|
||||
const { httpResponse, executeCommand } = require('server/utils/server')
|
||||
const {
|
||||
httpResponse,
|
||||
executeCommand,
|
||||
getSunstoneAuth,
|
||||
} = require('server/utils/server')
|
||||
const { getSunstoneConfig } = require('server/utils/yml')
|
||||
const { Actions: userActions } = require('server/utils/constants/commands/user')
|
||||
const { Actions: vmActions } = require('server/utils/constants/commands/vm')
|
||||
const { createTokenServerAdmin } = require('server/routes/api/auth/utils')
|
||||
|
||||
const { defaultEmptyFunction, defaultCommandVM } = defaults
|
||||
const { USER_INFO } = userActions
|
||||
const { VM_INFO } = vmActions
|
||||
|
||||
const { ok, unauthorized, internalServerError, badRequest } = httpCodes
|
||||
const { defaultEmptyFunction, defaultCommandVM, defaultTypeCrypto } = defaults
|
||||
|
||||
const { ok, internalServerError, badRequest } = httpCodes
|
||||
const httpBadRequest = httpResponse(badRequest, '', '')
|
||||
const appConfig = getSunstoneConfig()
|
||||
const prependCommand = appConfig.sunstone_prepend || ''
|
||||
const regexpSplitLine = /\r|\n/
|
||||
@ -39,7 +50,7 @@ const saveAsTemplate = (
|
||||
params = {},
|
||||
userData = {}
|
||||
) => {
|
||||
let rtn = httpBadRequest
|
||||
let rtn = httpResponse(badRequest, '', '')
|
||||
const { id, name, persistent } = params
|
||||
if (id && name) {
|
||||
let message = ''
|
||||
@ -66,7 +77,216 @@ const saveAsTemplate = (
|
||||
next()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a session to connect a VM by id through Guacamole.
|
||||
*
|
||||
* @param {object} res - http response
|
||||
* @param {Function} next - express stepper
|
||||
* @param {object} params - params of http request
|
||||
* @param {string} params.id - VM id
|
||||
* @param {'vnc'|'ssh'|'rdp'} params.type - Type connection
|
||||
* @param {object} userData - user of http request
|
||||
* @param {Function} xmlrpc - XML-RPC function
|
||||
*/
|
||||
const generateGuacamoleSession = (
|
||||
res = {},
|
||||
next = defaultEmptyFunction,
|
||||
params = {},
|
||||
userData = {},
|
||||
xmlrpc = defaultEmptyFunction
|
||||
) => {
|
||||
const { id: userAuthId } = userData
|
||||
const { id: vmId, type } = params
|
||||
const ensuredType = `${type}`.toLowerCase()
|
||||
|
||||
if (!['vnc', 'ssh', 'rdp'].includes(ensuredType)) {
|
||||
const messageError = "Type connection isn't supported by Guacamole"
|
||||
res.locals.httpCode = httpResponse(badRequest, messageError)
|
||||
next()
|
||||
}
|
||||
|
||||
const serverAdmin = getSunstoneAuth() ?? {}
|
||||
const { token: authToken } = createTokenServerAdmin(serverAdmin) ?? {}
|
||||
|
||||
if (!authToken) {
|
||||
res.locals.httpCode = httpResponse(badRequest, '')
|
||||
next()
|
||||
}
|
||||
|
||||
const { username } = serverAdmin
|
||||
const oneClient = xmlrpc(`${username}:${username}`, authToken)
|
||||
|
||||
// get authenticated user
|
||||
oneClient({
|
||||
action: USER_INFO,
|
||||
parameters: [parseInt(userAuthId, 10), true],
|
||||
callback: (userInfoErr, { USER } = {}) => {
|
||||
if (userInfoErr || !USER) {
|
||||
res.locals.httpCode = httpResponse(badRequest, userInfoErr)
|
||||
next()
|
||||
}
|
||||
|
||||
// get VM information by id
|
||||
oneClient({
|
||||
action: VM_INFO,
|
||||
parameters: [parseInt(vmId, 10), true],
|
||||
callback: (vmInfoErr, { VM } = {}) => {
|
||||
if (vmInfoErr || !VM) {
|
||||
res.locals.httpCode = httpResponse(unauthorized, vmInfoErr)
|
||||
next()
|
||||
}
|
||||
|
||||
const settings = {
|
||||
vnc: () => getVncSettings(VM),
|
||||
ssh: () => getSshSettings(VM, USER),
|
||||
rdp: () => getRdpSettings(VM),
|
||||
}[ensuredType]?.() ?? { error: '' }
|
||||
|
||||
if (settings.error) {
|
||||
res.locals.httpCode = httpResponse(badRequest, settings.error)
|
||||
next()
|
||||
}
|
||||
|
||||
// const minutesToAdd = 1
|
||||
// const currentDate = new Date()
|
||||
// const expiration = currentDate.getTime() + minutesToAdd * 60000
|
||||
|
||||
const connection = {
|
||||
// expiration,
|
||||
connection: {
|
||||
type: ensuredType,
|
||||
settings: {
|
||||
security: 'any',
|
||||
'ignore-cert': 'true',
|
||||
'enable-drive': 'true',
|
||||
'create-drive-path': 'true',
|
||||
...settings,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const wsToken = JSON.stringify(encryptConnection(connection))
|
||||
const encodedWsToken = Buffer.from(wsToken).toString('base64')
|
||||
|
||||
res.locals.httpCode = httpResponse(ok, encodedWsToken)
|
||||
next()
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const getVncSettings = (vmInfo) => {
|
||||
const config = {}
|
||||
|
||||
if (`${vmInfo.USER_TEMPLATE?.HYPERVISOR}`.toLowerCase() === 'vcenter') {
|
||||
const esxHost = vmInfo?.MONITORING?.VCENTER_ESX_HOST
|
||||
|
||||
if (!esxHost) {
|
||||
return {
|
||||
error: `Could not determine the vCenter ESX host where
|
||||
the VM is running. Wait till the VCENTER_ESX_HOST attribute is
|
||||
retrieved once the host has been monitored`,
|
||||
}
|
||||
}
|
||||
|
||||
config.hostname = esxHost
|
||||
}
|
||||
|
||||
if (!config.hostname) {
|
||||
const lastHistory = [vmInfo.HISTORY_RECORDS?.HISTORY ?? []].flat().at(-1)
|
||||
config.hostname = lastHistory?.HOSTNAME ?? 'localhost'
|
||||
}
|
||||
|
||||
config.port = vmInfo.TEMPLATE?.GRAPHICS?.PORT ?? '5900'
|
||||
config.password = vmInfo.TEMPLATE?.GRAPHICS?.PASSWD ?? null
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
const getSshSettings = (vmInfo, authUser) => {
|
||||
const config = {}
|
||||
|
||||
const nics = [
|
||||
vmInfo.TEMPLATE?.NIC ?? [],
|
||||
vmInfo.TEMPLATE?.NIC_ALIAS ?? [],
|
||||
].flat()
|
||||
|
||||
const nicWithExternalPortRange = nics.find((nic) => !nic.EXTERNAL_PORT_RANGE)
|
||||
const { EXTERNAL_PORT_RANGE } = nicWithExternalPortRange ?? {}
|
||||
|
||||
if (EXTERNAL_PORT_RANGE) {
|
||||
const lastHistory = [vmInfo.HISTORY_RECORDS?.HISTORY ?? []].flat().at(-1)
|
||||
const lastHostname = lastHistory?.HOSTNAME
|
||||
|
||||
if (lastHostname) {
|
||||
config.hostname = lastHostname
|
||||
config.port = parseInt(EXTERNAL_PORT_RANGE.split(':')[0], 10) + 21
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.hostname) {
|
||||
const nicWithSsh = nics.find(({ SSH }) => `${SSH}`.toLowerCase() === 'yes')
|
||||
config.hostname = nicWithSsh?.EXTERNAL_IP ?? nicWithSsh?.IP
|
||||
}
|
||||
|
||||
if (!config.hostname) {
|
||||
return { error: 'Wrong configuration. Cannot find a NIC with SSH' }
|
||||
}
|
||||
|
||||
config.port ??= vmInfo.TEMPLATE?.CONTEXT?.SSH_PORT ?? '22'
|
||||
|
||||
if (vmInfo.TEMPLATE?.CONTEXT?.SSH_PUBLIC_KEY) {
|
||||
config['private-key'] = authUser?.TEMPLATE?.SSH_PRIVATE_KEY
|
||||
config.passphrase = authUser?.TEMPLATE?.SSH_PASSPHRASE
|
||||
} else {
|
||||
config.username = vmInfo.TEMPLATE?.CONTEXT?.USERNAME
|
||||
config.password = vmInfo.TEMPLATE?.CONTEXT?.PASSWORD
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
const getRdpSettings = (vmInfo) => {
|
||||
const config = {}
|
||||
|
||||
const nics = [
|
||||
vmInfo.TEMPLATE?.NIC ?? [],
|
||||
vmInfo.TEMPLATE?.NIC_ALIAS ?? [],
|
||||
].flat()
|
||||
|
||||
const nicWithRdp = nics.find(({ RDP }) => `${RDP}`.toLowerCase() === 'yes')
|
||||
config.hostname = nicWithRdp?.EXTERNAL_IP ?? nicWithRdp?.IP
|
||||
|
||||
if (!config.hostname) {
|
||||
return { error: 'Wrong configuration. Cannot find a NIC with RDP' }
|
||||
}
|
||||
|
||||
config.port = vmInfo.TEMPLATE?.CONTEXT?.RDP_PORT ?? '3389'
|
||||
config.username = vmInfo.TEMPLATE?.CONTEXT?.USERNAME
|
||||
config.password = vmInfo.TEMPLATE?.CONTEXT?.PASSWORD
|
||||
config['resize-method'] = 'display-update'
|
||||
|
||||
if (config.username && config.password) config.security = 'nla'
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
const encryptConnection = (data) => {
|
||||
const iv = randomBytes(16)
|
||||
const key = global.paths.FIREEDGE_KEY
|
||||
const cipher = createCipheriv(defaultTypeCrypto, key, iv)
|
||||
|
||||
const ensuredData = typeof data === 'string' ? data : JSON.stringify(data)
|
||||
let value = cipher.update(ensuredData, 'utf-8', 'base64')
|
||||
value += cipher.final('base64')
|
||||
|
||||
return { iv: iv.toString('base64'), value }
|
||||
}
|
||||
|
||||
const functionRoutes = {
|
||||
saveAsTemplate,
|
||||
generateGuacamoleSession,
|
||||
}
|
||||
|
||||
module.exports = functionRoutes
|
||||
|
@ -15,13 +15,20 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
const { Actions, Commands } = require('server/routes/api/vm/routes')
|
||||
const { saveAsTemplate } = require('server/routes/api/vm/functions')
|
||||
const {
|
||||
saveAsTemplate,
|
||||
generateGuacamoleSession,
|
||||
} = require('server/routes/api/vm/functions')
|
||||
|
||||
const { VM_SAVEASTEMPLATE } = Actions
|
||||
const { VM_SAVEASTEMPLATE, GUACAMOLE } = Actions
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
...Commands[VM_SAVEASTEMPLATE],
|
||||
action: saveAsTemplate,
|
||||
},
|
||||
{
|
||||
...Commands[GUACAMOLE],
|
||||
action: generateGuacamoleSession,
|
||||
},
|
||||
]
|
||||
|
@ -17,15 +17,18 @@
|
||||
const {
|
||||
httpMethod,
|
||||
from: fromData,
|
||||
} = require('server/utils/constants/defaults')
|
||||
} = require('../../../utils/constants/defaults')
|
||||
|
||||
const basepath = '/vm'
|
||||
const { POST } = httpMethod
|
||||
const { POST, GET } = httpMethod
|
||||
const { resource, postBody } = fromData
|
||||
|
||||
const VM_SAVEASTEMPLATE = 'vm.saveastemplate'
|
||||
const GUACAMOLE = 'vm.guacamole'
|
||||
|
||||
const Actions = {
|
||||
VM_SAVEASTEMPLATE,
|
||||
GUACAMOLE,
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
@ -47,5 +50,18 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
[GUACAMOLE]: {
|
||||
path: `${basepath}/:id/guacamole/:type`,
|
||||
httpMethod: GET,
|
||||
auth: true,
|
||||
params: {
|
||||
id: {
|
||||
from: resource,
|
||||
},
|
||||
type: {
|
||||
from: resource,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user