-
Notifications
You must be signed in to change notification settings - Fork 16
feat: DH-18281 Input Filters UI Component #1165
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6176e29
0f60a66
d4e2ddd
cdb4955
57b8da4
a1f0606
17f4a9a
5913b94
7ee9331
6da2252
91c9439
9206b94
a07d601
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
from __future__ import annotations | ||
from typing import Any, Callable | ||
|
||
from .types import FilterChangeEventCallable | ||
from ..types import ColumnName | ||
from .basic import component_element | ||
from ..elements import Element | ||
from .._internal.utils import create_props | ||
from deephaven.table import Table | ||
|
||
|
||
def input_filters( | ||
table: Table, | ||
on_change: FilterChangeEventCallable | None = None, | ||
on_filters: Callable[[list[str]], None] | None = None, | ||
column_names: list[ColumnName] | None = None, | ||
key: str | None = None, | ||
) -> Element: | ||
""" | ||
This will call on_input_filters_changes when the filters change on the client. | ||
|
||
Args: | ||
on_change: Called with list of all FilterChangeEvents when the input filters change. | ||
on_filters: Called with list of applicable filter strings when the input filters change. | ||
columns: The list of columns to filter on. | ||
key: A unique identifier used by React to render elements in a list. | ||
|
||
Returns: | ||
The rendered button component. | ||
""" | ||
|
||
# _, props = create_props(locals()) | ||
children, props = create_props(locals()) | ||
|
||
return component_element("InputFilters", *children, **props) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
from __future__ import annotations | ||
from typing import Callable | ||
from . import use_state, use_memo | ||
|
||
from deephaven.table import Table | ||
|
||
|
||
def use_input_filters(table: Table) -> tuple[Table, Callable[[list[str]], None]]: | ||
""" | ||
Hook to add input filters to a table. | ||
|
||
Args: | ||
table: The table to add input filters to. | ||
|
||
Returns: | ||
A tuple containing the filtered table and a function to set the input filters. | ||
""" | ||
filters, set_filters = use_state([]) | ||
filtered_table = use_memo(lambda: table.where(filters), [filters]) | ||
return filtered_table, set_filters |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { createContext, useContext } from 'react'; | ||
|
||
/** | ||
* Context that holds the ID of the dashboard that we are currently in. | ||
*/ | ||
export const DashboardContext = createContext<string>(''); | ||
|
||
/** | ||
* Gets the panel ID from the nearest panel context. | ||
* @returns The panel ID | ||
*/ | ||
export function useDashboardId(): string { | ||
return useContext(DashboardContext); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import React, { useEffect, useState, useCallback, useMemo } from 'react'; | ||
import { useSelector } from 'react-redux'; | ||
import { RootState } from '@deephaven/redux'; | ||
import { | ||
getInputFiltersForDashboard, | ||
InputFilterEvent, | ||
} from '@deephaven/dashboard-core-plugins'; | ||
import { FilterChangeEvent } from '@deephaven/dashboard-core-plugins/dist/FilterPlugin'; | ||
import { useLayoutManager } from '@deephaven/dashboard'; | ||
import type { dh as DhType } from '@deephaven/jsapi-types'; | ||
import { IrisGridUtils } from '@deephaven/iris-grid'; | ||
import { nanoid } from 'nanoid'; // TODO get rid of this | ||
import { useDashboardId } from '../DashboardContext'; | ||
|
||
export interface InputFiltersProps { | ||
onChange?: (event: FilterChangeEvent[]) => void; | ||
onFilters?: (filters: string[]) => void; | ||
table: DhType.WidgetExportedObject; | ||
columnNames?: string[]; | ||
} | ||
|
||
// TODO from UITable, make common | ||
function useThrowError(): [ | ||
throwError: (error: unknown) => void, | ||
clearError: () => void, | ||
] { | ||
const [error, setError] = useState<unknown>(null); | ||
const clearError = useCallback(() => { | ||
setError(null); | ||
}, []); | ||
if (error != null) { | ||
// Re-throw the error so that the error boundary can catch it | ||
if (typeof error === 'string') { | ||
throw new Error(error); | ||
} | ||
throw error; | ||
} | ||
|
||
return [setError, clearError]; | ||
} | ||
|
||
function useTableColumns( | ||
exportedTable: DhType.WidgetExportedObject | ||
): DhType.Column[] | undefined { | ||
const [columns, setColumns] = useState<DhType.Column[]>(); | ||
const [throwError, clearError] = useThrowError(); | ||
|
||
// Just load the object on mount | ||
useEffect(() => { | ||
let isCancelled = false; | ||
async function loadColumns() { | ||
try { | ||
const reexportedTable = await exportedTable.reexport(); | ||
const table = (await reexportedTable.fetch()) as DhType.Table; | ||
setColumns(table.columns); | ||
if (!isCancelled) { | ||
clearError(); | ||
setColumns(table.columns); | ||
} | ||
} catch (e) { | ||
if (!isCancelled) { | ||
// Errors thrown from an async useEffect are not caught | ||
// by the component's error boundary | ||
throwError(e); | ||
} | ||
} | ||
} | ||
loadColumns(); | ||
return () => { | ||
isCancelled = true; | ||
}; | ||
}, [exportedTable, clearError, throwError]); | ||
|
||
return columns; | ||
} | ||
|
||
export function InputFilters(props: InputFiltersProps): JSX.Element { | ||
const { onChange, onFilters, table: exportedTable, columnNames } = props; | ||
const dashboardId = useDashboardId(); | ||
const { eventHub } = useLayoutManager(); | ||
const inputFilters = useSelector((state: RootState) => | ||
getInputFiltersForDashboard(state, dashboardId) | ||
); | ||
|
||
const tableColumns = useTableColumns(exportedTable); | ||
const columnsString = JSON.stringify(columnNames); // TODO workaround for changing columnNames reference | ||
const columns = useMemo( | ||
() => | ||
columnNames | ||
? tableColumns?.filter(column => columnNames.includes(column.name)) | ||
: tableColumns, | ||
[tableColumns, columnsString] | ||
Check warning on line 92 in plugins/ui/src/js/src/elements/InputFilters.tsx
|
||
); | ||
|
||
useEffect(() => { | ||
const id = nanoid(); // TODO use widget id | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the widgetId is used in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, the issue wasn't getting widget id. Originally, I was trying to get a ref to the |
||
eventHub.emit(InputFilterEvent.COLUMNS_CHANGED, id, columns); | ||
return () => { | ||
eventHub.emit(InputFilterEvent.COLUMNS_CHANGED, id, []); | ||
}; | ||
}, [columns, eventHub]); | ||
|
||
// If onChange is provided, call it with all of the input filters | ||
useEffect(() => { | ||
if (onChange) { | ||
onChange(inputFilters); | ||
} | ||
}, [inputFilters, onChange]); | ||
|
||
// If onFilters is provided, call it with the filters for the columns | ||
useEffect(() => { | ||
if (onFilters && columns != null) { | ||
const inputFiltersForColumns = IrisGridUtils.getInputFiltersForColumns( | ||
columns, | ||
// They may have picked a column, but not actually entered a value yet. In that case, don't need to update. | ||
inputFilters.filter( | ||
({ value, excludePanelIds }) => | ||
value != null && | ||
(excludePanelIds == null || | ||
(dashboardId != null && !excludePanelIds.includes(dashboardId))) | ||
) | ||
); | ||
const filters = inputFiltersForColumns.map( | ||
filter => `${filter.name}=\`${filter.value}\`` | ||
); // TODO use some util to do this? | ||
onFilters(filters); | ||
} | ||
}, [inputFilters, onFilters, columns, dashboardId]); | ||
|
||
return <div>{JSON.stringify(inputFilters)}</div>; | ||
} | ||
|
||
export default InputFilters; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
columnNames
shouldn't be changing, that's something I'll need to look into.