@@ -196,6 +251,7 @@ export default function TriagedRegressions({
{
field: 'created_at',
hide: true,
+ filterable: false,
valueGetter: (value) => {
return value.row.created_at
},
@@ -212,6 +268,7 @@ export default function TriagedRegressions({
{
field: 'updated_at',
hide: true,
+ filterable: false,
valueGetter: (value) => {
return value.row.updated_at
},
@@ -232,6 +289,7 @@ export default function TriagedRegressions({
},
headerName: 'Details',
flex: 2,
+ filterable: false,
renderCell: (param) => {
const triageUrl = '/component_readiness/triages/' + param.value
return (
@@ -252,7 +310,7 @@ export default function TriagedRegressions({
selectionModel={activeRow}
onSelectionModelChange={handleSetSelectionModel}
components={{ Toolbar: GridToolbar }}
- rows={triageEntries}
+ rows={filteredTriageEntries}
columns={columns}
getRowId={(row) => String(row.id)}
pageSize={entriesPerPage}
@@ -266,6 +324,12 @@ export default function TriagedRegressions({
componentsProps={{
toolbar: {
columns: columns,
+ addFilters: addFilters,
+ filterModel: filterModel,
+ setFilterModel: setFilterModel,
+ clearSearch: () => requestSearch(''),
+ doSearch: requestSearch,
+ autocompleteData: triageEntries,
},
}}
/>
diff --git a/sippy-ng/src/datagrid/GridToolbar.js b/sippy-ng/src/datagrid/GridToolbar.js
index 0f3906eebe..602a3f640d 100644
--- a/sippy-ng/src/datagrid/GridToolbar.js
+++ b/sippy-ng/src/datagrid/GridToolbar.js
@@ -47,6 +47,7 @@ export default function GridToolbar(props) {
columns={props.columns}
filterModel={props.filterModel}
setFilterModel={props.setFilterModel}
+ autocompleteData={props.autocompleteData}
/>
{props.bookmarks ? (
@@ -143,4 +144,5 @@ GridToolbar.propTypes = {
value: PropTypes.string,
downloadDataFunc: PropTypes.func,
downloadFilePrefix: PropTypes.string,
+ autocompleteData: PropTypes.array,
}
diff --git a/sippy-ng/src/datagrid/GridToolbarAutocomplete.js b/sippy-ng/src/datagrid/GridToolbarAutocomplete.js
index 04da195e58..32d26d38d9 100644
--- a/sippy-ng/src/datagrid/GridToolbarAutocomplete.js
+++ b/sippy-ng/src/datagrid/GridToolbarAutocomplete.js
@@ -43,7 +43,7 @@ export default function GridToolbarAutocomplete(props) {
// Based on https://stackoverflow.com/a/61973338/1683486
return (
{
setOpen(false)
}}
- onChange={(e, v) => v && props.onChange(v.name)}
- defaultValue={{ name: props.value }}
- isOptionEqualToValue={(option, value) => option.name === value.name}
- getOptionLabel={(option) => option.name}
+ onChange={(e, v) => {
+ if (typeof v === 'string') {
+ props.onChange(v)
+ } else if (v && v.name) {
+ props.onChange(v.name)
+ }
+ }}
+ onInputChange={(e, value) => {
+ if (e && e.type === 'change') {
+ props.onChange(value)
+ }
+ }}
+ value={props.value || ''}
+ inputValue={props.value || ''}
+ isOptionEqualToValue={(option, value) =>
+ option.name === (typeof value === 'string' ? value : value.name)
+ }
+ getOptionLabel={(option) =>
+ typeof option === 'string' ? option : option.name
+ }
options={options}
loading={loading}
renderInput={(params) => (
diff --git a/sippy-ng/src/datagrid/GridToolbarClientAutocomplete.js b/sippy-ng/src/datagrid/GridToolbarClientAutocomplete.js
new file mode 100644
index 0000000000..a75c56ee35
--- /dev/null
+++ b/sippy-ng/src/datagrid/GridToolbarClientAutocomplete.js
@@ -0,0 +1,92 @@
+import Autocomplete from '@mui/lab/Autocomplete'
+import PropTypes from 'prop-types'
+import React from 'react'
+import TextField from '@mui/material/TextField'
+
+/**
+ * Client-side autocomplete for filter fields
+ * Extracts unique values from the provided data instead of making API calls
+ */
+export default function GridToolbarClientAutocomplete(props) {
+ const [open, setOpen] = React.useState(false)
+
+ // Extract unique values from the data for this field
+ const options = React.useMemo(() => {
+ if (!props.data || !Array.isArray(props.data)) {
+ return []
+ }
+
+ const uniqueValues = new Set()
+ props.data.forEach((row) => {
+ const value = row[props.field]
+ if (value !== null && value !== undefined && value !== '') {
+ // Handle arrays (like variants)
+ if (Array.isArray(value)) {
+ value.forEach((v) => uniqueValues.add(String(v)))
+ } else {
+ uniqueValues.add(String(value))
+ }
+ }
+ })
+
+ // Convert to array and sort
+ return Array.from(uniqueValues)
+ .sort()
+ .map((v) => ({ name: v }))
+ }, [props.data, props.field])
+
+ return (
+ {
+ setOpen(true)
+ }}
+ onClose={() => {
+ setOpen(false)
+ }}
+ onChange={(e, v) => {
+ if (typeof v === 'string') {
+ props.onChange(v)
+ } else if (v && v.name) {
+ props.onChange(v.name)
+ }
+ }}
+ onInputChange={(e, value) => {
+ if (e && e.type === 'change') {
+ props.onChange(value)
+ }
+ }}
+ value={props.value || ''}
+ inputValue={props.value || ''}
+ isOptionEqualToValue={(option, value) =>
+ option.name === (typeof value === 'string' ? value : value.name)
+ }
+ getOptionLabel={(option) =>
+ typeof option === 'string' ? option : option.name
+ }
+ options={options}
+ renderInput={(params) => (
+
+ )}
+ />
+ )
+}
+
+GridToolbarClientAutocomplete.propTypes = {
+ id: PropTypes.string,
+ error: PropTypes.bool,
+ label: PropTypes.string,
+ field: PropTypes.string,
+ value: PropTypes.string,
+ onChange: PropTypes.func,
+ data: PropTypes.array.isRequired,
+}
diff --git a/sippy-ng/src/datagrid/GridToolbarFilterItem.js b/sippy-ng/src/datagrid/GridToolbarFilterItem.js
index 285e9ee88c..02b20b9a2c 100644
--- a/sippy-ng/src/datagrid/GridToolbarFilterItem.js
+++ b/sippy-ng/src/datagrid/GridToolbarFilterItem.js
@@ -14,6 +14,7 @@ import { Close } from '@mui/icons-material'
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers'
import { makeStyles } from '@mui/styles'
import GridToolbarAutocomplete from './GridToolbarAutocomplete'
+import GridToolbarClientAutocomplete from './GridToolbarClientAutocomplete'
import PropTypes from 'prop-types'
import React, { Fragment } from 'react'
@@ -120,24 +121,48 @@ export default function GridToolbarFilterItem(props) {
)
default:
if (autocomplete !== '') {
- return (
-
- props.setFilterModel({
- columnField: props.filterModel.columnField,
- not: props.filterModel.not,
- operatorValue: props.filterModel.operatorValue,
- value: value,
- })
- }
- />
- )
+ // Use client-side autocomplete if data is available, otherwise use server-side
+ if (props.autocompleteData && props.autocompleteData.length > 0) {
+ return (
+
+ props.setFilterModel({
+ columnField: props.filterModel.columnField,
+ not: props.filterModel.not,
+ operatorValue: props.filterModel.operatorValue,
+ value: value,
+ })
+ }
+ />
+ )
+ } else {
+ return (
+
+ props.setFilterModel({
+ columnField: props.filterModel.columnField,
+ not: props.filterModel.not,
+ operatorValue: props.filterModel.operatorValue,
+ value: value,
+ })
+ }
+ />
+ )
+ }
} else {
return (
@@ -281,4 +306,5 @@ GridToolbarFilterItem.propTypes = {
disabled: PropTypes.bool,
}).isRequired
),
+ autocompleteData: PropTypes.array,
}
diff --git a/sippy-ng/src/datagrid/GridToolbarFilterMenu.js b/sippy-ng/src/datagrid/GridToolbarFilterMenu.js
index 141dec848d..ba7f2f4ebe 100644
--- a/sippy-ng/src/datagrid/GridToolbarFilterMenu.js
+++ b/sippy-ng/src/datagrid/GridToolbarFilterMenu.js
@@ -234,6 +234,7 @@ export default function GridToolbarFilterMenu(props) {
destroy={() => removeFilter(index)}
filterModel={models[index]}
setFilterModel={(v) => updateModel(index, v)}
+ autocompleteData={props.autocompleteData}
/>
@@ -294,4 +295,5 @@ GridToolbarFilterMenu.propTypes = {
type: PropTypes.string,
})
),
+ autocompleteData: PropTypes.array,
}
diff --git a/sippy-ng/src/datagrid/filterUtils.js b/sippy-ng/src/datagrid/filterUtils.js
new file mode 100644
index 0000000000..e2eef63496
--- /dev/null
+++ b/sippy-ng/src/datagrid/filterUtils.js
@@ -0,0 +1,121 @@
+/**
+ * Client-side filtering utilities for DataGrid components.
+ *
+ * These utilities support Sippy's custom filter model which allows:
+ * - Multiple filters with AND/OR operators
+ * - NOT modifier for negating filters
+ * - Various filter operators (contains, equals, startsWith, endsWith, comparison operators)
+ */
+
+/**
+ * Applies a filter model to an array of rows
+ *
+ * @param {Array} rows - The data rows to filter
+ * @param {Object} filterModel - The filter model with items and linkOperator
+ * @param {Array} filterModel.items - Array of filter items
+ * @param {string} filterModel.linkOperator - 'and' or 'or' to combine filters
+ * @returns {Array} Filtered rows
+ */
+export function applyFilterModel(rows, filterModel) {
+ if (!filterModel || !filterModel.items || filterModel.items.length === 0) {
+ return rows
+ }
+
+ return rows.filter((row) => {
+ const results = filterModel.items.map((filter) =>
+ evaluateFilter(row, filter)
+ )
+
+ // Apply AND/OR logic
+ const linkOperator = filterModel.linkOperator || 'and'
+ return linkOperator === 'and'
+ ? results.every((r) => r)
+ : results.some((r) => r)
+ })
+}
+
+/**
+ * Evaluates a single filter against a row
+ *
+ * @param {Object} row - The data row
+ * @param {Object} filter - The filter to apply
+ * @param {string} filter.columnField - The field name to filter on
+ * @param {string} filter.operatorValue - The comparison operator
+ * @param {any} filter.value - The value to compare against
+ * @param {boolean} filter.not - Whether to negate the result
+ * @returns {boolean} Whether the row matches the filter
+ */
+export function evaluateFilter(row, filter) {
+ const fieldValue = row[filter.columnField]
+ let match = false
+
+ // Handle null/undefined values
+ if (fieldValue === null || fieldValue === undefined) {
+ return filter.not ? true : false
+ }
+
+ const value = String(fieldValue).toLowerCase()
+ const filterValue = String(filter.value).toLowerCase()
+
+ switch (filter.operatorValue) {
+ case 'contains':
+ match = value.includes(filterValue)
+ break
+ case 'equals':
+ match = value === filterValue
+ break
+ case 'startsWith':
+ match = value.startsWith(filterValue)
+ break
+ case 'endsWith':
+ match = value.endsWith(filterValue)
+ break
+ case 'isEmpty':
+ case 'is empty':
+ match = value === ''
+ break
+ case 'isNotEmpty':
+ case 'is not empty':
+ match = value !== ''
+ break
+ case '>':
+ match = parseFloat(fieldValue) > parseFloat(filter.value)
+ break
+ case '>=':
+ match = parseFloat(fieldValue) >= parseFloat(filter.value)
+ break
+ case '<':
+ match = parseFloat(fieldValue) < parseFloat(filter.value)
+ break
+ case '<=':
+ match = parseFloat(fieldValue) <= parseFloat(filter.value)
+ break
+ case '!=':
+ case 'not equals':
+ match = value !== filterValue
+ break
+ default:
+ // Default to contains for unknown operators
+ match = value.includes(filterValue)
+ }
+
+ return filter.not ? !match : match
+}
+
+/**
+ * React hook for managing filtered data with a filter model
+ *
+ * @param {Array} data - The data to filter
+ * @param {Object} filterModel - The filter model
+ * @returns {Array} Filtered data
+ *
+ * @example
+ * const filteredRows = useFilteredData(rows, filterModel)
+ */
+export function useFilteredData(data, filterModel) {
+ const React = require('react')
+ return React.useMemo(
+ () => applyFilterModel(data, filterModel),
+ [data, filterModel]
+ )
+}
diff --git a/sippy-ng/src/datagrid/filterUtils.md b/sippy-ng/src/datagrid/filterUtils.md
new file mode 100644
index 0000000000..a7f2577568
--- /dev/null
+++ b/sippy-ng/src/datagrid/filterUtils.md
@@ -0,0 +1,166 @@
+# Client-Side Filtering Utilities
+
+This module provides client-side filtering utilities for DataGrid components that work with Sippy's custom filter model.
+
+## Why This Exists
+
+MUI DataGrid's built-in client-side filtering only supports single filters. Sippy's filter system is more advanced, supporting:
+- **Multiple filters** with AND/OR operators
+- **NOT modifier** for negating filters
+- **Various filter operators** (contains, equals, startsWith, endsWith, comparison operators)
+
+For components that work with pre-loaded data (like modals), we need to manually filter the data client-side.
+
+## Usage
+
+### Basic Example
+
+```javascript
+import { applyFilterModel } from '../datagrid/filterUtils'
+import React from 'react'
+
+function MyComponent({ data, filterModel }) {
+ // Filter the data client-side
+ const filteredData = React.useMemo(
+ () => applyFilterModel(data, filterModel),
+ [data, filterModel]
+ )
+
+ return