Skip to content

Commit

Permalink
#659: editing cells with predefined values
Browse files Browse the repository at this point in the history
  • Loading branch information
aschonfeld committed Jan 23, 2023
1 parent 01816df commit bd12ab8
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 11 deletions.
3 changes: 3 additions & 0 deletions dtale/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,8 @@ def show(data=None, data_loader=None, name=None, context_vars=None, **options):
:type github_fork: bool, optional
:param hide_drop_rows: If true, this will hide the "Drop Rows" buton from users
:type hide_drop_rows: bool, optional
:param hide_shutdown: If true, this will hide the "Shutdown" buton from users
:type hide_shutdown: bool, optional
:Example:
Expand Down Expand Up @@ -757,6 +759,7 @@ def show(data=None, data_loader=None, name=None, context_vars=None, **options):
is_proxy=JUPYTER_SERVER_PROXY,
app_root=final_app_root,
hide_shutdown=final_options.get("hide_shutdown"),
column_edit_options=final_options.get("column_edit_options"),
)
instance.started_with_open_browser = final_options["open_browser"]
is_active = not running_with_flask_debug() and is_up(app_url)
Expand Down
9 changes: 9 additions & 0 deletions dtale/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ def build_show_options(options=None):
background_mode=None,
range_highlights=None,
vertical_headers=False,
hide_shutdown=False,
column_edit_options=None,
)
config_options = {}
config = get_config()
Expand Down Expand Up @@ -260,6 +262,13 @@ def build_show_options(options=None):
config_options["vertical_headers"] = get_config_val(
config, defaults, "vertical_headers", "getboolean"
)
config_options["column_edit_options"] = get_config_val(
config, defaults, "column_edit_options"
)
if config_options["column_edit_options"]:
config_options["column_edit_options"] = json.loads(
config_options["column_edit_options"]
)

return dict_merge(defaults, config_options, options)

Expand Down
3 changes: 3 additions & 0 deletions dtale/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,7 @@ def startup(
is_proxy=None,
vertical_headers=False,
hide_shutdown=False,
column_edit_options=None,
):
"""
Loads and stores data globally
Expand Down Expand Up @@ -1105,6 +1106,8 @@ def startup(
base_settings["nanDisplay"] = nan_display
if hide_shutdown is not None:
base_settings["hide_shutdown"] = hide_shutdown
if column_edit_options is not None:
base_settings["column_edit_options"] = column_edit_options
global_state.set_settings(data_id, base_settings)
if optimize_dataframe:
data = optimize_df(data)
Expand Down
21 changes: 21 additions & 0 deletions frontend/static/__tests__/dtale/GridCell-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,25 @@ describe('GridCell', () => {
]);
expect(loadFilterDataSpy).toHaveBeenLastCalledWith('1', 'baz');
});

it('renders select for column w/ custom options when edited', async () => {
const settings = { column_edit_options: { baz: ['foo', 'bar', 'bizzle'] } };
await buildMock(
{ columnIndex: 3, data: { 0: { baz: { raw: 'a', view: 'a' } } } },
{ editedCell: '3|1', settings },
true,
);
expect(container.getElementsByClassName('Select')).toHaveLength(1);
const select = container.getElementsByClassName('Select')[0] as HTMLElement;
await act(async () => {
await selectEvent.openMenu(select);
});
expect([...select.getElementsByClassName('Select__option')].map((o) => o.textContent)).toEqual([
'nan',
'foo',
'bar',
'bizzle',
]);
expect(loadFilterDataSpy).not.toHaveBeenCalled();
});
});
25 changes: 23 additions & 2 deletions frontend/static/__tests__/dtale/edited/EditedCellInfo-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import EditedCellInfo from '../../../dtale/edited/EditedCellInfo';
import * as serverState from '../../../dtale/serverStateManagement';
import { ActionType } from '../../../redux/actions/AppActions';
import * as chartActions from '../../../redux/actions/charts';
import { PopupType } from '../../../redux/state/AppState';
import { InstanceSettings, PopupType } from '../../../redux/state/AppState';
import * as ColumnFilterRepository from '../../../repository/ColumnFilterRepository';
import { mockColumnDef } from '../../mocks/MockColumnDef';
import reduxUtils from '../../redux-test-utils';
Expand Down Expand Up @@ -39,7 +39,7 @@ describe('DataViewerInfo tests', () => {

afterAll(jest.restoreAllMocks);

const buildInfo = async (editedCell?: string): Promise<void> => {
const buildInfo = async (editedCell?: string, settings?: Partial<InstanceSettings>): Promise<void> => {
const columns = [
{ name: 'a', dtype: 'string', index: 1, visible: true },
mockColumnDef({
Expand Down Expand Up @@ -69,6 +69,9 @@ describe('DataViewerInfo tests', () => {
if (editedCell) {
store.dispatch({ type: ActionType.EDIT_CELL, editedCell });
}
if (settings) {
store.dispatch({ type: ActionType.UPDATE_SETTINGS, settings });
}
result = await act(
async () =>
render(
Expand Down Expand Up @@ -170,4 +173,22 @@ describe('DataViewerInfo tests', () => {
]);
expect(loadFilterDataSpy).toHaveBeenLastCalledWith('1', 'baz');
});

it('renders select for column w/ custom options when edited', async () => {
const loadFilterDataSpy = jest.spyOn(ColumnFilterRepository, 'loadFilterData');
loadFilterDataSpy.mockResolvedValue({ success: true, hasMissing: false, uniques: ['a', 'b', 'c'] });
await buildInfo('2|1', { column_edit_options: { baz: ['foo', 'bar', 'bizzle'] } });
expect(result.getElementsByClassName('Select')).toHaveLength(1);
const select = result.getElementsByClassName('Select')[0] as HTMLElement;
await act(async () => {
await selectEvent.openMenu(select);
});
expect([...select.getElementsByClassName('Select__option')].map((o) => o.textContent)).toEqual([
'nan',
'foo',
'bar',
'bizzle',
]);
expect(loadFilterDataSpy).not.toHaveBeenCalled();
});
});
8 changes: 7 additions & 1 deletion frontend/static/dtale/GridCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,13 @@ const GridCell: React.FC<GridCellProps & WithTranslation> = ({
}
return (
<div className={className} style={{ ...style, ...valueStyle }} {...divProps}>
{colCfg?.resized ? <div className="resized">{value}</div> : value}
{colCfg?.resized ? (
<div className="resized" {...{ cell_idx: cellIdx }}>
{value}
</div>
) : (
value
)}
</div>
);
};
Expand Down
25 changes: 23 additions & 2 deletions frontend/static/dtale/GridCellEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const GridCellEditor: React.FC<GridCellEditorProps> = ({

const [value, setValue] = React.useState(props.value ?? '');
const [options, setOptions] = React.useState<Array<BaseOption<string>>>([]);
const [customOptions, setCustomOptions] = React.useState<Array<BaseOption<string>>>([]);
const input = React.useRef<HTMLInputElement>(null);

const escapeHandler = (event: KeyboardEvent): void => {
Expand All @@ -58,7 +59,10 @@ export const GridCellEditor: React.FC<GridCellEditorProps> = ({
}, []);

React.useEffect(() => {
if (gu.ColumnType.CATEGORY === gu.findColType(colCfg.dtype)) {
const settingsOptions = (settings.column_edit_options ?? {})[colCfg.name] ?? [];
if (settingsOptions.length) {
setCustomOptions(settingsOptions.map((so) => ({ value: so })));
} else if (gu.ColumnType.CATEGORY === gu.findColType(colCfg.dtype)) {
(async () => {
const filterData = await ColumnFilterRepository.loadFilterData(dataId, colCfg.name);
setOptions(filterData?.uniques?.map((v) => ({ value: `${v}` })) ?? []);
Expand All @@ -80,7 +84,24 @@ export const GridCellEditor: React.FC<GridCellEditorProps> = ({
});
};

if (gu.ColumnType.BOOL === gu.findColType(colCfg.dtype)) {
if (customOptions.length) {
return (
<div
onKeyDown={onKeyDown}
tabIndex={-1}
style={{ background: 'lightblue', width: 'inherit', height: 'inherit', padding: '0' }}
className="editor-select"
>
<DtaleSelect
value={{ value }}
options={[{ value: 'nan' }, ...customOptions]}
onChange={(state: BaseOption<string> | Array<BaseOption<any>> | undefined) =>
setValue((state as BaseOption<string>)?.value ?? '')
}
/>
</div>
);
} else if (gu.ColumnType.BOOL === gu.findColType(colCfg.dtype)) {
return (
<div
onKeyDown={onKeyDown}
Expand Down
36 changes: 30 additions & 6 deletions frontend/static/dtale/edited/EditedCellInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const EditedCellInfo: React.FC<EditedCellInfoProps & WithTranslation> = ({
const [value, setValue] = React.useState<string>();
const [origValue, setOrigValue] = React.useState<string>();
const [options, setOptions] = React.useState<Array<BaseOption<string>>>([]);
const [customOptions, setCustomOptions] = React.useState<Array<BaseOption<string>>>([]);

const cell = React.useMemo(() => {
if (!editedCell) {
Expand All @@ -69,11 +70,17 @@ const EditedCellInfo: React.FC<EditedCellInfoProps & WithTranslation> = ({
}, [origValue]);

React.useEffect(() => {
if (cell?.colCfg && ColumnType.CATEGORY === findColType(cell.colCfg.dtype)) {
(async () => {
const filterData = await ColumnFilterRepository.loadFilterData(dataId, cell.colCfg.name);
setOptions(filterData?.uniques?.map((v) => ({ value: `${v}` })) ?? []);
})();
if (cell?.colCfg) {
const { name } = cell.colCfg;
const settingsOptions = (settings.column_edit_options ?? {})[name] ?? [];
if (settingsOptions.length) {
setCustomOptions(settingsOptions.map((so) => ({ value: so })));
} else if (ColumnType.CATEGORY === findColType(cell.colCfg.dtype)) {
(async () => {
const filterData = await ColumnFilterRepository.loadFilterData(dataId, name);
setOptions(filterData?.uniques?.map((v) => ({ value: `${v}` })) ?? []);
})();
}
}
}, [cell?.colCfg.name]);

Expand All @@ -97,7 +104,24 @@ const EditedCellInfo: React.FC<EditedCellInfoProps & WithTranslation> = ({
const isBool = React.useMemo(() => ColumnType.BOOL === colType, [colType]);

const getInput = (): React.ReactNode => {
if (isBool) {
if (customOptions.length) {
return (
<div
onKeyDown={onKeyDown}
tabIndex={-1}
style={{ width: 'inherit', height: 'inherit', padding: '0' }}
className="editor-select"
>
<DtaleSelect
value={{ value }}
options={[{ value: 'nan' }, ...customOptions]}
onChange={(state: BaseOption<string> | Array<BaseOption<any>> | undefined) =>
setValue((state as BaseOption<string>)?.value ?? '')
}
/>
</div>
);
} else if (isBool) {
return (
<div onKeyDown={onKeyDown} tabIndex={-1} style={{ width: 'inherit', height: 'inherit', padding: '0 0.65em' }}>
<Checkbox
Expand Down
1 change: 1 addition & 0 deletions frontend/static/redux/state/AppState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ export interface InstanceSettings {
columnFilters?: Record<string, ColumnFilter>;
invertFilter?: boolean;
hide_shutdown: boolean;
column_edit_options?: Record<string, string[]>;
}

export const BASE_INSTANCE_SETTINGS: InstanceSettings = Object.freeze({
Expand Down
1 change: 1 addition & 0 deletions tests/dtale/config/dtale.ini
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ hide_columns = c
column_formats = {"a": {"fmt": {"html": true}}}
sort = a|ASC
locked = a,b
column_edit_options = {"a": ["foo", "bar", "baz"]}

[auth]
active = False
Expand Down
3 changes: 3 additions & 0 deletions tests/dtale/config/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ def test_build_show_options(unittest):
)
unittest.assertEqual(final_options["sort"], [("a", "ASC")])
unittest.assertEqual(final_options["locked"], ["a", "b"])
unittest.assertEqual(
final_options["column_edit_options"], {"a": ["foo", "bar", "baz"]}
)

final_options = build_show_options(options)
assert not final_options["allow_cell_edits"]
Expand Down

0 comments on commit bd12ab8

Please sign in to comment.