Skip to content

Commit 837dfe7

Browse files
mhnaeemdiemol
andauthored
[grid][ui] Add search field for running sessions (#11197)
* [grid][ui] Added search field for running sessions Changes Include: 1) Modified the EnhancedTableToolbar to accept children elements as props, also fixed its styling using Box elements 2) Added a new React component called RunningSessionsSearchBar, this component contains three things: search input field, info/help icon, help dialog 3) Added search filters during the render step for the rows, results are filter right after they are sorted for rendering - the ordering should still work 4) Updated the tests for RunningSessions to check for some of the use cases using search field Fixes #10632 * [grid][ui] Allow users to do a lazy search The search field will allow users to enter any string and it will filter out any sessions containing that string. Users can still use the complex searches by using the = symbol and the search syntax. Fixes #10632 Co-authored-by: Diego Molina <[email protected]>
1 parent 472eebb commit 837dfe7

File tree

4 files changed

+285
-6
lines changed

4 files changed

+285
-6
lines changed

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

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,19 @@
1818
import React from 'react'
1919
import Toolbar from '@mui/material/Toolbar'
2020
import Typography from '@mui/material/Typography'
21+
import Box from '@mui/material/Box'
22+
23+
interface EnhancedTableToolbarProps {
24+
title: string
25+
children?: JSX.Element
26+
}
27+
28+
function EnhancedTableToolbar (props: EnhancedTableToolbarProps) {
29+
const {
30+
title,
31+
children
32+
} = props
2133

22-
function EnhancedTableToolbar (props) {
23-
const { title } = props
2434
return (
2535
<Toolbar sx={{ paddingLeft: 2, paddingRight: 1 }}>
2636
<Typography
@@ -30,7 +40,21 @@ function EnhancedTableToolbar (props) {
3040
id='tableTitle'
3141
component='div'
3242
>
33-
{title}
43+
<Box
44+
component='span'
45+
display='flex'
46+
alignItems='center'
47+
>
48+
<Box
49+
component='span'
50+
display='flex'
51+
justifyContent='center'
52+
flex={1}
53+
>
54+
{title}
55+
</Box>
56+
{children}
57+
</Box>
3458
</Typography>
3559
</Toolbar>
3660
)

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import EnhancedTableToolbar from '../EnhancedTableToolbar'
4646
import prettyMilliseconds from 'pretty-ms'
4747
import BrowserLogo from '../common/BrowserLogo'
4848
import OsLogo from '../common/OsLogo'
49+
import RunningSessionsSearchBar from './RunningSessionsSearchBar'
4950
import { Size } from '../../models/size'
5051
import LiveView from '../LiveView/LiveView'
5152
import SessionData, { createSessionData } from '../../models/session-data'
@@ -174,6 +175,8 @@ function RunningSessions (props) {
174175
const [page, setPage] = useState(0)
175176
const [dense, setDense] = useState(false)
176177
const [rowsPerPage, setRowsPerPage] = useState(5)
178+
const [searchFilter, setSearchFilter] = useState('')
179+
const [searchBarHelpOpen, setSearchBarHelpOpen] = useState(false)
177180

178181
const handleRequestSort = (event: React.MouseEvent<unknown>,
179182
property: keyof SessionData) => {
@@ -268,7 +271,14 @@ function RunningSessions (props) {
268271
{rows.length > 0 && (
269272
<div>
270273
<Paper sx={{ width: '100%', marginBottom: 2 }}>
271-
<EnhancedTableToolbar title='Running' />
274+
<EnhancedTableToolbar title='Running'>
275+
<RunningSessionsSearchBar
276+
searchFilter={searchFilter}
277+
handleSearch={setSearchFilter}
278+
searchBarHelpOpen={searchBarHelpOpen}
279+
setSearchBarHelpOpen={setSearchBarHelpOpen}
280+
/>
281+
</EnhancedTableToolbar>
272282
<TableContainer>
273283
<Table
274284
sx={{ minWidth: '750px' }}
@@ -283,6 +293,26 @@ function RunningSessions (props) {
283293
/>
284294
<TableBody>
285295
{stableSort(rows, getComparator(order, orderBy))
296+
.filter((session) => {
297+
if (searchFilter === '') {
298+
// don't filter anything on empty search field
299+
return true
300+
}
301+
302+
if (!searchFilter.includes('=')) {
303+
// filter on the entire session if users don't use `=` symbol
304+
return JSON.stringify(session)
305+
.toLowerCase()
306+
.includes(searchFilter.toLowerCase())
307+
}
308+
309+
const [filterField, filterItem] = searchFilter.split('=')
310+
if (filterField.startsWith('capabilities,')) {
311+
const capabilityID = filterField.split(',')[1]
312+
return (JSON.parse(session.capabilities as string) as object)[capabilityID] === filterItem
313+
}
314+
return session[filterField] === filterItem
315+
})
286316
.slice(page * rowsPerPage,
287317
page * rowsPerPage + rowsPerPage)
288318
.map((row, index) => {
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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 from 'react'
21+
import InfoIcon from '@mui/icons-material/Info'
22+
import Typography from '@mui/material/Typography'
23+
import Table from '@mui/material/Table'
24+
import TableCell from '@mui/material/TableCell'
25+
import TableContainer from '@mui/material/TableContainer'
26+
import TableHead from '@mui/material/TableHead'
27+
import TableRow from '@mui/material/TableRow'
28+
import TableBody from '@mui/material/TableBody'
29+
import OutlinedInput from '@mui/material/OutlinedInput'
30+
import IconButton from '@mui/material/IconButton'
31+
import DialogTitle from '@mui/material/DialogTitle'
32+
import Dialog from '@mui/material/Dialog'
33+
import DialogActions from '@mui/material/DialogActions'
34+
import DialogContent from '@mui/material/DialogContent'
35+
import Button from '@mui/material/Button'
36+
import Box from '@mui/material/Box'
37+
38+
interface RunningSessionsSearchBarProps {
39+
searchFilter: string
40+
handleSearch: (value: string) => void
41+
searchBarHelpOpen: boolean
42+
setSearchBarHelpOpen: (value: boolean) => void
43+
}
44+
45+
function RunningSessionsSearchBar ({
46+
searchFilter,
47+
handleSearch,
48+
searchBarHelpOpen,
49+
setSearchBarHelpOpen
50+
}: RunningSessionsSearchBarProps): JSX.Element {
51+
return (
52+
<Box
53+
component='span'
54+
display='flex'
55+
justifyContent='flex-end'
56+
>
57+
<OutlinedInput
58+
id='search-query-tab-running'
59+
autoFocus
60+
margin='dense'
61+
type='text'
62+
value={searchFilter}
63+
placeholder='search sessions...'
64+
onChange={(e) => {
65+
handleSearch(e.target.value)
66+
}}
67+
/>
68+
<IconButton
69+
sx={{ padding: '1px' }}
70+
onClick={() => setSearchBarHelpOpen(true)}
71+
size='large'
72+
>
73+
<InfoIcon />
74+
</IconButton>
75+
<SearchBarHelpDialog isDialogOpen={searchBarHelpOpen} onClose={() => setSearchBarHelpOpen(false)} />
76+
</Box>
77+
)
78+
}
79+
80+
interface SearchBarHelpDialogProps {
81+
isDialogOpen: boolean
82+
onClose: (e) => void
83+
}
84+
85+
function SearchBarHelpDialog ({
86+
isDialogOpen,
87+
onClose
88+
}: SearchBarHelpDialogProps): JSX.Element {
89+
return (
90+
<Dialog
91+
onClose={onClose}
92+
aria-labelledby='search-bar-help-dialog'
93+
open={isDialogOpen}
94+
fullWidth
95+
maxWidth='sm'
96+
>
97+
<DialogTitle id='search-bar-help-dialog'>
98+
<Typography
99+
gutterBottom component='span'
100+
sx={{ paddingX: '10px' }}
101+
>
102+
<Box
103+
fontWeight='fontWeightBold'
104+
mr={1}
105+
display='inline'
106+
>
107+
Search Bar Help Dialog
108+
</Box>
109+
</Typography>
110+
</DialogTitle>
111+
<DialogContent
112+
dividers
113+
sx={{ height: '500px' }}
114+
>
115+
<p>
116+
The search field will do a lazy search and look for all the sessions with a matching string
117+
however if you want to do more complex searches you can use some of the queries below.
118+
</p>
119+
<TableContainer>
120+
<Table sx={{ minWidth: 300 }} aria-label='search bar help table' size='small'>
121+
<TableHead>
122+
<TableRow>
123+
<TableCell><Box fontWeight='bold'>Property to Search</Box></TableCell>
124+
<TableCell><Box fontWeight='bold'>Sample Query</Box></TableCell>
125+
</TableRow>
126+
</TableHead>
127+
<TableBody>
128+
<TableRow>
129+
<TableCell>Session IDs</TableCell>
130+
<TableCell><pre>id=aee43d32ks10e85d359029719c20b146</pre></TableCell>
131+
</TableRow>
132+
<TableRow>
133+
<TableCell>Browser Name</TableCell>
134+
<TableCell><pre>browserName=chrome</pre></TableCell>
135+
</TableRow>
136+
<TableRow>
137+
<TableCell>Capability</TableCell>
138+
<TableCell><pre>capabilities,platformName=windows</pre></TableCell>
139+
</TableRow>
140+
</TableBody>
141+
</Table>
142+
</TableContainer>
143+
<p>The basic syntax for searching is <strong><i>key=value</i></strong> or <strong><i>capabilities,key=value</i></strong>.
144+
All properties under <strong><i>SessionData</i></strong> are available for search and most capabilities are also searchable
145+
</p>
146+
</DialogContent>
147+
<DialogActions>
148+
<Button
149+
onClick={onClose}
150+
color='primary'
151+
variant='contained'
152+
>
153+
Close
154+
</Button>
155+
</DialogActions>
156+
</Dialog>
157+
)
158+
}
159+
160+
export default RunningSessionsSearchBar

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

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,34 @@ const sessionsInfo: SessionInfo[] = [
3838
stereotype: '{"browserName": "chrome"}',
3939
lastStarted: '18/02/2021 13:12:05'
4040
}
41+
},
42+
{
43+
id: 'yhVTTv2iHuqMB3chxkfDBLqlzeyORnvf',
44+
capabilities: '{ "browserName": "edge", "browserVersion": "96.0.1054.72", "platformName": "windows" }',
45+
startTime: '18/02/2021 13:13:05',
46+
uri: 'http://192.168.3.7:4444',
47+
nodeId: 'h9x799f4-4397-4fbb-9344-1d5a3074695e',
48+
nodeUri: 'http://192.168.1.3:5555',
49+
sessionDurationMillis: '123456',
50+
slot: {
51+
id: '5070c2eb-8094-4692-8911-14c533619f7d',
52+
stereotype: '{"browserName": "edge"}',
53+
lastStarted: '18/02/2021 13:13:05'
54+
}
55+
},
56+
{
57+
id: 'p1s201AORfsFN11r1JB1Ycd9ygyRdCin',
58+
capabilities: '{ "browserName": "firefox", "browserVersion": "103.0", "platformName": "windows", "se:random_cap": "test_func" }',
59+
startTime: '18/02/2021 13:15:05',
60+
uri: 'http://192.168.4.7:4444',
61+
nodeId: 'h9x799f4-4397-4fbb-9344-1d5a3074695e',
62+
nodeUri: 'http://192.168.1.3:5555',
63+
sessionDurationMillis: '123456',
64+
slot: {
65+
id: 'ae48d687-610b-472d-9e0c-3ebc28ad7211',
66+
stereotype: '{"browserName": "firefox"}',
67+
lastStarted: '18/02/2021 13:15:05'
68+
}
4169
}
4270
]
4371

@@ -64,11 +92,48 @@ it('renders basic session information', () => {
6492
})
6593

6694
it('renders detailed session information', async () => {
67-
render(<RunningSessions sessions={sessions} origin={origin}/>)
95+
render(<RunningSessions sessions={sessions} origin={origin} />)
6896
const session = sessions[0]
6997
const sessionRow = screen.getByText(session.id).closest('tr')
7098
const user = userEvent.setup()
71-
await user.click(within(sessionRow as HTMLElement).getByRole('button'))
99+
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
72100
const dialogPane = screen.getByText('Capabilities:').closest('div')
73101
expect(dialogPane).toHaveTextContent('Capabilities:' + session.capabilities)
74102
})
103+
104+
it('search field works as expected for normal fields', async () => {
105+
const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)
106+
const user = userEvent.setup()
107+
await user.type(getByPlaceholderText('search sessions...'), 'browserName=edge')
108+
expect(queryByText(sessions[0].id)).not.toBeInTheDocument()
109+
expect(getByText(sessions[1].id)).toBeInTheDocument()
110+
expect(queryByText(sessions[2].id)).not.toBeInTheDocument()
111+
})
112+
113+
it('search field works as expected for capabilities', async () => {
114+
const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)
115+
const user = userEvent.setup()
116+
await user.type(getByPlaceholderText('search sessions...'), 'capabilities,se:random_cap=test_func')
117+
expect(queryByText(sessions[0].id)).not.toBeInTheDocument()
118+
expect(queryByText(sessions[1].id)).not.toBeInTheDocument()
119+
expect(getByText(sessions[2].id)).toBeInTheDocument()
120+
})
121+
122+
it('search field works for multiple results', async () => {
123+
const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)
124+
const user = userEvent.setup()
125+
await user.type(getByPlaceholderText('search sessions...'), 'nodeId=h9x799f4-4397-4fbb-9344-1d5a3074695e')
126+
expect(queryByText(sessions[0].id)).not.toBeInTheDocument()
127+
expect(getByText(sessions[1].id)).toBeInTheDocument()
128+
expect(getByText(sessions[2].id)).toBeInTheDocument()
129+
})
130+
131+
it('search field works for lazy search', async () => {
132+
const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)
133+
const user = userEvent.setup()
134+
await user.type(getByPlaceholderText('search sessions...'), 'browserName')
135+
expect(getByPlaceholderText('search sessions...')).toHaveValue('browserName')
136+
expect(queryByText(sessions[0].id)).toBeInTheDocument()
137+
expect(getByText(sessions[1].id)).toBeInTheDocument()
138+
expect(getByText(sessions[2].id)).toBeInTheDocument()
139+
})

0 commit comments

Comments
 (0)