Skip to content

Commit a3318ee

Browse files
committed
[grid] UI Sessions capability fields to display as additional columns
Signed-off-by: Viet Nguyen Duc <nguyenducviet4496@gmail.com>
1 parent 65ffd18 commit a3318ee

File tree

3 files changed

+252
-9
lines changed

3 files changed

+252
-9
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* Licensed to the Software Freedom Conservancy (SFC) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The SFC licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import React, { useState, useEffect } from 'react'
21+
import {
22+
Box,
23+
Button,
24+
Checkbox,
25+
Dialog,
26+
DialogActions,
27+
DialogContent,
28+
DialogTitle,
29+
FormControlLabel,
30+
FormGroup,
31+
IconButton,
32+
Tooltip,
33+
Typography
34+
} from '@mui/material'
35+
import { ViewColumn as ViewColumnIcon } from '@mui/icons-material'
36+
37+
interface ColumnSelectorProps {
38+
sessions: any[]
39+
selectedColumns: string[]
40+
onColumnSelectionChange: (columns: string[]) => void
41+
}
42+
43+
const ColumnSelector: React.FC<ColumnSelectorProps> = ({
44+
sessions,
45+
selectedColumns,
46+
onColumnSelectionChange
47+
}) => {
48+
const [open, setOpen] = useState(false)
49+
const [availableColumns, setAvailableColumns] = useState<string[]>([])
50+
const [localSelectedColumns, setLocalSelectedColumns] = useState<string[]>(selectedColumns)
51+
52+
useEffect(() => {
53+
setLocalSelectedColumns(selectedColumns)
54+
}, [selectedColumns])
55+
56+
useEffect(() => {
57+
let allKeys = new Set<string>()
58+
try {
59+
const savedKeys = localStorage.getItem('selenium-grid-all-capability-keys')
60+
if (savedKeys) {
61+
const parsedKeys = JSON.parse(savedKeys)
62+
parsedKeys.forEach((key: string) => allKeys.add(key))
63+
}
64+
} catch (e) {
65+
console.error('Error loading saved capability keys:', e)
66+
}
67+
68+
sessions.forEach(session => {
69+
try {
70+
const capabilities = JSON.parse(session.capabilities)
71+
Object.keys(capabilities).forEach(key => {
72+
if (
73+
typeof capabilities[key] !== 'object' &&
74+
!key.startsWith('goog:') &&
75+
!key.startsWith('moz:') &&
76+
key !== 'alwaysMatch' &&
77+
key !== 'firstMatch'
78+
) {
79+
allKeys.add(key)
80+
}
81+
})
82+
} catch (e) {
83+
console.error('Error parsing capabilities:', e)
84+
}
85+
})
86+
87+
const keysArray = Array.from(allKeys).sort()
88+
localStorage.setItem('selenium-grid-all-capability-keys', JSON.stringify(keysArray))
89+
90+
setAvailableColumns(keysArray)
91+
}, [sessions])
92+
93+
const handleToggle = (column: string) => {
94+
setLocalSelectedColumns(prev => {
95+
if (prev.includes(column)) {
96+
return prev.filter(col => col !== column)
97+
} else {
98+
return [...prev, column]
99+
}
100+
})
101+
}
102+
103+
const handleClose = () => {
104+
setOpen(false)
105+
}
106+
107+
const handleSave = () => {
108+
onColumnSelectionChange(localSelectedColumns)
109+
setOpen(false)
110+
}
111+
112+
const handleSelectAll = (checked: boolean) => {
113+
if (checked) {
114+
setLocalSelectedColumns([...availableColumns])
115+
} else {
116+
setLocalSelectedColumns([])
117+
}
118+
}
119+
120+
return (
121+
<Box>
122+
<Tooltip title="Select columns to display" arrow placement="top">
123+
<IconButton
124+
aria-label="select columns"
125+
onClick={() => setOpen(true)}
126+
>
127+
<ViewColumnIcon />
128+
</IconButton>
129+
</Tooltip>
130+
131+
<Dialog
132+
open={open}
133+
onClose={handleClose}
134+
maxWidth="sm"
135+
fullWidth
136+
>
137+
<DialogTitle>
138+
Select Columns to Display
139+
</DialogTitle>
140+
<DialogContent dividers>
141+
<Typography variant="body2" gutterBottom>
142+
Select capability fields to display as additional columns:
143+
</Typography>
144+
<FormGroup>
145+
<FormControlLabel
146+
control={
147+
<Checkbox
148+
checked={localSelectedColumns.length === availableColumns.length && availableColumns.length > 0}
149+
indeterminate={localSelectedColumns.length > 0 && localSelectedColumns.length < availableColumns.length}
150+
onChange={(e) => handleSelectAll(e.target.checked)}
151+
/>
152+
}
153+
label={<Typography fontWeight="bold">Select All / Unselect All</Typography>}
154+
/>
155+
{availableColumns.map(column => (
156+
<FormControlLabel
157+
key={column}
158+
control={
159+
<Checkbox
160+
checked={localSelectedColumns.includes(column)}
161+
onChange={() => handleToggle(column)}
162+
/>
163+
}
164+
label={column}
165+
/>
166+
))}
167+
</FormGroup>
168+
</DialogContent>
169+
<DialogActions>
170+
<Button onClick={handleClose}>Cancel</Button>
171+
<Button onClick={handleSave} variant="contained" color="primary">
172+
Apply
173+
</Button>
174+
</DialogActions>
175+
</Dialog>
176+
</Box>
177+
)
178+
}
179+
180+
export default ColumnSelector

javascript/grid-ui/src/components/RunningSessions/RunningSessions.tsx

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { Size } from '../../models/size'
5151
import LiveView from '../LiveView/LiveView'
5252
import SessionData, { createSessionData } from '../../models/session-data'
5353
import { useNavigate } from 'react-router-dom'
54+
import ColumnSelector from './ColumnSelector'
5455

5556
function descendingComparator<T> (a: T, b: T, orderBy: keyof T): number {
5657
if (orderBy === 'sessionDurationMillis') {
@@ -94,7 +95,7 @@ interface HeadCell {
9495
numeric: boolean
9596
}
9697

97-
const headCells: HeadCell[] = [
98+
const fixedHeadCells: HeadCell[] = [
9899
{ id: 'id', numeric: false, label: 'Session' },
99100
{ id: 'capabilities', numeric: false, label: 'Capabilities' },
100101
{ id: 'startTime', numeric: false, label: 'Start time' },
@@ -107,10 +108,11 @@ interface EnhancedTableProps {
107108
property: keyof SessionData) => void
108109
order: Order
109110
orderBy: string
111+
headCells: HeadCell[]
110112
}
111113

112114
function EnhancedTableHead (props: EnhancedTableProps): JSX.Element {
113-
const { order, orderBy, onRequestSort } = props
115+
const { order, orderBy, onRequestSort, headCells } = props
114116
const createSortHandler = (property: keyof SessionData) => (event: React.MouseEvent<unknown>) => {
115117
onRequestSort(event, property)
116118
}
@@ -181,6 +183,16 @@ function RunningSessions (props) {
181183
const [rowsPerPage, setRowsPerPage] = useState(10)
182184
const [searchFilter, setSearchFilter] = useState('')
183185
const [searchBarHelpOpen, setSearchBarHelpOpen] = useState(false)
186+
const [selectedColumns, setSelectedColumns] = useState<string[]>(() => {
187+
try {
188+
const savedColumns = localStorage.getItem('selenium-grid-selected-columns')
189+
return savedColumns ? JSON.parse(savedColumns) : []
190+
} catch (e) {
191+
console.error('Error loading saved columns:', e)
192+
return []
193+
}
194+
})
195+
const [headCells, setHeadCells] = useState<HeadCell[]>(fixedHeadCells)
184196
const liveViewRef = useRef(null)
185197
const navigate = useNavigate()
186198

@@ -264,8 +276,27 @@ function RunningSessions (props) {
264276

265277
const { sessions, origin, sessionId } = props
266278

279+
const getCapabilityValue = (capabilitiesStr: string, key: string): string => {
280+
try {
281+
const capabilities = JSON.parse(capabilitiesStr as string)
282+
const value = capabilities[key]
283+
284+
if (value === undefined || value === null) {
285+
return ''
286+
}
287+
288+
if (typeof value === 'object') {
289+
return JSON.stringify(value)
290+
}
291+
292+
return String(value)
293+
} catch (e) {
294+
return ''
295+
}
296+
}
297+
267298
const rows = sessions.map((session) => {
268-
return createSessionData(
299+
const sessionData = createSessionData(
269300
session.id,
270301
session.capabilities,
271302
session.startTime,
@@ -276,6 +307,12 @@ function RunningSessions (props) {
276307
session.slot,
277308
origin
278309
)
310+
311+
selectedColumns.forEach(column => {
312+
sessionData[column] = getCapabilityValue(session.capabilities, column)
313+
})
314+
315+
return sessionData
279316
})
280317
const emptyRows = rowsPerPage - Math.min(rowsPerPage, rows.length - page * rowsPerPage)
281318

@@ -291,19 +328,39 @@ function RunningSessions (props) {
291328
setRowLiveViewOpen(s)
292329
}
293330
}, [sessionId, sessions])
331+
332+
useEffect(() => {
333+
const dynamicHeadCells = selectedColumns.map(column => ({
334+
id: column,
335+
numeric: false,
336+
label: column
337+
}))
338+
339+
setHeadCells([...fixedHeadCells, ...dynamicHeadCells])
340+
}, [selectedColumns])
294341

295342
return (
296343
<Box width='100%'>
297344
{rows.length > 0 && (
298345
<div>
299346
<Paper sx={{ width: '100%', marginBottom: 2 }}>
300347
<EnhancedTableToolbar title='Running'>
301-
<RunningSessionsSearchBar
302-
searchFilter={searchFilter}
303-
handleSearch={setSearchFilter}
304-
searchBarHelpOpen={searchBarHelpOpen}
305-
setSearchBarHelpOpen={setSearchBarHelpOpen}
306-
/>
348+
<Box display="flex" alignItems="center">
349+
<ColumnSelector
350+
sessions={sessions}
351+
selectedColumns={selectedColumns}
352+
onColumnSelectionChange={(columns) => {
353+
setSelectedColumns(columns)
354+
localStorage.setItem('selenium-grid-selected-columns', JSON.stringify(columns))
355+
}}
356+
/>
357+
<RunningSessionsSearchBar
358+
searchFilter={searchFilter}
359+
handleSearch={setSearchFilter}
360+
searchBarHelpOpen={searchBarHelpOpen}
361+
setSearchBarHelpOpen={setSearchBarHelpOpen}
362+
/>
363+
</Box>
307364
</EnhancedTableToolbar>
308365
<TableContainer>
309366
<Table
@@ -316,6 +373,7 @@ function RunningSessions (props) {
316373
order={order}
317374
orderBy={orderBy}
318375
onRequestSort={handleRequestSort}
376+
headCells={headCells}
319377
/>
320378
<TableBody>
321379
{stableSort(rows, getComparator(order, orderBy))
@@ -494,6 +552,10 @@ function RunningSessions (props) {
494552
<TableCell align='left'>
495553
{row.nodeUri}
496554
</TableCell>
555+
{/* Add dynamic columns */}
556+
{selectedColumns.map(column => (
557+
<TableCell key={column} align='left'>{row[column]}</TableCell>
558+
))}
497559
</TableRow>
498560
)
499561
})}

javascript/grid-ui/src/models/session-data.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ interface SessionData {
3333
slot: any
3434
vnc: string
3535
name: string
36+
[key: string]: any
3637
}
3738

3839
export function createSessionData (

0 commit comments

Comments
 (0)