@@ -12,6 +12,7 @@ import { getSettings } from '../react-common/settingsReactSide';
1212import { CollapseButton } from './collapseButton' ;
1313import { VariableExplorerButtonCellFormatter } from './variableExplorerButtonCellFormatter' ;
1414import { CellStyle , VariableExplorerCellFormatter } from './variableExplorerCellFormatter' ;
15+ import { VariableExplorerEmptyRowsView } from './variableExplorerEmptyRows' ;
1516
1617import * 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
3740const 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+
4349interface IGridRow {
4450 // tslint:disable-next-line:no-any
4551 [ name : string ] : any ;
4652}
4753
4854export 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
0 commit comments