Skip to content

Commit 8df92cb

Browse files
committed
[grid] UI Sessions capability fields to display as additional columns
Signed-off-by: Viet Nguyen Duc <[email protected]>
1 parent 65ffd18 commit 8df92cb

File tree

3 files changed

+250
-9
lines changed

3 files changed

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