Skip to content

Commit

Permalink
[grid][ui] Add search field for running sessions (#11197)
Browse files Browse the repository at this point in the history
* [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 <diemol@users.noreply.github.com>
  • Loading branch information
mhnaeem and diemol committed Nov 1, 2022
1 parent 472eebb commit 837dfe7
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 6 deletions.
30 changes: 27 additions & 3 deletions javascript/grid-ui/src/components/EnhancedTableToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,19 @@
import React from 'react'
import Toolbar from '@mui/material/Toolbar'
import Typography from '@mui/material/Typography'
import Box from '@mui/material/Box'

interface EnhancedTableToolbarProps {
title: string
children?: JSX.Element
}

function EnhancedTableToolbar (props: EnhancedTableToolbarProps) {
const {
title,
children
} = props

function EnhancedTableToolbar (props) {
const { title } = props
return (
<Toolbar sx={{ paddingLeft: 2, paddingRight: 1 }}>
<Typography
Expand All @@ -30,7 +40,21 @@ function EnhancedTableToolbar (props) {
id='tableTitle'
component='div'
>
{title}
<Box
component='span'
display='flex'
alignItems='center'
>
<Box
component='span'
display='flex'
justifyContent='center'
flex={1}
>
{title}
</Box>
{children}
</Box>
</Typography>
</Toolbar>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import EnhancedTableToolbar from '../EnhancedTableToolbar'
import prettyMilliseconds from 'pretty-ms'
import BrowserLogo from '../common/BrowserLogo'
import OsLogo from '../common/OsLogo'
import RunningSessionsSearchBar from './RunningSessionsSearchBar'
import { Size } from '../../models/size'
import LiveView from '../LiveView/LiveView'
import SessionData, { createSessionData } from '../../models/session-data'
Expand Down Expand Up @@ -174,6 +175,8 @@ function RunningSessions (props) {
const [page, setPage] = useState(0)
const [dense, setDense] = useState(false)
const [rowsPerPage, setRowsPerPage] = useState(5)
const [searchFilter, setSearchFilter] = useState('')
const [searchBarHelpOpen, setSearchBarHelpOpen] = useState(false)

const handleRequestSort = (event: React.MouseEvent<unknown>,
property: keyof SessionData) => {
Expand Down Expand Up @@ -268,7 +271,14 @@ function RunningSessions (props) {
{rows.length > 0 && (
<div>
<Paper sx={{ width: '100%', marginBottom: 2 }}>
<EnhancedTableToolbar title='Running' />
<EnhancedTableToolbar title='Running'>
<RunningSessionsSearchBar
searchFilter={searchFilter}
handleSearch={setSearchFilter}
searchBarHelpOpen={searchBarHelpOpen}
setSearchBarHelpOpen={setSearchBarHelpOpen}
/>
</EnhancedTableToolbar>
<TableContainer>
<Table
sx={{ minWidth: '750px' }}
Expand All @@ -283,6 +293,26 @@ function RunningSessions (props) {
/>
<TableBody>
{stableSort(rows, getComparator(order, orderBy))
.filter((session) => {
if (searchFilter === '') {
// don't filter anything on empty search field
return true
}

if (!searchFilter.includes('=')) {
// filter on the entire session if users don't use `=` symbol
return JSON.stringify(session)
.toLowerCase()
.includes(searchFilter.toLowerCase())
}

const [filterField, filterItem] = searchFilter.split('=')
if (filterField.startsWith('capabilities,')) {
const capabilityID = filterField.split(',')[1]
return (JSON.parse(session.capabilities as string) as object)[capabilityID] === filterItem
}
return session[filterField] === filterItem
})
.slice(page * rowsPerPage,
page * rowsPerPage + rowsPerPage)
.map((row, index) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* Licensed to the Software Freedom Conservancy (SFC) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The SFC licenses this file
* to you 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 React from 'react'
import InfoIcon from '@mui/icons-material/Info'
import Typography from '@mui/material/Typography'
import Table from '@mui/material/Table'
import TableCell from '@mui/material/TableCell'
import TableContainer from '@mui/material/TableContainer'
import TableHead from '@mui/material/TableHead'
import TableRow from '@mui/material/TableRow'
import TableBody from '@mui/material/TableBody'
import OutlinedInput from '@mui/material/OutlinedInput'
import IconButton from '@mui/material/IconButton'
import DialogTitle from '@mui/material/DialogTitle'
import Dialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions'
import DialogContent from '@mui/material/DialogContent'
import Button from '@mui/material/Button'
import Box from '@mui/material/Box'

interface RunningSessionsSearchBarProps {
searchFilter: string
handleSearch: (value: string) => void
searchBarHelpOpen: boolean
setSearchBarHelpOpen: (value: boolean) => void
}

function RunningSessionsSearchBar ({
searchFilter,
handleSearch,
searchBarHelpOpen,
setSearchBarHelpOpen
}: RunningSessionsSearchBarProps): JSX.Element {
return (
<Box
component='span'
display='flex'
justifyContent='flex-end'
>
<OutlinedInput
id='search-query-tab-running'
autoFocus
margin='dense'
type='text'
value={searchFilter}
placeholder='search sessions...'
onChange={(e) => {
handleSearch(e.target.value)
}}
/>
<IconButton
sx={{ padding: '1px' }}
onClick={() => setSearchBarHelpOpen(true)}
size='large'
>
<InfoIcon />
</IconButton>
<SearchBarHelpDialog isDialogOpen={searchBarHelpOpen} onClose={() => setSearchBarHelpOpen(false)} />
</Box>
)
}

interface SearchBarHelpDialogProps {
isDialogOpen: boolean
onClose: (e) => void
}

function SearchBarHelpDialog ({
isDialogOpen,
onClose
}: SearchBarHelpDialogProps): JSX.Element {
return (
<Dialog
onClose={onClose}
aria-labelledby='search-bar-help-dialog'
open={isDialogOpen}
fullWidth
maxWidth='sm'
>
<DialogTitle id='search-bar-help-dialog'>
<Typography
gutterBottom component='span'
sx={{ paddingX: '10px' }}
>
<Box
fontWeight='fontWeightBold'
mr={1}
display='inline'
>
Search Bar Help Dialog
</Box>
</Typography>
</DialogTitle>
<DialogContent
dividers
sx={{ height: '500px' }}
>
<p>
The search field will do a lazy search and look for all the sessions with a matching string
however if you want to do more complex searches you can use some of the queries below.
</p>
<TableContainer>
<Table sx={{ minWidth: 300 }} aria-label='search bar help table' size='small'>
<TableHead>
<TableRow>
<TableCell><Box fontWeight='bold'>Property to Search</Box></TableCell>
<TableCell><Box fontWeight='bold'>Sample Query</Box></TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>Session IDs</TableCell>
<TableCell><pre>id=aee43d32ks10e85d359029719c20b146</pre></TableCell>
</TableRow>
<TableRow>
<TableCell>Browser Name</TableCell>
<TableCell><pre>browserName=chrome</pre></TableCell>
</TableRow>
<TableRow>
<TableCell>Capability</TableCell>
<TableCell><pre>capabilities,platformName=windows</pre></TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<p>The basic syntax for searching is <strong><i>key=value</i></strong> or <strong><i>capabilities,key=value</i></strong>.
All properties under <strong><i>SessionData</i></strong> are available for search and most capabilities are also searchable
</p>
</DialogContent>
<DialogActions>
<Button
onClick={onClose}
color='primary'
variant='contained'
>
Close
</Button>
</DialogActions>
</Dialog>
)
}

export default RunningSessionsSearchBar
69 changes: 67 additions & 2 deletions javascript/grid-ui/src/tests/components/RunningSessions.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,34 @@ const sessionsInfo: SessionInfo[] = [
stereotype: '{"browserName": "chrome"}',
lastStarted: '18/02/2021 13:12:05'
}
},
{
id: 'yhVTTv2iHuqMB3chxkfDBLqlzeyORnvf',
capabilities: '{ "browserName": "edge", "browserVersion": "96.0.1054.72", "platformName": "windows" }',
startTime: '18/02/2021 13:13:05',
uri: 'http://192.168.3.7:4444',
nodeId: 'h9x799f4-4397-4fbb-9344-1d5a3074695e',
nodeUri: 'http://192.168.1.3:5555',
sessionDurationMillis: '123456',
slot: {
id: '5070c2eb-8094-4692-8911-14c533619f7d',
stereotype: '{"browserName": "edge"}',
lastStarted: '18/02/2021 13:13:05'
}
},
{
id: 'p1s201AORfsFN11r1JB1Ycd9ygyRdCin',
capabilities: '{ "browserName": "firefox", "browserVersion": "103.0", "platformName": "windows", "se:random_cap": "test_func" }',
startTime: '18/02/2021 13:15:05',
uri: 'http://192.168.4.7:4444',
nodeId: 'h9x799f4-4397-4fbb-9344-1d5a3074695e',
nodeUri: 'http://192.168.1.3:5555',
sessionDurationMillis: '123456',
slot: {
id: 'ae48d687-610b-472d-9e0c-3ebc28ad7211',
stereotype: '{"browserName": "firefox"}',
lastStarted: '18/02/2021 13:15:05'
}
}
]

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

it('renders detailed session information', async () => {
render(<RunningSessions sessions={sessions} origin={origin}/>)
render(<RunningSessions sessions={sessions} origin={origin} />)
const session = sessions[0]
const sessionRow = screen.getByText(session.id).closest('tr')
const user = userEvent.setup()
await user.click(within(sessionRow as HTMLElement).getByRole('button'))
await user.click(within(sessionRow as HTMLElement).getByTestId('InfoIcon'))
const dialogPane = screen.getByText('Capabilities:').closest('div')
expect(dialogPane).toHaveTextContent('Capabilities:' + session.capabilities)
})

it('search field works as expected for normal fields', async () => {
const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)
const user = userEvent.setup()
await user.type(getByPlaceholderText('search sessions...'), 'browserName=edge')
expect(queryByText(sessions[0].id)).not.toBeInTheDocument()
expect(getByText(sessions[1].id)).toBeInTheDocument()
expect(queryByText(sessions[2].id)).not.toBeInTheDocument()
})

it('search field works as expected for capabilities', async () => {
const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)
const user = userEvent.setup()
await user.type(getByPlaceholderText('search sessions...'), 'capabilities,se:random_cap=test_func')
expect(queryByText(sessions[0].id)).not.toBeInTheDocument()
expect(queryByText(sessions[1].id)).not.toBeInTheDocument()
expect(getByText(sessions[2].id)).toBeInTheDocument()
})

it('search field works for multiple results', async () => {
const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)
const user = userEvent.setup()
await user.type(getByPlaceholderText('search sessions...'), 'nodeId=h9x799f4-4397-4fbb-9344-1d5a3074695e')
expect(queryByText(sessions[0].id)).not.toBeInTheDocument()
expect(getByText(sessions[1].id)).toBeInTheDocument()
expect(getByText(sessions[2].id)).toBeInTheDocument()
})

it('search field works for lazy search', async () => {
const { getByPlaceholderText, getByText, queryByText } = render(<RunningSessions sessions={sessions} origin={origin} />)
const user = userEvent.setup()
await user.type(getByPlaceholderText('search sessions...'), 'browserName')
expect(getByPlaceholderText('search sessions...')).toHaveValue('browserName')
expect(queryByText(sessions[0].id)).toBeInTheDocument()
expect(getByText(sessions[1].id)).toBeInTheDocument()
expect(getByText(sessions[2].id)).toBeInTheDocument()
})

0 comments on commit 837dfe7

Please sign in to comment.