From b634a4d74ddffe1f59680e4281b579e09c79de5b Mon Sep 17 00:00:00 2001 From: Joyce Er Date: Tue, 23 Feb 2021 14:31:13 -0800 Subject: [PATCH] Restyle data viewer and add slice data panel (#4805) --- .eslintrc.js | 4 - ThirdPartyNotices-Repository.txt | 28 + news/1 Enhancements/305.md | 1 + news/1 Enhancements/4689.md | 1 + package-lock.json | 169 +++++ package.json | 1 + .../dataframes/vscodeDataFrame.py | 5 - src/client/common/application/types.ts | 5 + .../application/webviewPanels/webviewPanel.ts | 6 +- .../datascience/data-viewing/dataViewer.ts | 27 +- .../jupyterVariableDataProvider.ts | 4 +- src/client/datascience/data-viewing/types.ts | 5 + .../datascience/jupyter/debuggerVariables.ts | 10 +- .../datascience/jupyter/kernelVariables.ts | 7 +- src/client/datascience/types.ts | 1 + src/datascience-ui/data-explorer/index.tsx | 2 + .../data-explorer/mainPanel.css | 28 +- .../data-explorer/mainPanel.tsx | 48 +- .../data-explorer/reactSlickGrid.css | 61 +- .../data-explorer/reactSlickGrid.tsx | 96 ++- .../data-explorer/reactSlickGridFilterBox.css | 35 +- .../data-explorer/reactSlickGridFilterBox.tsx | 36 +- .../data-explorer/sliceControl.css | 127 ++++ .../data-explorer/sliceControl.tsx | 283 +++++++- .../react-common/seti/seti.less | 656 ++++++++++++++++++ src/datascience-ui/react-common/seti/seti.ttf | Bin 0 -> 53504 bytes .../data-viewing/dataViewer.unit.test.ts | 4 +- src/test/datascience/mountedWebView.ts | 3 + .../datascience/uiTests/webBrowserPanel.ts | 3 + 29 files changed, 1525 insertions(+), 131 deletions(-) create mode 100644 news/1 Enhancements/305.md create mode 100644 news/1 Enhancements/4689.md create mode 100644 src/datascience-ui/data-explorer/sliceControl.css create mode 100644 src/datascience-ui/react-common/seti/seti.less create mode 100644 src/datascience-ui/react-common/seti/seti.ttf diff --git a/.eslintrc.js b/.eslintrc.js index f8ff49aabfc..76fd14572af 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -193,7 +193,6 @@ module.exports = { 'src/test/datascience/cellMatcher.unit.test.ts', 'src/test/datascience/crossProcessLock.unit.test.ts', 'src/test/datascience/uiTests/helpers.ts', - 'src/test/datascience/uiTests/webBrowserPanel.ts', 'src/test/datascience/uiTests/notebookUi.ts', 'src/test/datascience/uiTests/webBrowserPanelProvider.ts', 'src/test/datascience/uiTests/recorder.ts', @@ -297,7 +296,6 @@ module.exports = { 'src/test/datascience/jupyter/jupyterConnection.unit.test.ts', 'src/test/datascience/jupyter/serverCache.unit.test.ts', 'src/test/datascience/mockWorkspaceConfig.ts', - 'src/test/datascience/mountedWebView.ts', 'src/test/datascience/mockProcessService.ts', 'src/test/datascience/testNativeEditorProvider.ts', 'src/test/datascience/cellFactory.unit.test.ts', @@ -415,11 +413,9 @@ module.exports = { 'src/datascience-ui/common/index.ts', 'src/datascience-ui/startPage/index.tsx', 'src/datascience-ui/startPage/startPage.tsx', - 'src/datascience-ui/data-explorer/index.tsx', 'src/datascience-ui/data-explorer/globalJQueryImports.ts', 'src/datascience-ui/data-explorer/emptyRowsView.tsx', 'src/datascience-ui/data-explorer/progressBar.tsx', - 'src/datascience-ui/data-explorer/reactSlickGridFilterBox.tsx', 'src/client/interpreter/interpreterService.ts', 'src/client/interpreter/configuration/interpreterComparer.ts', 'src/client/interpreter/configuration/interpreterSelector/commands/base.ts', diff --git a/ThirdPartyNotices-Repository.txt b/ThirdPartyNotices-Repository.txt index 116fe2e6a98..2d4e963a130 100644 --- a/ThirdPartyNotices-Repository.txt +++ b/ThirdPartyNotices-Repository.txt @@ -956,3 +956,31 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= END OF vscodeJupyter NOTICES, INFORMATION, AND LICENSE + +%% Seti UI NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= + +MIT License + +Copyright (c) 2014 Jesse Weed + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF Seti UI NOTICES, INFORMATION, AND LICENSE \ No newline at end of file diff --git a/news/1 Enhancements/305.md b/news/1 Enhancements/305.md new file mode 100644 index 00000000000..d16455cb692 --- /dev/null +++ b/news/1 Enhancements/305.md @@ -0,0 +1 @@ +Add ability to view a slice of the current variable in the data viewer using either axis/index dropdowns or a slice expression input field. diff --git a/news/1 Enhancements/4689.md b/news/1 Enhancements/4689.md new file mode 100644 index 00000000000..8e2235d40ca --- /dev/null +++ b/news/1 Enhancements/4689.md @@ -0,0 +1 @@ +Always open the data viewer in the last view group that it was moved to. diff --git a/package-lock.json b/package-lock.json index 1efacefed05..c4153edfe68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1279,6 +1279,75 @@ } } }, + "@fluentui/date-time-utilities": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@fluentui/date-time-utilities/-/date-time-utilities-7.9.0.tgz", + "integrity": "sha512-D8p5WWeonqRO1EgIvo7WSlX1rcm87r2VQd62zTJPQImx8rpwc77CRI+iAvfxyVHRZMdt4Qk6Jq99dUaudPWaZw==", + "requires": { + "@uifabric/set-version": "^7.0.23", + "tslib": "^1.10.0" + } + }, + "@fluentui/dom-utilities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@fluentui/dom-utilities/-/dom-utilities-1.1.1.tgz", + "integrity": "sha512-w40gi8fzCpwa7U8cONiuu8rszPStkVOL/weDf5pCbYEb1gdaV7MDPSNkgM6IV0Kz+k017noDgK9Fv4ru1Dwz1g==", + "requires": { + "@uifabric/set-version": "^7.0.23", + "tslib": "^1.10.0" + } + }, + "@fluentui/keyboard-key": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@fluentui/keyboard-key/-/keyboard-key-0.2.13.tgz", + "integrity": "sha512-HLZNtkETFUuCP76Wk/oF54+tVp6aPGzsoJRsmnkh78gloC9CGp8JK+LQUYfj9dtzcHDHq64/dAA2e4j2tzjhaQ==", + "requires": { + "tslib": "^1.10.0" + } + }, + "@fluentui/react": { + "version": "7.160.1", + "resolved": "https://registry.npmjs.org/@fluentui/react/-/react-7.160.1.tgz", + "integrity": "sha512-RvqlSffkiYS87r1fwAJMsr0UwMsgXBrc9FY+4gKg25yN33LJK7tbFIrhE0MiAL9Hp+K78mQT7mFYat6VLsvNPA==", + "requires": { + "@uifabric/set-version": "^7.0.23", + "office-ui-fabric-react": "^7.160.1", + "tslib": "^1.10.0" + } + }, + "@fluentui/react-focus": { + "version": "7.17.4", + "resolved": "https://registry.npmjs.org/@fluentui/react-focus/-/react-focus-7.17.4.tgz", + "integrity": "sha512-L7MK538JOSpLQubyVxYZV1ftd3hViBQhcFftuJfah/mdekQkIcFTS0fsymQ4MK5i7bn13jE7lPM8QfH23wpaJg==", + "requires": { + "@fluentui/keyboard-key": "^0.2.12", + "@uifabric/merge-styles": "^7.19.1", + "@uifabric/set-version": "^7.0.23", + "@uifabric/styling": "^7.18.0", + "@uifabric/utilities": "^7.33.4", + "tslib": "^1.10.0" + } + }, + "@fluentui/react-window-provider": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-window-provider/-/react-window-provider-1.0.1.tgz", + "integrity": "sha512-5hvruDyF0uE8+6YN6Y+d2sEzexBadxUNxUjDcDreTPsmtHPwF5FPBYLhoD7T84L5U4YNvKxKh25tYJm6E0GE2w==", + "requires": { + "@uifabric/set-version": "^7.0.23", + "tslib": "^1.10.0" + } + }, + "@fluentui/theme": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@fluentui/theme/-/theme-1.7.3.tgz", + "integrity": "sha512-S97i1SBL5ytQtZQpygAIvOnQSg9tFZM25843xCY40eWRA/eAdPixzWvVmV8PPQs/K5WmXhghepWaC1SjxVO90g==", + "requires": { + "@uifabric/merge-styles": "^7.19.1", + "@uifabric/set-version": "^7.0.23", + "@uifabric/utilities": "^7.33.4", + "tslib": "^1.10.0" + } + }, "@icons/material": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz", @@ -2627,6 +2696,11 @@ "tinyqueue": "^1.1.0" } }, + "@microsoft/load-themed-styles": { + "version": "1.10.147", + "resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.147.tgz", + "integrity": "sha512-fqkftQUoc2fjR9F+4uZkCt2hJhgZlkgM33k4qD4UdI75+SDOK9Zp5iU3dWzvwDWWVIXTOE+GKMFlmUtrlKZ+fg==" + }, "@nodelib/fs.scandir": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", @@ -4747,6 +4821,81 @@ "eslint-visitor-keys": "^1.1.0" } }, + "@uifabric/foundation": { + "version": "7.9.24", + "resolved": "https://registry.npmjs.org/@uifabric/foundation/-/foundation-7.9.24.tgz", + "integrity": "sha512-f9W6x6gqczgkayA89TsiGzdze2A8RJXc/GbANYvBad3zeyDlQahepMrlgRMOXcTiwZIiltTje+7ADtvD/o176Q==", + "requires": { + "@uifabric/merge-styles": "^7.19.1", + "@uifabric/set-version": "^7.0.23", + "@uifabric/styling": "^7.18.0", + "@uifabric/utilities": "^7.33.4", + "tslib": "^1.10.0" + } + }, + "@uifabric/icons": { + "version": "7.5.21", + "resolved": "https://registry.npmjs.org/@uifabric/icons/-/icons-7.5.21.tgz", + "integrity": "sha512-ZTqLpdCZeCChcMWCgEyWUke2wJxfi3SNdSjNFXWK90SsDWlafg3s/eDd7+n6oRi4pHlF6eBnc4oTLR6PFXt8kQ==", + "requires": { + "@uifabric/set-version": "^7.0.23", + "@uifabric/styling": "^7.18.0", + "tslib": "^1.10.0" + } + }, + "@uifabric/merge-styles": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@uifabric/merge-styles/-/merge-styles-7.19.1.tgz", + "integrity": "sha512-yqUwmk62Kgu216QNPE9vOfS3h0kiSbTvoqM5QcZi+IzpqsBOlzZx3A9Er9UiDaqHRd5lsYF5pO/jeUULmBWF/A==", + "requires": { + "@uifabric/set-version": "^7.0.23", + "tslib": "^1.10.0" + } + }, + "@uifabric/react-hooks": { + "version": "7.13.11", + "resolved": "https://registry.npmjs.org/@uifabric/react-hooks/-/react-hooks-7.13.11.tgz", + "integrity": "sha512-9qX4hiZ4MelUxOxx4IPl+vgn9/e43KkYTpYedl2QtUxFpDjsOVJ0jbG3Dkokyl+Kvw1gerYCT1SXL8UC4kG97w==", + "requires": { + "@fluentui/react-window-provider": "^1.0.1", + "@uifabric/set-version": "^7.0.23", + "@uifabric/utilities": "^7.33.4", + "tslib": "^1.10.0" + } + }, + "@uifabric/set-version": { + "version": "7.0.23", + "resolved": "https://registry.npmjs.org/@uifabric/set-version/-/set-version-7.0.23.tgz", + "integrity": "sha512-9E+YKtnH2kyMKnK9XZZsqyM8OCxEJIIfxtaThTlQpYOzrWAGJxQADFbZ7+Usi0U2xHnWNPFROjq+B9ocEzhqMA==", + "requires": { + "tslib": "^1.10.0" + } + }, + "@uifabric/styling": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@uifabric/styling/-/styling-7.18.0.tgz", + "integrity": "sha512-PuCSr2PeVDLTtyqOjS67QBgN7rDYf044oRvMEkSCP57p9Du8rIAJio0LQjVNRKo+QlmfgFzmT3XjpS2LYkhFIg==", + "requires": { + "@fluentui/theme": "^1.7.3", + "@microsoft/load-themed-styles": "^1.10.26", + "@uifabric/merge-styles": "^7.19.1", + "@uifabric/set-version": "^7.0.23", + "@uifabric/utilities": "^7.33.4", + "tslib": "^1.10.0" + } + }, + "@uifabric/utilities": { + "version": "7.33.4", + "resolved": "https://registry.npmjs.org/@uifabric/utilities/-/utilities-7.33.4.tgz", + "integrity": "sha512-FgEL2+GWOHMNMQqmI5Mko293W3c9PWIc/WrlU6jQ5AU83M1BQSxhS0LPNnRbahukiEFNkosCOOKzKCNdjzkCwA==", + "requires": { + "@fluentui/dom-utilities": "^1.1.1", + "@uifabric/merge-styles": "^7.19.1", + "@uifabric/set-version": "^7.0.23", + "prop-types": "^15.7.2", + "tslib": "^1.10.0" + } + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -18785,6 +18934,26 @@ "has": "^1.0.3" } }, + "office-ui-fabric-react": { + "version": "7.160.1", + "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-7.160.1.tgz", + "integrity": "sha512-yfwYBXZscIJgL8r/SRSiAhgLzp9QvbA7UtWsDxEleJH1YG2FG7fbUe/JkO/76WkUJjytilXGgAS9ZbL5NLdBXA==", + "requires": { + "@fluentui/date-time-utilities": "^7.9.0", + "@fluentui/react-focus": "^7.17.4", + "@fluentui/react-window-provider": "^1.0.1", + "@microsoft/load-themed-styles": "^1.10.26", + "@uifabric/foundation": "^7.9.24", + "@uifabric/icons": "^7.5.21", + "@uifabric/merge-styles": "^7.19.1", + "@uifabric/react-hooks": "^7.13.11", + "@uifabric/set-version": "^7.0.23", + "@uifabric/styling": "^7.18.0", + "@uifabric/utilities": "^7.33.4", + "prop-types": "^15.7.2", + "tslib": "^1.10.0" + } + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", diff --git a/package.json b/package.json index 1047ebbce90..c868db66868 100644 --- a/package.json +++ b/package.json @@ -1867,6 +1867,7 @@ }, "dependencies": { "@enonic/fnv-plus": "^1.3.0", + "@fluentui/react": "^7.160.1", "@jupyter-widgets/base": "^2.0.1", "@jupyter-widgets/controls": "^1.5.2", "@jupyter-widgets/jupyterlab-manager": "^1.0.2", diff --git a/pythonFiles/vscode_datascience_helpers/dataframes/vscodeDataFrame.py b/pythonFiles/vscode_datascience_helpers/dataframes/vscodeDataFrame.py index 820948ccf3f..90e685f1c2a 100644 --- a/pythonFiles/vscode_datascience_helpers/dataframes/vscodeDataFrame.py +++ b/pythonFiles/vscode_datascience_helpers/dataframes/vscodeDataFrame.py @@ -164,11 +164,6 @@ def _VSCODE_getDataFrameInfo(df): columnTypes = _VSCODE_builtins.list(df.dtypes) - # Make sure the index column exists - if indexColumn not in columnNames: - columnNames.insert(0, indexColumn) - columnTypes.insert(0, "int64") - # Then loop and generate our output json columns = [] for n in _VSCODE_builtins.range(0, _VSCODE_builtins.len(columnNames)): diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 4d5bb567cc7..00a49ea7997 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -1109,6 +1109,11 @@ export interface IWebviewViewOptions extends IWebviewOptions { // Wraps the VS Code webview panel export const IWebviewPanel = Symbol('IWebviewPanel'); export interface IWebviewPanel extends IWebview { + /** + * Editor position of the panel. This property is only set if the webview is in + * one of the editor view columns. + */ + viewColumn: ViewColumn | undefined; setTitle(val: string): void; /** * Makes the webpanel show up. diff --git a/src/client/common/application/webviewPanels/webviewPanel.ts b/src/client/common/application/webviewPanels/webviewPanel.ts index 30178d75dac..14f3f52d1c6 100644 --- a/src/client/common/application/webviewPanels/webviewPanel.ts +++ b/src/client/common/application/webviewPanels/webviewPanel.ts @@ -3,7 +3,7 @@ 'use strict'; import '../../extensions'; -import { Uri, WebviewOptions, WebviewPanel as vscodeWebviewPanel, window } from 'vscode'; +import { Uri, ViewColumn, WebviewOptions, WebviewPanel as vscodeWebviewPanel, window } from 'vscode'; import { IFileSystem } from '../../platform/types'; import { IDisposableRegistry } from '../../types'; import { IWebviewPanel, IWebviewPanelOptions } from '../types'; @@ -44,6 +44,10 @@ export class WebviewPanel extends Webview implements IWebviewPanel { } } + public get viewColumn(): ViewColumn | undefined { + return this.panel?.viewColumn; + } + public isVisible(): boolean { return this.panel ? this.panel.visible : false; } diff --git a/src/client/datascience/data-viewing/dataViewer.ts b/src/client/datascience/data-viewing/dataViewer.ts index de406ab3558..f68a74b0281 100644 --- a/src/client/datascience/data-viewing/dataViewer.ts +++ b/src/client/datascience/data-viewing/dataViewer.ts @@ -3,21 +3,28 @@ 'use strict'; import '../../common/extensions'; -import { inject, injectable } from 'inversify'; +import { inject, injectable, named } from 'inversify'; import * as path from 'path'; -import { ViewColumn } from 'vscode'; +import { Memento, ViewColumn } from 'vscode'; import { IApplicationShell, IWebviewPanelProvider, IWorkspaceService } from '../../common/application/types'; import { EXTENSION_ROOT_DIR, UseCustomEditorApi } from '../../common/constants'; import { traceError } from '../../common/logger'; -import { IConfigurationService, IDisposable, IExperimentService, Resource } from '../../common/types'; +import { + GLOBAL_MEMENTO, + IConfigurationService, + IDisposable, + IExperimentService, + IMemento, + Resource +} from '../../common/types'; import * as localize from '../../common/utils/localize'; import { noop } from '../../common/utils/misc'; import { StopWatch } from '../../common/utils/stopWatch'; import { sendTelemetryEvent } from '../../telemetry'; import { HelpLinks, Telemetry } from '../constants'; import { JupyterDataRateLimitError } from '../jupyter/jupyterDataRateLimitError'; -import { ICodeCssGenerator, IThemeFinder } from '../types'; +import { ICodeCssGenerator, IThemeFinder, WebViewViewChangeEventArgs } from '../types'; import { WebviewPanelHost } from '../webviews/webviewPanelHost'; import { DataViewerMessageListener } from './dataViewerMessageListener'; import { @@ -31,6 +38,7 @@ import { } from './types'; import { Experiments } from '../../common/experiments/groups'; +const PREFERRED_VIEWGROUP = 'JupyterDataViewerPreferredViewColumn'; const dataExplorerDir = path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'viewers'); @injectable() export class DataViewer extends WebviewPanelHost implements IDataViewer, IDisposable { @@ -48,7 +56,8 @@ export class DataViewer extends WebviewPanelHost implements @inject(IWorkspaceService) workspaceService: IWorkspaceService, @inject(IApplicationShell) private applicationShell: IApplicationShell, @inject(UseCustomEditorApi) useCustomEditorApi: boolean, - @inject(IExperimentService) private experimentService: IExperimentService + @inject(IExperimentService) private experimentService: IExperimentService, + @inject(IMemento) @named(GLOBAL_MEMENTO) readonly globalMemento: Memento ) { super( configuration, @@ -60,7 +69,7 @@ export class DataViewer extends WebviewPanelHost implements dataExplorerDir, [path.join(dataExplorerDir, 'commons.initial.bundle.js'), path.join(dataExplorerDir, 'dataExplorer.js')], localize.DataScience.dataExplorerTitle(), - ViewColumn.One, + globalMemento.get(PREFERRED_VIEWGROUP) ?? ViewColumn.One, useCustomEditorApi ); } @@ -100,6 +109,12 @@ export class DataViewer extends WebviewPanelHost implements } } + protected async onViewStateChanged(args: WebViewViewChangeEventArgs) { + if (args.current.active && args.current.visible && args.previous.active && args.current.visible) { + await this.globalMemento.update(PREFERRED_VIEWGROUP, this.webPanel?.viewColumn); + } + } + protected get owningResource(): Resource { return undefined; } diff --git a/src/client/datascience/data-viewing/jupyterVariableDataProvider.ts b/src/client/datascience/data-viewing/jupyterVariableDataProvider.ts index 71999299417..161bee678b2 100644 --- a/src/client/datascience/data-viewing/jupyterVariableDataProvider.ts +++ b/src/client/datascience/data-viewing/jupyterVariableDataProvider.ts @@ -96,7 +96,9 @@ export class JupyterVariableDataProvider implements IJupyterVariableDataProvider shape: JupyterVariableDataProvider.parseShape(variable.shape), sliceExpression, type: variable.type, - maximumRowChunkSize: variable.maximumRowChunkSize + maximumRowChunkSize: variable.maximumRowChunkSize, + name: variable.name, + fileName: variable.fileName }; } return dataFrameInfo; diff --git a/src/client/datascience/data-viewing/types.ts b/src/client/datascience/data-viewing/types.ts index 574d6eb3c85..3d919e31d16 100644 --- a/src/client/datascience/data-viewing/types.ts +++ b/src/client/datascience/data-viewing/types.ts @@ -68,6 +68,11 @@ export interface IDataFrameInfo { maximumRowChunkSize?: number; type?: string; originalVariableType?: string; + name?: string; + /** + * The name of the file that this variable was declared in. + */ + fileName?: string; } export interface IDataViewerDataProvider { diff --git a/src/client/datascience/jupyter/debuggerVariables.ts b/src/client/datascience/jupyter/debuggerVariables.ts index 1367d83d516..4271eace38e 100644 --- a/src/client/datascience/jupyter/debuggerVariables.ts +++ b/src/client/datascience/jupyter/debuggerVariables.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. 'use strict'; import { inject, injectable, named } from 'inversify'; +import * as path from 'path'; import { DebugAdapterTracker, Disposable, Event, EventEmitter } from 'vscode'; import { DebugProtocol } from 'vscode-debugprotocol'; @@ -133,12 +134,19 @@ export class DebuggerVariables extends DebugLocationTracker (targetVariable as any).frameId ); + let fileName; + if (notebook) { + fileName = path.basename(notebook.identity.path); + } else if (this.debugLocation?.fileName) { + fileName = path.basename(this.debugLocation.fileName); + } // Results should be the updated variable. return results ? { ...targetVariable, ...JSON.parse(results.result), - maximumRowChunkSize: MaximumRowChunkSizeForDebugger + maximumRowChunkSize: MaximumRowChunkSizeForDebugger, + fileName } : targetVariable; } diff --git a/src/client/datascience/jupyter/kernelVariables.ts b/src/client/datascience/jupyter/kernelVariables.ts index 12fe436a6ba..9c437ab06a8 100644 --- a/src/client/datascience/jupyter/kernelVariables.ts +++ b/src/client/datascience/jupyter/kernelVariables.ts @@ -5,7 +5,7 @@ import type { nbformat } from '@jupyterlab/coreutils'; import { inject, injectable } from 'inversify'; import stripAnsi from 'strip-ansi'; import * as uuid from 'uuid/v4'; - +import * as path from 'path'; import { CancellationToken, Event, EventEmitter, Uri } from 'vscode'; import { PYTHON_LANGUAGE } from '../../common/constants'; import { Experiments } from '../../common/experiments/groups'; @@ -139,10 +139,13 @@ export class KernelVariables implements IJupyterVariables { true ); + const fileName = path.basename(notebook.identity.path); + // Combine with the original result (the call only returns the new fields) return { ...targetVariable, - ...this.deserializeJupyterResult(results) + ...this.deserializeJupyterResult(results), + fileName }; } diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts index f88feb9147b..8950348cae6 100644 --- a/src/client/datascience/types.ts +++ b/src/client/datascience/types.ts @@ -888,6 +888,7 @@ export interface IJupyterVariable { rowCount?: number; indexColumn?: string; maximumRowChunkSize?: number; + fileName?: string; } export const IJupyterVariableDataProvider = Symbol('IJupyterVariableDataProvider'); diff --git a/src/datascience-ui/data-explorer/index.tsx b/src/datascience-ui/data-explorer/index.tsx index 7c2a3b53cc5..fc3b639bd68 100644 --- a/src/datascience-ui/data-explorer/index.tsx +++ b/src/datascience-ui/data-explorer/index.tsx @@ -11,6 +11,7 @@ import '../common/index.css'; import * as React from 'react'; import * as ReactDOM from 'react-dom'; +import { initializeIcons } from '@fluentui/react'; import { IVsCodeApi } from '../react-common/postOffice'; import { detectBaseTheme } from '../react-common/themeDetector'; @@ -20,6 +21,7 @@ import { MainPanel } from './mainPanel'; export declare function acquireVsCodeApi(): IVsCodeApi; const baseTheme = detectBaseTheme(); +initializeIcons(); /* eslint-disable */ ReactDOM.render( diff --git a/src/datascience-ui/data-explorer/mainPanel.css b/src/datascience-ui/data-explorer/mainPanel.css index 0616067bf24..df884152de0 100644 --- a/src/datascience-ui/data-explorer/mainPanel.css +++ b/src/datascience-ui/data-explorer/mainPanel.css @@ -1,4 +1,3 @@ - .main-panel { position: absolute; bottom: 0; @@ -11,3 +10,30 @@ overflow: hidden; } +.breadcrumb-container { + display: flex; + align-items: flex-start; + padding-left: 16px; +} + +.breadcrumb { + font-family: var(--vscode-font-family); + font-weight: var(--vscode-font-weight); + font-size: var(--vscode-font-size); + color: var(--vscode-breadcrumb-foreground); + background-color: var(--vscode-breadcrumb-background); + display: flex; + flex-direction: row; + line-height: var(--vscode-font-size); + padding-top: 2px; +} + +.image-button-image { + height: var(--vscode-font-size); +} + +.breadcrumb-file-icon { + color: #519aba; + font-size: 18px; + padding-right: 2px; +} diff --git a/src/datascience-ui/data-explorer/mainPanel.tsx b/src/datascience-ui/data-explorer/mainPanel.tsx index 472beac9d8a..cc40e4b93a8 100644 --- a/src/datascience-ui/data-explorer/mainPanel.tsx +++ b/src/datascience-ui/data-explorer/mainPanel.tsx @@ -5,7 +5,6 @@ import './mainPanel.css'; import { JSONArray } from '@phosphor/coreutils'; import * as React from 'react'; -import * as uuid from 'uuid/v4'; import { CellFetchAllLimit, @@ -28,6 +27,10 @@ import { StyleInjector } from '../react-common/styleInjector'; import { cellFormatterFunc } from './cellFormatter'; import { ISlickGridAdd, ISlickGridSlice, ISlickRow, ReactSlickGrid } from './reactSlickGrid'; import { generateTestData } from './testData'; +import { Image, ImageName } from '../react-common/image'; + +import '../react-common/codicon/codicon.css'; +import '../react-common/seti/seti.less'; const SliceableTypes: Set = new Set(['ndarray', 'Tensor', 'EagerTensor']); @@ -52,6 +55,8 @@ interface IMainPanelState { originalVariableType?: string; isSliceDataEnabled: boolean; maximumRowChunkSize?: number; + variableName?: string; + fileName?: string; } export class MainPanel extends React.Component implements IMessageHandler { @@ -148,11 +153,36 @@ export class MainPanel extends React.Component postOffice={this.postOffice} /> {progressBar} + {this.renderBreadcrumb()} {this.state.totalRowCount > 0 && this.state.styleReady && this.renderGrid()} ); }; + private renderBreadcrumb() { + if (this.state.fileName) { + let breadcrumbText = this.state.variableName; + if (this.state.originalVariableShape) { + breadcrumbText += ' (' + this.state.originalVariableShape?.join(', ') + ')'; + } + return ( +
+
+
+ {this.state.fileName} + + {breadcrumbText} +
+
+ ); + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any public handleMessage = (msg: string, payload?: any) => { switch (msg) { @@ -233,6 +263,8 @@ export class MainPanel extends React.Component const indexColumn = variable.indexColumn ? variable.indexColumn : 'index'; const originalVariableType = this.state.originalVariableType ?? variable.type; const originalVariableShape = this.state.originalVariableShape ?? variable.shape; + const variableName = this.state.variableName ?? variable.name; + const fileName = this.state.fileName ?? variable.fileName; const isSliceDataEnabled = payload.isSliceDataEnabled && SliceableTypes.has(originalVariableType || ''); // New data coming in, so reset everything and clear our cache of columns @@ -249,6 +281,8 @@ export class MainPanel extends React.Component originalVariableShape, dataDimensionality: variable.dataDimensionality ?? 2, isSliceDataEnabled, + variableName, + fileName, // Maximum number of rows is 100 if evaluating in debugger, undefined otherwise maximumRowChunkSize: variable.maximumRowChunkSize ?? this.state.maximumRowChunkSize }); @@ -322,8 +356,14 @@ export class MainPanel extends React.Component } private generateColumns(variable: IDataFrameInfo): Slick.Column[] { + // Generate an index column + const indexColumn = { + key: this.state.indexColumn, + type: ColumnType.Number + }; if (variable.columns) { - return variable.columns.map((c: { key: string; type: ColumnType }, i: number) => { + const columns = [indexColumn].concat(variable.columns); + return columns.map((c: { key: string; type: ColumnType }, i: number) => { return { type: c.type, field: c.key.toString(), @@ -344,12 +384,12 @@ export class MainPanel extends React.Component // Set of columns to update based on this batch of rows const columnsToUpdate = new Set(); // Make sure we have an index field and all rows have an item - const normalizedRows = rows.map((r: any | undefined) => { + const normalizedRows = rows.map((r: any | undefined, idx: number) => { if (!r) { r = {}; } if (!r.hasOwnProperty(this.state.indexColumn)) { - r[this.state.indexColumn] = uuid(); + r[this.state.indexColumn] = this.state.fetchedRowCount + idx; } for (let [key, value] of Object.entries(r)) { switch (value) { diff --git a/src/datascience-ui/data-explorer/reactSlickGrid.css b/src/datascience-ui/data-explorer/reactSlickGrid.css index a90e60dbc80..10f743af5f5 100644 --- a/src/datascience-ui/data-explorer/reactSlickGrid.css +++ b/src/datascience-ui/data-explorer/reactSlickGrid.css @@ -21,13 +21,13 @@ border: none; border-radius: 5px; cursor: pointer; - margin:8px 4px; + margin: 8px 4px; margin-right: 18px; } .react-grid-header-cell { padding: 0px 4px; - background-color: var(--vscode-debugToolBar-background); + background-color: var(--vscode-menu-background); color: var(--vscode-editor-foreground); text-align: left; font-weight: bold; @@ -35,7 +35,7 @@ } .react-grid-cell { - padding: 0px 4px; + padding: 4px; background-color: var(--vscode-editor-background); color: var(--vscode-editor-foreground); border-bottom-color: var(--vscode-editor-inactiveSelectionBackground); @@ -45,15 +45,23 @@ } .react-grid-cell.active { - background-color: var(--vscode-button-background); + background-color: var(--vscode-list-focusBackground); color: var(--vscode-button-foreground); } /* Some overrides necessary to get the colors we want */ .slick-headerrow-column { - background-color: var(--vscode-debugToolBar-background); + background-color: var(--vscode-menu-background); border-right-color: var(--vscode-editor-inactiveSelectionBackground); border-right-style: solid; + border-bottom-color: var(--vscode-menu-background); +} + +.slick-headerrow-column.ui-state-default { + padding-left: 3px; + padding-right: 3px; + padding-bottom: 3px; + padding-top: 1px; } .slick-header-column.ui-state-default, @@ -74,32 +82,33 @@ .react-grid-header-cell > .slick-sort-indicator-asc::before { background: none; - content: '▲'; + font: normal normal normal 16px/1 codicon; + content: '\eaa1'; /* VS Code arrow-up codicon */ align-items: center; } .react-grid-header-cell > .slick-sort-indicator-desc::before { background: none; - content: '▼'; + font: normal normal normal 16px/1 codicon; + content: '\ea9a'; /* VS Code arrow-down codicon */ align-items: center; } .slick-row:hover > .react-grid-cell { - background-color: var(--override-selection-background, var(--vscode-editor-selectionBackground)); + background-color: var(--override-selection-background, var(--vscode-list-hoverBackground)); } /* Slick.Editors.Text */ input.editor-text { width: 100%; height: 100%; - border: 0 none; + box-sizing: border-box; + border: 2px var(--vscode-input-border); + padding: 2px; margin: 0; outline: 0 none; - padding: 0; - border-bottom-color: var(--vscode-editor-inactiveSelectionBackground); - border-right-color: var(--vscode-editor-inactiveSelectionBackground); - border-right-style: solid; - background-color: var(--vscode-button-background); + border-style: solid; + background-color: var(--vscode-list-focusBackground); color: var(--vscode-button-foreground); text-align: left; /* input does not inherit font from body */ @@ -111,13 +120,21 @@ input.editor-text { .slick-cell.editable { border-color: none; border-style: none; + border: 0; + padding: 0; + margin: 0; } -input.slice-data { - background-color: var(--vscode-editor-background); - color: var(--vscode-editor-foreground); - border-style: solid; - border-radius: 1pt; - border-color: var(--vscode-editor-inactiveSelectionBackground); - outline: 0 none; -} \ No newline at end of file +.control-container { + border-bottom-color: var(--vscode-editor-inactiveSelectionBackground); + border-bottom-style: solid; + border-bottom-width: 1px; + padding: 6px; + display: flex; + justify-content: start; + flex-direction: row; +} + +.codicon-button { + cursor: pointer; +} diff --git a/src/datascience-ui/data-explorer/reactSlickGrid.tsx b/src/datascience-ui/data-explorer/reactSlickGrid.tsx index 44c34a65416..ea63b01721c 100644 --- a/src/datascience-ui/data-explorer/reactSlickGrid.tsx +++ b/src/datascience-ui/data-explorer/reactSlickGrid.tsx @@ -30,6 +30,7 @@ import 'slickgrid/slick.editors'; // Adding comments to ensure order of imports does not change due to auto formatters. // eslint-disable-next-line import/order import 'slickgrid/plugins/slick.autotooltips'; +import 'slickgrid/plugins/slick.headerbuttons'; // Adding comments to ensure order of imports does not change due to auto formatters. // eslint-disable-next-line import/order import 'slickgrid/slick.grid.css'; @@ -206,7 +207,7 @@ export class ReactSlickGrid extends React.Component(this.containerRef.current, this.dataView, columns, options); grid.registerPlugin(new Slick.AutoTooltips({ enableForCells: true, enableForHeaderCells: true })); - + grid.registerPlugin(new Slick.Plugins.HeaderButtons()); // Setup our dataview this.dataView.beginUpdate(); this.dataView.setFilter(this.filter.bind(this)); @@ -290,7 +291,6 @@ export class ReactSlickGrid extends React.Component -
-
- - {this.renderTemporarySliceIndicator()} -
-
+ {this.renderSliceControls()}
); } - public renderTemporarySliceIndicator = () => { - if (this.props.isSliceDataEnabled && this.props.originalVariableShape) { + public renderSliceControls = () => { + if ( + this.props.isSliceDataEnabled && + this.props.originalVariableShape && + this.props.originalVariableShape.filter((v) => !!v).length > 1 + ) { return ( - +
+ +
); } }; @@ -354,6 +348,11 @@ export class ReactSlickGrid extends React.Component { + this.columnFilters = new Map(); + this.dataView.refresh(); + }; + private styleColumns(columns: Slick.Column[]) { // Transform columns so they are sortable and stylable return columns.map((c) => { @@ -371,13 +370,13 @@ export class ReactSlickGrid extends React.Component { - c.width = maxFieldWidth; + if (c.id !== '0') { + c.width = maxFieldWidth; + } else { + c.width = maxFieldWidth / 2; + c.name = ''; + c.header = { + buttons: [ + { + cssClass: 'codicon codicon-filter codicon-button', + handler: this.clickFilterButton + } + ] + }; + } }); this.state.grid.setColumns(columns); @@ -494,13 +506,14 @@ export class ReactSlickGrid extends React.Component { this.updateCssStyles(); - - // Hide the header row after we finally resize our columns - this.state.grid!.setHeaderRowVisibility(false); }, 0); } } + private clickFilterButton = () => { + this.setState({ showingFilters: !this.state.showingFilters }); + }; + private computeFont(): string | null { if (this.containerRef.current) { const style = getComputedStyle(this.containerRef.current); @@ -565,13 +578,22 @@ export class ReactSlickGrid extends React.Component { - e.preventDefault(); - this.setState({ showingFilters: !this.state.showingFilters }); - }; - private renderFilterCell = (_e: Slick.EventData, args: Slick.OnHeaderRowCellRenderedEventArgs) => { - ReactDOM.render(, args.node); + if (args.column.id === '0') { + ReactDOM.render( +
, + args.node + ); + } else { + ReactDOM.render( + , + args.node + ); + } }; private compareElements(a: any, b: any, col?: Slick.Column): number { @@ -616,8 +638,7 @@ function readonlyCellEditor(this: any, args: any) { $input = slickgridJQ("") .appendTo(args.container) .on('keydown.nav', handleKeyDown) - .focus() - .select(); + .focus(); }; this.destroy = function destroy() { @@ -636,7 +657,6 @@ function readonlyCellEditor(this: any, args: any) { defaultValue = generateDisplayValue(item[args.column.field]); $input.val(defaultValue); $input[0].defaultValue = defaultValue; - $input.select(); }; this.applyValue = function applyValue() { diff --git a/src/datascience-ui/data-explorer/reactSlickGridFilterBox.css b/src/datascience-ui/data-explorer/reactSlickGridFilterBox.css index 0d567065077..9305d3080c9 100644 --- a/src/datascience-ui/data-explorer/reactSlickGridFilterBox.css +++ b/src/datascience-ui/data-explorer/reactSlickGridFilterBox.css @@ -1,17 +1,26 @@ .filter-box { - border-color: var(--vscode-editor-inactiveSelectionBackground); - border-style: solid; - border-width: 1px; - display: block; - position: relative; - left: -2px; - top: -3px; - width: 98%; - padding: 1px; - margin: 0px; + font-family: var(--vscode-font-family); + font-weight: var(--vscode-font-weight); + font-size: var(--vscode-font-size); + color: var(--vscode-settings-textInputForeground); + background-color: var(--vscode-settings-textInputBackground); + box-sizing: border-box; + height: 100%; + border: 0px none; + border-radius: 0px; } -.filter-box:focus { - border-color: var(--vscode-editor-selectionBackground); +input { outline: none; -} \ No newline at end of file + color: var(--vscode-editor-foreground); +} + +input:focus { + outline: none; + color: var(--vscode-editor-foreground); +} + +.iconContainer-41 { + width: auto; + padding-right: 3px; +} diff --git a/src/datascience-ui/data-explorer/reactSlickGridFilterBox.tsx b/src/datascience-ui/data-explorer/reactSlickGridFilterBox.tsx index 12c1a9530cd..8b58f34b461 100644 --- a/src/datascience-ui/data-explorer/reactSlickGridFilterBox.tsx +++ b/src/datascience-ui/data-explorer/reactSlickGridFilterBox.tsx @@ -3,11 +3,24 @@ 'use strict'; import * as React from 'react'; +import { IIconProps, SearchBox } from '@fluentui/react'; import './reactSlickGridFilterBox.css'; +const filterIcon: IIconProps = { + iconName: 'Filter', + styles: { + root: { + fontSize: 'var(--vscode-font-size)', + width: 'var(--vscode-font-size)', + color: 'var(--vscode-settings-textInputForeground)' + } + } +}; + interface IFilterProps { column: Slick.Column; + fontSize: number; onChange(val: string, column: Slick.Column): void; } @@ -18,20 +31,27 @@ export class ReactSlickGridFilterBox extends React.Component { public render() { return ( - ); } - private updateInputValue = (evt: React.SyntheticEvent) => { - const element = evt.currentTarget as HTMLInputElement; - if (element) { - this.props.onChange(element.value, this.props.column); + private clearInputValue = () => { + this.props.onChange('', this.props.column); + }; + + private updateInputValue = ( + _event?: React.ChangeEvent | undefined, + newValue?: string | undefined + ) => { + if (newValue !== undefined) { + this.props.onChange(newValue, this.props.column); } }; } diff --git a/src/datascience-ui/data-explorer/sliceControl.css b/src/datascience-ui/data-explorer/sliceControl.css new file mode 100644 index 00000000000..246d1d6f5c2 --- /dev/null +++ b/src/datascience-ui/data-explorer/sliceControl.css @@ -0,0 +1,127 @@ +.slice-data { + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + padding: 4px; + /* input does not inherit font from body */ + font-family: var(--vscode-font-family); + font-weight: var(--vscode-font-weight); + font-size: var(--vscode-font-size); + height: 30px; +} + +[class*='ms-Dropdown is-disabled'], +.submit-slice-button:disabled, +.slice-data:disabled { + color: #cccccc; + opacity: 0.4; +} + +.slicing-control { + font-family: var(--vscode-font-family); + font-weight: var(--vscode-font-weight); + font-size: var(--vscode-font-size); +} + +.slice-summary { + display: flex; + flex-direction: row; +} + +.slice-summary-detail { + padding-bottom: 2px; +} + +.current-slice { + margin-left: 10px; + padding-left: 5px; + padding-right: 5px; + padding-top: 0px; + background-color: var(--vscode-input-background); +} + +.slice-form { + align-self: center; + flex-direction: column; + justify-content: space-between; + padding: 4px; +} + +.slice-control-row { + display: flex; + flex-direction: row; + margin-left: 30px; +} + +details > summary { + list-style-type: none; + display: flex; +} + +details > summary::-webkit-details-marker { + display: none; +} + +details > summary::before { + font: normal normal normal 16px/1 codicon; + content: '\eab6'; +} + +details[open] > summary::before { + font: normal normal normal 16px/1 codicon; + content: '\eab4'; +} + +.submit-slice-button { + color: var(--vscode-button-foreground); + background-color: var(--vscode-button-background); + border: none; + margin-left: 10px; + padding: 4px 8px; + height: 30px; + font-family: var(--vscode-font-family); + font-weight: var(--vscode-font-weight); + font-size: var(--vscode-font-size); +} + +.slice-enablement-checkbox { + margin-right: 6px; + width: 20px; +} + +.slice-enablement-checkbox-container { + padding-top: 4px; + padding-bottom: 4px; +} + +:focus { + outline: none; +} + +/* Overrides for Fluent UI controls */ +[class*='fieldGroup'] { + border: 0 none; + background: none; +} + +[class*='text'] { + color: var(--vscode-editor-foreground); + font-family: var(--vscode-font-family); + font-weight: var(--vscode-font-weight); + font-size: var(--vscode-font-size); +} + +[class*='ms-Dropdown-label'] { + color: var(--vscode-editor-foreground); + font-family: var(--vscode-font-family); + font-weight: var(--vscode-font-weight); + font-size: var(--vscode-font-size); +} + +[class*='ms-Dropdown-title'] { + font-family: var(--vscode-font-family); + font-weight: var(--vscode-font-weight); + font-size: var(--vscode-font-size); + background-color: var(--vscode-dropdown-background); + border: var(--vscode-dropdown-border); + color: var(--vscode-dropdown-foreground); +} diff --git a/src/datascience-ui/data-explorer/sliceControl.tsx b/src/datascience-ui/data-explorer/sliceControl.tsx index 770afbe794f..3aff1d82304 100644 --- a/src/datascience-ui/data-explorer/sliceControl.tsx +++ b/src/datascience-ui/data-explorer/sliceControl.tsx @@ -1,53 +1,288 @@ +import { Dropdown, IDropdownOption, ResponsiveMode, TextField } from '@fluentui/react'; import * as React from 'react'; import { IGetSliceRequest } from '../../client/datascience/data-viewing/types'; +import './sliceControl.css'; + +const sliceRegEx = /^\s*(?\d+:)|(?:\d+)|(?:(?-?\d+)(?::(?-?\d+))?(?::(?-?\d+))?)\s*$/; + interface ISliceControlProps { originalVariableShape: number[]; handleSliceRequest(slice: IGetSliceRequest): void; } interface ISliceControlState { - value: string; + sliceExpression: string; + inputValue: string; + isExpanded: boolean; + isActive: boolean; + selectedAxis0?: number; + selectedIndex0?: number; + selectedAxis1?: number; + selectedIndex1?: number; } -// Temporary UI entrypoint to slicing functionality until we get a proper UI designed export class SliceControl extends React.Component { constructor(props: ISliceControlProps) { super(props); - this.state = { value: '[' + this.props.originalVariableShape.map(() => ':').join(', ') + ']' }; + const initialSlice = this.preselectedSliceExpression(); + this.state = { isExpanded: false, isActive: false, sliceExpression: initialSlice, inputValue: initialSlice }; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } - public handleChange(event: React.FormEvent) { - this.setState({ value: event.currentTarget.value }); - } + public render() { + const indexOptions = this.generateIndexDropdownOptions(); + const axisOptions = this.generateAxisDropdownOptions(); - public handleSubmit(event: React.SyntheticEvent) { - event.preventDefault(); - this.props.handleSliceRequest({ - slice: this.state.value - }); - } + const dropdownStyles = { + dropdownItem: { + color: 'var(--vscode-dropdown-foreground)', + fontFamily: 'var(--vscode-font-family)', + fontWeight: 'var(--vscode-font-weight)', + fontSize: 'var(--vscode-font-size)', + backgroundColor: 'var(--vscode-dropdown-background)' + }, + caretDown: { + color: 'var(--vscode-dropdown-foreground)' + } + }; - render() { return ( -
-
-
+ + ); + } + + private renderReadonlyIndicator = () => { + if (this.state.isActive) { + return {this.state.sliceExpression}; + } + }; + + private toggleEnablement = () => { + const isActive = !this.state.isActive; + const newState = { isActive }; + const slice = isActive + ? this.state.sliceExpression + : '[' + this.props.originalVariableShape.map(() => ':').join(', ') + ']'; + this.props.handleSliceRequest({ slice }); + this.applyInputBoxToDropdowns(); + this.setState(newState); + }; + + private handleChange = ( + _event: React.FormEvent, + newValue: string | undefined + ) => { + this.setState({ inputValue: newValue ?? '' }); + }; + + private handleSubmit = (event: React.SyntheticEvent) => { + event.preventDefault(); + this.setState({ sliceExpression: this.state.inputValue }); + // Update axis and index dropdown selections + this.applyInputBoxToDropdowns(); + this.props.handleSliceRequest({ + slice: this.state.inputValue + }); + }; + + private preselectedSliceExpression() { + let numDimensionsToPreselect = this.props.originalVariableShape.length - 2; + return ( + '[' + + this.props.originalVariableShape + .map(() => { + if (numDimensionsToPreselect > 0) { + numDimensionsToPreselect -= 1; + return '0'; + } + return ':'; + }) + .join(', ') + + ']' ); } + + private validateSliceExpression = () => { + const { inputValue } = this.state; + if (inputValue.startsWith('[') && inputValue.endsWith(']')) { + let hasOutOfRangeIndex: { shapeIndex: number; value: number } | undefined; + const parsedExpression = inputValue + .substring(1, inputValue.length - 1) + .split(',') + .map((shapeEl, shapeIndex) => { + // Validate IndexErrors + const match = sliceRegEx.exec(shapeEl); + if (match?.groups?.Start && !match.groups.Stop) { + const value = parseInt(match.groups.Start); + const numberOfElementsAlongAxis = this.props.originalVariableShape[shapeIndex]; + if ( + (value >= 0 && value >= numberOfElementsAlongAxis) || + // Python allows negative index values + (value < 0 && value < -numberOfElementsAlongAxis) + ) { + hasOutOfRangeIndex = { shapeIndex, value }; + } + return value; + } + }); + + if (hasOutOfRangeIndex) { + const { shapeIndex, value } = hasOutOfRangeIndex; + return `IndexError at axis ${shapeIndex}, index ${value}`; + } else if (parsedExpression && parsedExpression.length !== this.props.originalVariableShape.length) { + return 'Invalid slice expression'; + } + } + return ''; + }; + + private applyInputBoxToDropdowns = () => { + setTimeout(() => { + const shape = this.state.sliceExpression; + if (shape.startsWith('[') && shape.endsWith(']')) { + const dropdowns: { axis: number; index: number }[] = []; + let numRangeObjects = 0; + shape + .substring(1, shape.length - 1) + .split(',') + .forEach((shapeEl, idx) => { + // Validate the slice object + const match = sliceRegEx.exec(shapeEl); + if (match?.groups?.Start && !match.groups.Stop) { + // Can map index expressions like [2, :, :] to dropdowns + dropdowns.push({ axis: idx, index: parseInt(match.groups.Start) }); + } else if (match?.groups?.StopRange !== undefined || match?.groups?.StartRange !== undefined) { + // Can't map expressions like [0:, :] to dropdown + numRangeObjects += 1; + } + }); + const state = {}; + const ndim = this.props.originalVariableShape.length; + if ( + numRangeObjects === 0 && + ((ndim === 2 && dropdowns.length === 1) || (ndim > 2 && dropdowns.length === ndim - 2)) + ) { + // Apply values to dropdowns + for (let i = 0; i < dropdowns.length; i++) { + const selection = dropdowns[i]; + (state as any)[`selectedAxis${i.toString()}`] = selection.axis; + (state as any)[`selectedIndex${i.toString()}`] = selection.index; + } + } else { + // Unset dropdowns + for (const key in this.state) { + if (key.startsWith('selected')) { + (state as any)[key] = null; + } + } + } + this.setState(state); + } + }); + }; + + private applyDropdownsToInputBox = () => { + setTimeout(() => { + if (this.state.selectedAxis0 !== undefined && this.state.selectedIndex0 !== undefined) { + // Calculate new slice expression from dropdown values + const newSliceExpression = + '[' + + this.props.originalVariableShape + .map((_val, idx) => { + if (idx === this.state.selectedAxis0) { + return this.state.selectedIndex0; + } + return ':'; + }) + .join(', ') + + ']'; + this.setState({ sliceExpression: newSliceExpression, inputValue: newSliceExpression }); + this.props.handleSliceRequest({ slice: newSliceExpression }); + } + }); + }; + + private updateAxis = (_data: React.FormEvent, option: IDropdownOption | undefined) => { + this.setState({ selectedAxis0: option?.key as number }); + this.applyDropdownsToInputBox(); + }; + + private updateIndex = (_data: React.FormEvent, option: IDropdownOption | undefined) => { + this.setState({ selectedIndex0: option?.key as number }); + this.applyDropdownsToInputBox(); + }; + + private generateAxisDropdownOptions = () => { + return this.props.originalVariableShape.map((_val, idx) => { + return { key: idx, text: idx.toString() }; + }); + }; + + private generateIndexDropdownOptions = () => { + if (this.state.selectedAxis0 !== undefined) { + const range = this.props.originalVariableShape[this.state.selectedAxis0]; + const result = []; + for (let i = 0; i < range; i++) { + result.push({ key: i, text: i.toString() }); + } + return result; + } + return []; + }; } diff --git a/src/datascience-ui/react-common/seti/seti.less b/src/datascience-ui/react-common/seti/seti.less new file mode 100644 index 00000000000..e5e67185a55 --- /dev/null +++ b/src/datascience-ui/react-common/seti/seti.less @@ -0,0 +1,656 @@ +@font-face { + font-family: 'seti'; + src: // url('atom://seti-ui/styles/_fonts/seti/seti.eot?#iefix') format('eot'), + // url('atom://seti-ui/styles/_fonts/seti/seti.woff2') format('woff2'), + url('./seti.ttf') format('truetype'); +} + +.icon-base-pseudo { + font-family: 'seti'; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-style: normal; + font-variant: normal; + font-weight: normal; + text-decoration: none; + text-transform: none; +} + +.icon-char(@filename) { + @R: '\E001'; + @apple: '\E002'; + @argdown: '\E003'; + @asm: '\E004'; + @audio: '\E005'; + @babel: '\E006'; + @bower: '\E007'; + @bsl: '\E008'; + @c-sharp: '\E009'; + @c: '\E00A'; + @cake: '\E00B'; + @cake_php: '\E00C'; + @checkbox-unchecked: '\E00D'; + @checkbox: '\E00E'; + @cjsx: '\E00F'; + @clock: '\E010'; + @clojure: '\E011'; + @code-climate: '\E012'; + @code-search: '\E013'; + @coffee: '\E014'; + @coffee_erb: '\E015'; + @coldfusion: '\E016'; + @config: '\E017'; + @cpp: '\E018'; + @crystal: '\E019'; + @crystal_embedded: '\E01A'; + @css: '\E01B'; + @csv: '\E01C'; + @cu: '\E01D'; + @d: '\E01E'; + @dart: '\E01F'; + @db: '\E020'; + @default: '\E021'; + @deprecation-cop: '\E022'; + @docker: '\E023'; + @editorconfig: '\E024'; + @ejs: '\E025'; + @elixir: '\E026'; + @elixir_script: '\E027'; + @elm: '\E028'; + @error: '\E029'; + @eslint: '\E02A'; + @ethereum: '\E02B'; + @f-sharp: '\E02C'; + @favicon: '\E02D'; + @firebase: '\E02E'; + @firefox: '\E02F'; + @folder: '\E030'; + @font: '\E031'; + @git: '\E032'; + @git_folder: '\E033'; + @git_ignore: '\E034'; + @github: '\E035'; + @go: '\E036'; + @go2: '\E037'; + @gradle: '\E038'; + @grails: '\E039'; + @graphql: '\E03A'; + @grunt: '\E03B'; + @gulp: '\E03C'; + @hacklang: '\E03D'; + @haml: '\E03E'; + @happenings: '\E03F'; + @haskell: '\E040'; + @haxe: '\E041'; + @heroku: '\E042'; + @hex: '\E043'; + @html: '\E044'; + @html_erb: '\E045'; + @ignored: '\E046'; + @illustrator: '\E047'; + @image: '\E048'; + @info: '\E049'; + @ionic: '\E04A'; + @jade: '\E04B'; + @java: '\E04C'; + @javascript: '\E04D'; + @jenkins: '\E04E'; + @jinja: '\E04F'; + @js_erb: '\E050'; + @json: '\E051'; + @julia: '\E052'; + @karma: '\E053'; + @kotlin: '\E054'; + @less: '\E055'; + @license: '\E056'; + @liquid: '\E057'; + @livescript: '\E058'; + @lock: '\E059'; + @lua: '\E05A'; + @makefile: '\E05B'; + @markdown: '\E05C'; + @maven: '\E05D'; + @mdo: '\E05E'; + @mustache: '\E05F'; + @new-file: '\E060'; + @npm: '\E061'; + @npm_ignored: '\E062'; + @nunjucks: '\E063'; + @ocaml: '\E064'; + @odata: '\E065'; + @pddl: '\E066'; + @pdf: '\E067'; + @perl: '\E068'; + @photoshop: '\E069'; + @php: '\E06A'; + @plan: '\E06B'; + @platformio: '\E06C'; + @powershell: '\E06D'; + @project: '\E06E'; + @prolog: '\E06F'; + @pug: '\E070'; + @puppet: '\E071'; + @python: '\E072'; + @rails: '\E073'; + @react: '\E074'; + @reasonml: '\E075'; + @rollup: '\E076'; + @ruby: '\E077'; + @rust: '\E078'; + @salesforce: '\E079'; + @sass: '\E07A'; + @sbt: '\E07B'; + @scala: '\E07C'; + @search: '\E07D'; + @settings: '\E07E'; + @shell: '\E07F'; + @slim: '\E080'; + @smarty: '\E081'; + @spring: '\E082'; + @stylelint: '\E083'; + @stylus: '\E084'; + @sublime: '\E085'; + @svg: '\E086'; + @swift: '\E087'; + @terraform: '\E088'; + @tex: '\E089'; + @time-cop: '\E08A'; + @todo: '\E08B'; + @tsconfig: '\E08C'; + @twig: '\E08D'; + @typescript: '\E08E'; + @vala: '\E08F'; + @video: '\E090'; + @vue: '\E091'; + @wasm: '\E092'; + @wat: '\E093'; + @webpack: '\E094'; + @wgt: '\E095'; + @windows: '\E096'; + @word: '\E097'; + @xls: '\E098'; + @xml: '\E099'; + @yarn: '\E09A'; + @yml: '\E09B'; + @zip: '\E09C'; + + content: @@filename; +} + +.icon(@filename, @insert: before) { + @pseudo-selector: ~':@{insert}'; + + &@{pseudo-selector} { + &:extend(.icon-base-pseudo); + .icon-char(@filename); + } +} + +.icon-R { + .icon(R); +} +.icon-apple { + .icon(apple); +} +.icon-argdown { + .icon(argdown); +} +.icon-asm { + .icon(asm); +} +.icon-audio { + .icon(audio); +} +.icon-babel { + .icon(babel); +} +.icon-bower { + .icon(bower); +} +.icon-bsl { + .icon(bsl); +} +.icon-c-sharp { + .icon(c-sharp); +} +.icon-c { + .icon(c); +} +.icon-cake { + .icon(cake); +} +.icon-cake_php { + .icon(cake_php); +} +.icon-checkbox-unchecked { + .icon(checkbox-unchecked); +} +.icon-checkbox { + .icon(checkbox); +} +.icon-cjsx { + .icon(cjsx); +} +.icon-clock { + .icon(clock); +} +.icon-clojure { + .icon(clojure); +} +.icon-code-climate { + .icon(code-climate); +} +.icon-code-search { + .icon(code-search); +} +.icon-coffee { + .icon(coffee); +} +.icon-coffee_erb { + .icon(coffee_erb); +} +.icon-coldfusion { + .icon(coldfusion); +} +.icon-config { + .icon(config); +} +.icon-cpp { + .icon(cpp); +} +.icon-crystal { + .icon(crystal); +} +.icon-crystal_embedded { + .icon(crystal_embedded); +} +.icon-css { + .icon(css); +} +.icon-csv { + .icon(csv); +} +.icon-cu { + .icon(cu); +} +.icon-d { + .icon(d); +} +.icon-dart { + .icon(dart); +} +.icon-db { + .icon(db); +} +.icon-default { + .icon(default); +} +.icon-deprecation-cop { + .icon(deprecation-cop); +} +.icon-docker { + .icon(docker); +} +.icon-editorconfig { + .icon(editorconfig); +} +.icon-ejs { + .icon(ejs); +} +.icon-elixir { + .icon(elixir); +} +.icon-elixir_script { + .icon(elixir_script); +} +.icon-elm { + .icon(elm); +} +.icon-error { + .icon(error); +} +.icon-eslint { + .icon(eslint); +} +.icon-ethereum { + .icon(ethereum); +} +.icon-f-sharp { + .icon(f-sharp); +} +.icon-favicon { + .icon(favicon); +} +.icon-firebase { + .icon(firebase); +} +.icon-firefox { + .icon(firefox); +} +.icon-folder { + .icon(folder); +} +.icon-font { + .icon(font); +} +.icon-git { + .icon(git); +} +.icon-git_folder { + .icon(git_folder); +} +.icon-git_ignore { + .icon(git_ignore); +} +.icon-github { + .icon(github); +} +.icon-go { + .icon(go); +} +.icon-go2 { + .icon(go2); +} +.icon-gradle { + .icon(gradle); +} +.icon-grails { + .icon(grails); +} +.icon-graphql { + .icon(graphql); +} +.icon-grunt { + .icon(grunt); +} +.icon-gulp { + .icon(gulp); +} +.icon-hacklang { + .icon(hacklang); +} +.icon-haml { + .icon(haml); +} +.icon-happenings { + .icon(happenings); +} +.icon-haskell { + .icon(haskell); +} +.icon-haxe { + .icon(haxe); +} +.icon-heroku { + .icon(heroku); +} +.icon-hex { + .icon(hex); +} +.icon-html { + .icon(html); +} +.icon-html_erb { + .icon(html_erb); +} +.icon-ignored { + .icon(ignored); +} +.icon-illustrator { + .icon(illustrator); +} +.icon-image { + .icon(image); +} +.icon-info { + .icon(info); +} +.icon-ionic { + .icon(ionic); +} +.icon-jade { + .icon(jade); +} +.icon-java { + .icon(java); +} +.icon-javascript { + .icon(javascript); +} +.icon-jenkins { + .icon(jenkins); +} +.icon-jinja { + .icon(jinja); +} +.icon-js_erb { + .icon(js_erb); +} +.icon-json { + .icon(json); +} +.icon-julia { + .icon(julia); +} +.icon-karma { + .icon(karma); +} +.icon-kotlin { + .icon(kotlin); +} +.icon-less { + .icon(less); +} +.icon-license { + .icon(license); +} +.icon-liquid { + .icon(liquid); +} +.icon-livescript { + .icon(livescript); +} +.icon-lock { + .icon(lock); +} +.icon-lua { + .icon(lua); +} +.icon-makefile { + .icon(makefile); +} +.icon-markdown { + .icon(markdown); +} +.icon-maven { + .icon(maven); +} +.icon-mdo { + .icon(mdo); +} +.icon-mustache { + .icon(mustache); +} +.icon-new-file { + .icon(new-file); +} +.icon-npm { + .icon(npm); +} +.icon-npm_ignored { + .icon(npm_ignored); +} +.icon-nunjucks { + .icon(nunjucks); +} +.icon-ocaml { + .icon(ocaml); +} +.icon-odata { + .icon(odata); +} +.icon-pddl { + .icon(pddl); +} +.icon-pdf { + .icon(pdf); +} +.icon-perl { + .icon(perl); +} +.icon-photoshop { + .icon(photoshop); +} +.icon-php { + .icon(php); +} +.icon-plan { + .icon(plan); +} +.icon-platformio { + .icon(platformio); +} +.icon-powershell { + .icon(powershell); +} +.icon-project { + .icon(project); +} +.icon-prolog { + .icon(prolog); +} +.icon-pug { + .icon(pug); +} +.icon-puppet { + .icon(puppet); +} +.icon-python { + .icon(python); +} +.icon-rails { + .icon(rails); +} +.icon-react { + .icon(react); +} +.icon-reasonml { + .icon(reasonml); +} +.icon-rollup { + .icon(rollup); +} +.icon-ruby { + .icon(ruby); +} +.icon-rust { + .icon(rust); +} +.icon-salesforce { + .icon(salesforce); +} +.icon-sass { + .icon(sass); +} +.icon-sbt { + .icon(sbt); +} +.icon-scala { + .icon(scala); +} +.icon-search { + .icon(search); +} +.icon-settings { + .icon(settings); +} +.icon-shell { + .icon(shell); +} +.icon-slim { + .icon(slim); +} +.icon-smarty { + .icon(smarty); +} +.icon-spring { + .icon(spring); +} +.icon-stylelint { + .icon(stylelint); +} +.icon-stylus { + .icon(stylus); +} +.icon-sublime { + .icon(sublime); +} +.icon-svg { + .icon(svg); +} +.icon-swift { + .icon(swift); +} +.icon-terraform { + .icon(terraform); +} +.icon-tex { + .icon(tex); +} +.icon-time-cop { + .icon(time-cop); +} +.icon-todo { + .icon(todo); +} +.icon-tsconfig { + .icon(tsconfig); +} +.icon-twig { + .icon(twig); +} +.icon-typescript { + .icon(typescript); +} +.icon-vala { + .icon(vala); +} +.icon-video { + .icon(video); +} +.icon-vue { + .icon(vue); +} +.icon-wasm { + .icon(wasm); +} +.icon-wat { + .icon(wat); +} +.icon-webpack { + .icon(webpack); +} +.icon-wgt { + .icon(wgt); +} +.icon-windows { + .icon(windows); +} +.icon-word { + .icon(word); +} +.icon-xls { + .icon(xls); +} +.icon-xml { + .icon(xml); +} +.icon-yarn { + .icon(yarn); +} +.icon-yml { + .icon(yml); +} +.icon-zip { + .icon(zip); +} diff --git a/src/datascience-ui/react-common/seti/seti.ttf b/src/datascience-ui/react-common/seti/seti.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6da1ead50a1a6d0f6f16377d99577865edf8ab45 GIT binary patch literal 53504 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 bt6S#%msYI@7B|<-2QICe?^xLoR{8${zY8sr literal 0 HcmV?d00001 diff --git a/src/test/datascience/data-viewing/dataViewer.unit.test.ts b/src/test/datascience/data-viewing/dataViewer.unit.test.ts index 0c65fadaed6..a43457c8180 100644 --- a/src/test/datascience/data-viewing/dataViewer.unit.test.ts +++ b/src/test/datascience/data-viewing/dataViewer.unit.test.ts @@ -19,6 +19,7 @@ import { JupyterVariableDataProvider } from '../../../client/datascience/data-vi import { IDataViewer, IDataViewerDataProvider } from '../../../client/datascience/data-viewing/types'; import { ThemeFinder } from '../../../client/datascience/themeFinder'; import { ICodeCssGenerator, IThemeFinder } from '../../../client/datascience/types'; +import { MockMemento } from '../../mocks/mementos'; suite('DataScience - DataViewer', () => { let dataViewer: IDataViewer; @@ -58,7 +59,8 @@ suite('DataScience - DataViewer', () => { instance(workspaceService), instance(applicationShell), false, - instance(experimentService) + instance(experimentService), + new MockMemento() ); }); test('Data viewer showData calls gets dataFrame info from data provider', async () => { diff --git a/src/test/datascience/mountedWebView.ts b/src/test/datascience/mountedWebView.ts index 3216fdb1c0e..b437873c910 100644 --- a/src/test/datascience/mountedWebView.ts +++ b/src/test/datascience/mountedWebView.ts @@ -59,6 +59,9 @@ export class MountedWebView implements IMountedWebView, IDisposable { private visible = true; private disposedEvent = new EventEmitter(); private loadFailedEmitter = new EventEmitter(); + public get viewColumn() { + return 1; + } constructor(mount: () => ReactWrapper, React.Component>, public readonly id: string) { // Setup the acquireVsCodeApi. The react control will cache this value when it's mounted. diff --git a/src/test/datascience/uiTests/webBrowserPanel.ts b/src/test/datascience/uiTests/webBrowserPanel.ts index 218df16345f..d4707c78219 100644 --- a/src/test/datascience/uiTests/webBrowserPanel.ts +++ b/src/test/datascience/uiTests/webBrowserPanel.ts @@ -162,6 +162,9 @@ export class WebBrowserPanel implements IWebviewPanel, IDisposable { private server?: IWebServer; private serverUrl: string | undefined; private loadFailedEmitter = new EventEmitter(); + public get viewColumn() { + return this.panel?.viewColumn; + } constructor( private readonly disposableRegistry: IDisposableRegistry, private readonly options: IWebviewPanelOptions