1515// specific language governing permissions and limitations
1616// under the License.
1717
18- import { Box , Card , CardContent , Grid , Typography } from '@mui/material'
19- import React from 'react'
18+ import { Box , Card , CardContent , Dialog , DialogActions , DialogContent , DialogTitle , Grid , IconButton , Typography , Button , keyframes , styled } from '@mui/material'
19+ import React , { useState , useRef } from 'react'
20+ import { Videocam as VideocamIcon } from '@mui/icons-material'
2021import NodeDetailsDialog from './NodeDetailsDialog'
2122import NodeLoad from './NodeLoad'
2223import Stereotypes from './Stereotypes'
2324import OsLogo from '../common/OsLogo'
25+ import LiveView from '../LiveView/LiveView'
26+
27+ const pulse = keyframes `
28+ 0% {
29+ box-shadow: 0 0 0 0 rgba(25, 118, 210, 0.7);
30+ transform: scale(1);
31+ }
32+ 50% {
33+ box-shadow: 0 0 0 5px rgba(25, 118, 210, 0);
34+ transform: scale(1.05);
35+ }
36+ 100% {
37+ box-shadow: 0 0 0 0 rgba(25, 118, 210, 0);
38+ transform: scale(1);
39+ }
40+ `
41+
42+ const LiveIconButton = styled ( IconButton ) ( ( { theme } ) => ( {
43+ marginLeft : theme . spacing ( 1 ) ,
44+ position : 'relative' ,
45+ '&::after' : {
46+ content : '""' ,
47+ position : 'absolute' ,
48+ width : '100%' ,
49+ height : '100%' ,
50+ borderRadius : '50%' ,
51+ animation : `${ pulse } 2s infinite` ,
52+ zIndex : 0
53+ }
54+ } ) )
55+
56+ function getVncUrl ( session , origin ) {
57+ try {
58+ const parsed = JSON . parse ( session . capabilities )
59+ let vnc = parsed [ 'se:vnc' ] ?? ''
60+ if ( vnc . length > 0 ) {
61+ try {
62+ const url = new URL ( origin )
63+ const vncUrl = new URL ( vnc )
64+ url . pathname = vncUrl . pathname
65+ url . protocol = url . protocol === 'https:' ? 'wss:' : 'ws:'
66+ return url . href
67+ } catch ( error ) {
68+ console . log ( error )
69+ return ''
70+ }
71+ }
72+ return ''
73+ } catch ( e ) {
74+ return ''
75+ }
76+ }
2477
2578function Node ( props ) {
26- const { node } = props
79+ const { node, sessions = [ ] , origin } = props
80+ const [ liveViewSessionId , setLiveViewSessionId ] = useState ( '' )
81+ const liveViewRef = useRef < { disconnect : ( ) => void } > ( null )
82+
83+ const vncSession = sessions . find ( session => {
84+ try {
85+ const capabilities = JSON . parse ( session . capabilities )
86+ return capabilities [ 'se:vnc' ] !== undefined && capabilities [ 'se:vnc' ] !== ''
87+ } catch ( e ) {
88+ return false
89+ }
90+ } )
91+
92+ const handleLiveViewIconClick = ( ) => {
93+ if ( vncSession ) {
94+ setLiveViewSessionId ( vncSession . id )
95+ }
96+ }
97+
98+ const handleDialogClose = ( ) => {
99+ if ( liveViewRef . current ) {
100+ liveViewRef . current . disconnect ( )
101+ }
102+ setLiveViewSessionId ( '' )
103+ }
27104 const getCardStyle = ( status : string ) => ( {
28105 height : '100%' ,
29106 flexGrow : 1 ,
@@ -32,6 +109,7 @@ function Node (props) {
32109 } )
33110
34111 return (
112+ < >
35113 < Card sx = { getCardStyle ( node . status ) } >
36114 < CardContent sx = { { pl : 2 , pr : 1 } } >
37115 < Grid
@@ -62,14 +140,54 @@ function Node (props) {
62140 </ Typography >
63141 </ Grid >
64142 < Grid item xs = { 12 } >
143+ < Box display = "flex" alignItems = "center" >
65144 < Stereotypes stereotypes = { node . slotStereotypes } />
145+ { vncSession && (
146+ < LiveIconButton onClick = { handleLiveViewIconClick } size = 'medium' color = "primary" >
147+ < VideocamIcon data-testid = "VideocamIcon" />
148+ </ LiveIconButton >
149+ ) }
150+ </ Box >
66151 </ Grid >
67152 < Grid item xs = { 12 } >
68153 < NodeLoad node = { node } />
69154 </ Grid >
70155 </ Grid >
71156 </ CardContent >
72157 </ Card >
158+ { vncSession && liveViewSessionId && (
159+ < Dialog
160+ onClose = { handleDialogClose }
161+ aria-labelledby = 'live-view-dialog'
162+ open = { liveViewSessionId === vncSession . id }
163+ fullWidth
164+ maxWidth = 'xl'
165+ fullScreen
166+ >
167+ < DialogTitle id = 'live-view-dialog' >
168+ < Typography gutterBottom component = 'span' sx = { { paddingX : '10px' } } >
169+ < Box fontWeight = 'fontWeightBold' mr = { 1 } display = 'inline' >
170+ Node Session Live View
171+ </ Box >
172+ { node . uri }
173+ </ Typography >
174+ </ DialogTitle >
175+ < DialogContent dividers sx = { { height : '600px' } } >
176+ < LiveView
177+ ref = { liveViewRef as any }
178+ url = { getVncUrl ( vncSession , origin ) }
179+ scaleViewport
180+ onClose = { handleDialogClose }
181+ />
182+ </ DialogContent >
183+ < DialogActions >
184+ < Button onClick = { handleDialogClose } color = 'primary' variant = 'contained' >
185+ Close
186+ </ Button >
187+ </ DialogActions >
188+ </ Dialog >
189+ ) }
190+ </ >
73191 )
74192}
75193
0 commit comments