Skip to content

Commit f32fa5b

Browse files
Merge pull request microsoft#5411 from IanMatthewHuff/dev/ianhu/sortVarExpColumns
Column sorting for variable explorer
2 parents a49cebf + a508a49 commit f32fa5b

File tree

7 files changed

+197
-11
lines changed

7 files changed

+197
-11
lines changed

news/1 Enhancements/5228.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Show a message when no variables are defined

news/1 Enhancements/5281.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow column sorting in variable explorer

package.nls.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,5 +236,6 @@
236236
"Common.noIWillDoItLater": "No, I will do it later",
237237
"Common.notNow": "Not now",
238238
"Common.gotIt": "Got it!",
239-
"Interpreters.selectInterpreterTip": "Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar"
239+
"Interpreters.selectInterpreterTip": "Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar",
240+
"DataScience.noRowsInVariableExplorer": "No variables defined"
240241
}

src/datascience-ui/history-react/variableExplorer.tsx

Lines changed: 121 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { getSettings } from '../react-common/settingsReactSide';
1212
import { CollapseButton } from './collapseButton';
1313
import { VariableExplorerButtonCellFormatter } from './variableExplorerButtonCellFormatter';
1414
import { CellStyle, VariableExplorerCellFormatter } from './variableExplorerCellFormatter';
15+
import { VariableExplorerEmptyRowsView } from './variableExplorerEmptyRows';
1516

1617
import * as AdazzleReactDataGrid from 'react-data-grid';
1718

@@ -32,21 +33,27 @@ interface IVariableExplorerState {
3233
gridHeight: number;
3334
height: number;
3435
fontSize: number;
36+
sortDirection: string;
37+
sortColumn: string | number;
3538
}
3639

3740
const defaultColumnProperties = {
3841
filterable: false,
39-
sortable: false,
42+
sortable: true,
4043
resizable: true
4144
};
4245

46+
// Sanity check on our string comparisons
47+
const MaxStringCompare = 400;
48+
4349
interface IGridRow {
4450
// tslint:disable-next-line:no-any
4551
[name: string]: any;
4652
}
4753

4854
export class VariableExplorer extends React.Component<IVariableExplorerProps, IVariableExplorerState> {
4955
private divRef: React.RefObject<HTMLDivElement>;
56+
private variableFetchCount: number;
5057

5158
constructor(prop: IVariableExplorerProps) {
5259
super(prop);
@@ -55,16 +62,19 @@ export class VariableExplorer extends React.Component<IVariableExplorerProps, IV
5562
{key: 'type', name: getLocString('DataScience.variableExplorerTypeColumn', 'Type'), type: 'string', width: 120},
5663
{key: 'size', name: getLocString('DataScience.variableExplorerSizeColumn', 'Count'), type: 'string', width: 120, formatter: <VariableExplorerCellFormatter cellStyle={CellStyle.numeric} />},
5764
{key: 'value', name: getLocString('DataScience.variableExplorerValueColumn', 'Value'), type: 'string', width: 300},
58-
{key: 'buttons', name: '', type: 'boolean', width: 34, formatter: <VariableExplorerButtonCellFormatter showDataExplorer={this.props.showDataExplorer} baseTheme={this.props.baseTheme} /> }
65+
{key: 'buttons', name: '', type: 'boolean', width: 34, sortable: false, resizable: false, formatter: <VariableExplorerButtonCellFormatter showDataExplorer={this.props.showDataExplorer} baseTheme={this.props.baseTheme} /> }
5966
];
6067
this.state = { open: false,
6168
gridColumns: columns,
6269
gridRows: [],
6370
gridHeight: 200,
6471
height: 0,
65-
fontSize: 14};
72+
fontSize: 14,
73+
sortColumn: 'name',
74+
sortDirection: 'NONE'};
6675

6776
this.divRef = React.createRef<HTMLDivElement>();
77+
this.variableFetchCount = 0;
6878
}
6979

7080
public render() {
@@ -86,13 +96,15 @@ export class VariableExplorer extends React.Component<IVariableExplorerProps, IV
8696
<div className={contentClassName}>
8797
<div id='variable-explorer-data-grid'>
8898
<AdazzleReactDataGrid
89-
columns = {this.state.gridColumns.map(c => { return {...c, ...defaultColumnProperties}; })}
99+
columns = {this.state.gridColumns.map(c => { return {...defaultColumnProperties, ...c }; })}
90100
rowGetter = {this.getRow}
91101
rowsCount = {this.state.gridRows.length}
92102
minHeight = {this.state.gridHeight}
93103
headerRowHeight = {this.state.fontSize + 9}
94104
rowHeight = {this.state.fontSize + 9}
95105
onRowDoubleClick = {this.rowDoubleClick}
106+
onGridSort = {this.sortRows}
107+
emptyRowsView = {VariableExplorerEmptyRowsView}
96108
/>
97109
</div>
98110
</div>
@@ -126,10 +138,15 @@ export class VariableExplorer extends React.Component<IVariableExplorerProps, IV
126138
// Help to keep us independent of main history window state if we choose to break out the variable explorer
127139
public newVariablesData(newVariables: IJupyterVariable[]) {
128140
const newGridRows = newVariables.map(newVar => {
129-
return { buttons: {name: newVar.name, supportsDataExplorer: newVar.supportsDataExplorer}, name: newVar.name, type: newVar.type, size: '', value: getLocString('DataScience.variableLoadingValue', 'Loading...')};
141+
return { buttons: {name: newVar.name, supportsDataExplorer: newVar.supportsDataExplorer},
142+
name: newVar.name,
143+
type: newVar.type,
144+
size: '',
145+
value: getLocString('DataScience.variableLoadingValue', 'Loading...')};
130146
});
131147

132148
this.setState({ gridRows: newGridRows});
149+
this.variableFetchCount = newGridRows.length;
133150
}
134151

135152
// Update the value of a single variable already in our list
@@ -148,13 +165,22 @@ export class VariableExplorer extends React.Component<IVariableExplorerProps, IV
148165
newSize = newVariable.count.toString();
149166
}
150167

151-
const newGridRow = {...newGridRows[i], value: newVariable.value, size: newSize};
168+
const newGridRow = {...newGridRows[i],
169+
value: newVariable.value,
170+
size: newSize};
152171

153172
newGridRows[i] = newGridRow;
154173
}
155174
}
156175

157-
this.setState({ gridRows: newGridRows });
176+
// Update that we have retreived a new variable
177+
// When we hit zero we have all the vars and can sort our values
178+
this.variableFetchCount = this.variableFetchCount - 1;
179+
if (this.variableFetchCount === 0) {
180+
this.setState({ gridRows: this.internalSortRows(newGridRows, this.state.sortColumn, this.state.sortDirection) });
181+
} else {
182+
this.setState({ gridRows: newGridRows });
183+
}
158184
}
159185

160186
public toggleInputBlock = () => {
@@ -169,6 +195,94 @@ export class VariableExplorer extends React.Component<IVariableExplorerProps, IV
169195
this.props.variableExplorerToggled(!this.state.open);
170196
}
171197

198+
public sortRows = (sortColumn: string | number, sortDirection: string) => {
199+
this.setState({
200+
sortColumn,
201+
sortDirection,
202+
gridRows: this.internalSortRows(this.state.gridRows, sortColumn, sortDirection)
203+
});
204+
}
205+
206+
private getColumnType(key: string | number) : string | undefined {
207+
let column;
208+
if (typeof key === 'string') {
209+
//tslint:disable-next-line:no-any
210+
column = this.state.gridColumns.find(c => c.key === key) as any;
211+
} else {
212+
// This is the index lookup
213+
column = this.state.gridColumns[key];
214+
}
215+
216+
// Special case our size column, it's displayed as a string
217+
// but we will sort it like a number
218+
if (column && column.key === 'size') {
219+
return 'number';
220+
} else if (column && column.type) {
221+
return column.type;
222+
}
223+
}
224+
225+
private internalSortRows = (gridRows: IGridRow[], sortColumn: string | number, sortDirection: string): IGridRow[] => {
226+
// Default to the name column
227+
if (sortDirection === 'NONE') {
228+
sortColumn = 'name';
229+
sortDirection = 'ASC';
230+
}
231+
232+
const columnType = this.getColumnType(sortColumn);
233+
const isStringColumn = columnType === 'string' || columnType === 'object';
234+
const invert = sortDirection !== 'DESC';
235+
236+
// Use a special comparer for string columns as we can't compare too much of a string
237+
// or it will take too long
238+
const comparer = isStringColumn ?
239+
//tslint:disable-next-line:no-any
240+
(a: any, b: any): number => {
241+
const aVal = a[sortColumn] as string;
242+
const bVal = b[sortColumn] as string;
243+
const aStr = aVal ? aVal.substring(0, Math.min(aVal.length, MaxStringCompare)).toUpperCase() : aVal;
244+
const bStr = bVal ? bVal.substring(0, Math.min(bVal.length, MaxStringCompare)).toUpperCase() : bVal;
245+
const result = aStr > bStr ? -1 : 1;
246+
return invert ? -1 * result : result;
247+
} :
248+
//tslint:disable-next-line:no-any
249+
(a: any, b: any): number => {
250+
const aVal = this.getComparisonValue(a, sortColumn);
251+
const bVal = this.getComparisonValue(b, sortColumn);
252+
const result = aVal > bVal ? -1 : 1;
253+
return invert ? -1 * result : result;
254+
};
255+
256+
return gridRows.sort(comparer);
257+
}
258+
259+
// Get the numerical comparison value for a column
260+
private getComparisonValue(gridRow: IGridRow, sortColumn: string | number): number {
261+
return (sortColumn === 'size') ? this.sizeColumnComparisonValue(gridRow) : gridRow[sortColumn];
262+
}
263+
264+
// The size column needs special casing
265+
private sizeColumnComparisonValue(gridRow: IGridRow): number {
266+
const sizeStr: string = gridRow.size as string;
267+
268+
if (!sizeStr) {
269+
return -1;
270+
}
271+
272+
let sizeNumber = -1;
273+
const commaIndex = sizeStr.indexOf(',');
274+
// First check the shape case like so (5000,1000) in this case we want the 5000 to compare with
275+
if (sizeStr[0] === '(' && commaIndex > 0) {
276+
sizeNumber = parseInt(sizeStr.substring(1, commaIndex), 10);
277+
} else {
278+
// If not in the shape format, assume a to i conversion
279+
sizeNumber = parseInt(sizeStr, 10);
280+
}
281+
282+
// If our parse fails we get NaN for any case that like return -1
283+
return isNaN(sizeNumber) ? -1 : sizeNumber;
284+
}
285+
172286
private rowDoubleClick = (_rowIndex: number, row: IGridRow) => {
173287
// On row double click, see if data explorer is supported and open it if it is
174288
if (row.buttons && row.buttons.supportsDataExplorer !== undefined
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#variable-explorer-empty-rows {
2+
margin: 5px;
3+
font-family: var(--code-font-family);
4+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
'use strict';
4+
import './variableExplorerEmptyRows.css';
5+
6+
import * as React from 'react';
7+
import { getLocString } from '../react-common/locReactSide';
8+
9+
export const VariableExplorerEmptyRowsView = () => {
10+
// IANHU: Change
11+
const message = getLocString('DataScience.noRowsInVariableExplorer', 'No variables defined');
12+
13+
return (
14+
<div id='variable-explorer-empty-rows'>
15+
{message}
16+
</div>
17+
);
18+
};

src/test/datascience/variableexplorer.functional.test.tsx

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ myTuple = 1,2,3,4,5,6,7,8,9
220220
{name: 'myDataframe', value: ' 0\n0 0.00000\n1 2.00004\n2 4.00008\n3 6.00012\n4 8.00016\n5 10.00020\n6 12.00024\n7 14.00028\n8 16.00032\n', supportsDataExplorer: true, type: 'DataFrame', size: 54, shape: '', count: 0, truncated: false},
221221
{name: 'myFloat', value: '9999.9999', supportsDataExplorer: false, type: 'float', size: 58, shape: '', count: 0, truncated: false},
222222
{name: 'myInt', value: '99999999', supportsDataExplorer: false, type: 'int', size: 56, shape: '', count: 0, truncated: false},
223+
{name: 'mynpArray', value: `[0.00000000e+00 2.00004000e+00 4.00008000e+00 ... 9.99959999e+04
224+
9.99980000e+04 1.00000000e+05]`, supportsDataExplorer: true, type: 'ndarray', size: 54, shape: '', count: 0, truncated: false},
223225
{name: 'mySeries', value: `0 0.00000
224226
1 2.00004
225227
2 4.00008
@@ -230,9 +232,44 @@ myTuple = 1,2,3,4,5,6,7,8,9
230232
7 14.00028
231233
8 16.00032
232234
9 `, supportsDataExplorer: true, type: 'Series', size: 54, shape: '', count: 0, truncated: false},
233-
{name: 'myTuple', value: '(1, 2, 3, 4, 5, 6, 7, 8, 9)', supportsDataExplorer: false, type: 'tuple', size: 54, shape: '', count: 0, truncated: false},
234-
{name: 'mynpArray', value: `[0.00000000e+00 2.00004000e+00 4.00008000e+00 ... 9.99959999e+04
235-
9.99980000e+04 1.00000000e+05]`, supportsDataExplorer: true, type: 'ndarray', size: 54, shape: '', count: 0, truncated: false}
235+
{name: 'myTuple', value: '(1, 2, 3, 4, 5, 6, 7, 8, 9)', supportsDataExplorer: false, type: 'tuple', size: 54, shape: '', count: 0, truncated: false}
236+
];
237+
verifyVariables(wrapper, targetVariables);
238+
}, () => { return ioc; });
239+
240+
runMountedTest('Variable explorer - Sorting', async (wrapper) => {
241+
const basicCode: string = `b = 2
242+
c = 3
243+
stra = 'a'
244+
strb = 'b'
245+
strc = 'c'`;
246+
247+
openVariableExplorer(wrapper);
248+
249+
await addCode(getOrCreateHistory, wrapper, 'a=1\na');
250+
await addCode(getOrCreateHistory, wrapper, basicCode, 4);
251+
252+
await waitForUpdate(wrapper, VariableExplorer, 7);
253+
254+
let targetVariables: IJupyterVariable[] = [
255+
{name: 'a', value: '1', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false},
256+
{name: 'b', value: '2', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false},
257+
{name: 'c', value: '3', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false},
258+
{name: 'stra', value: 'a', supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false},
259+
{name: 'strb', value: 'b', supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false},
260+
{name: 'strc', value: 'c', supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false},
261+
];
262+
verifyVariables(wrapper, targetVariables);
263+
264+
sortVariableExplorer(wrapper, 'value', 'DESC');
265+
266+
targetVariables = [
267+
{name: 'strc', value: 'c', supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false},
268+
{name: 'strb', value: 'b', supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false},
269+
{name: 'stra', value: 'a', supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false},
270+
{name: 'c', value: '3', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false},
271+
{name: 'b', value: '2', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false},
272+
{name: 'a', value: '1', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false},
236273
];
237274
verifyVariables(wrapper, targetVariables);
238275
}, () => { return ioc; });
@@ -249,6 +286,16 @@ function openVariableExplorer(wrapper: ReactWrapper<any, Readonly<{}>, React.Com
249286
}
250287
}
251288

289+
function sortVariableExplorer(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, sortColumn: string, sortDirection: string) {
290+
const varExp: VariableExplorer = wrapper.find('VariableExplorer').instance() as VariableExplorer;
291+
292+
assert(varExp);
293+
294+
if (varExp) {
295+
varExp.sortRows(sortColumn, sortDirection);
296+
}
297+
}
298+
252299
// Verify a set of rows versus a set of expected variables
253300
function verifyVariables(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, targetVariables: IJupyterVariable[]) {
254301
const foundRows = wrapper.find('div.react-grid-Row');

0 commit comments

Comments
 (0)