From c7b9ff67946afc9dd626fd2f3a06bf87f36e0bf4 Mon Sep 17 00:00:00 2001 From: Jordi Bunster Date: Sun, 25 Apr 2021 21:31:10 -0700 Subject: [PATCH 1/8] Lazy loaded Monaco (#720) Lazy loaded Monaco --- .../DiffEditor/DiffEditorComponent.ts | 6 +- .../Controls/Editor/EditorComponent.ts | 13 ++-- src/Explorer/Controls/Editor/EditorReact.tsx | 5 +- .../JsonEditor/JsonEditorComponent.ts | 13 ++-- .../IndexingPolicyComponent.tsx | 12 ++-- src/Explorer/LazyMonaco.ts | 5 ++ src/Explorer/Tabs/ScriptTabBase.ts | 65 +------------------ webpack.config.js | 11 ++-- 8 files changed, 40 insertions(+), 90 deletions(-) create mode 100644 src/Explorer/LazyMonaco.ts diff --git a/src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts b/src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts index 854cb26f3..a20bb4718 100644 --- a/src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts +++ b/src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts @@ -1,6 +1,6 @@ import * as ViewModels from "../../../Contracts/ViewModels"; +import { loadMonaco, monaco } from "../../LazyMonaco"; import template from "./diff-editor-component.html"; -import * as monaco from "monaco-editor"; /** * Helper class for ko component registration @@ -92,7 +92,7 @@ export class DiffEditorViewModel { /** * Create the monaco editor on diff mode and attach to DOM */ - protected createDiffEditor( + protected async createDiffEditor( originalContent: string, modifiedContent: string, createCallback: (e: monaco.editor.IStandaloneDiffEditor) => void @@ -111,7 +111,7 @@ export class DiffEditorViewModel { } const language = this.params.editorLanguage || "json"; - + const monaco = await loadMonaco(); const originalModel = monaco.editor.createModel(originalContent, language); const modifiedModel = monaco.editor.createModel(modifiedContent, language); const diffEditor: monaco.editor.IStandaloneDiffEditor = monaco.editor.createDiffEditor( diff --git a/src/Explorer/Controls/Editor/EditorComponent.ts b/src/Explorer/Controls/Editor/EditorComponent.ts index 5f77c0a52..0b697c8e2 100644 --- a/src/Explorer/Controls/Editor/EditorComponent.ts +++ b/src/Explorer/Controls/Editor/EditorComponent.ts @@ -1,7 +1,6 @@ +import { loadMonaco, monaco } from "../../LazyMonaco"; import { JsonEditorParams, JsonEditorViewModel } from "../JsonEditor/JsonEditorComponent"; import template from "./editor-component.html"; -import * as monaco from "monaco-editor"; -import { SqlCompletionItemProvider, ErrorMarkProvider } from "@azure/cosmos-language-service"; /** * Helper class for ko component registration @@ -49,15 +48,17 @@ class EditorViewModel extends JsonEditorViewModel { return this.params.contentType; } - protected registerCompletionItemProvider() { - let sqlCompletionItemProvider = new SqlCompletionItemProvider(); + protected async registerCompletionItemProvider() { if (EditorViewModel.providerRegistered.indexOf("sql") < 0) { - monaco.languages.registerCompletionItemProvider("sql", sqlCompletionItemProvider); + const { SqlCompletionItemProvider } = await import("@azure/cosmos-language-service"); + const monaco = await loadMonaco(); + monaco.languages.registerCompletionItemProvider("sql", new SqlCompletionItemProvider()); EditorViewModel.providerRegistered.push("sql"); } } - protected getErrorMarkers(input: string): Q.Promise { + protected async getErrorMarkers(input: string): Promise { + const { ErrorMarkProvider } = await import("@azure/cosmos-language-service"); return ErrorMarkProvider.getErrorMark(input); } } diff --git a/src/Explorer/Controls/Editor/EditorReact.tsx b/src/Explorer/Controls/Editor/EditorReact.tsx index 49cf2aca2..71273ed20 100644 --- a/src/Explorer/Controls/Editor/EditorReact.tsx +++ b/src/Explorer/Controls/Editor/EditorReact.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import * as monaco from "monaco-editor"; +import { loadMonaco, monaco } from "../../LazyMonaco"; export interface EditorReactProps { language: string; @@ -61,7 +61,7 @@ export class EditorReact extends React.Component { /** * Create the monaco editor and attach to DOM */ - private createEditor(createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) { + private async createEditor(createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) { const options: monaco.editor.IEditorConstructionOptions = { value: this.props.content, language: this.props.language, @@ -74,6 +74,7 @@ export class EditorReact extends React.Component { }; this.rootNode.innerHTML = ""; + const monaco = await loadMonaco(); createCallback(monaco.editor.create(this.rootNode, options)); } diff --git a/src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts b/src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts index 542f3e6a8..381281d30 100644 --- a/src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts +++ b/src/Explorer/Controls/JsonEditor/JsonEditorComponent.ts @@ -1,6 +1,5 @@ -import Q from "q"; -import * as monaco from "monaco-editor"; import * as ViewModels from "../../../Contracts/ViewModels"; +import { loadMonaco, monaco } from "../../LazyMonaco"; import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel"; import template from "./json-editor-component.html"; @@ -88,7 +87,7 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel { /** * Create the monaco editor and attach to DOM */ - protected createEditor(content: string, createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) { + protected async createEditor(content: string, createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) { this.registerCompletionItemProvider(); this.editorContainer = document.getElementById(this.getEditorId()); const options: monaco.editor.IEditorConstructionOptions = { @@ -102,6 +101,7 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel { }; this.editorContainer.innerHTML = ""; + const monaco = await loadMonaco(); createCallback(monaco.editor.create(this.editorContainer, options)); } @@ -109,15 +109,16 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel { protected registerCompletionItemProvider() {} // Interface. Will be implemented in children editor view model such as EditorViewModel. - protected getErrorMarkers(input: string): Q.Promise { - return Q.Promise(() => {}); + protected async getErrorMarkers(_: string): Promise { + return []; } protected getEditorLanguage(): string { return "json"; } - protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) { + protected async configureEditor(editor: monaco.editor.IStandaloneCodeEditor) { + const monaco = await loadMonaco(); this.editor = editor; const queryEditorModel = this.editor.getModel(); if (!this.params.isReadOnly && this.params.updatedContent) { diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx index 631880844..9e4949bf1 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/IndexingPolicyComponent.tsx @@ -1,9 +1,9 @@ +import { MessageBar, MessageBarType, Stack } from "office-ui-fabric-react"; import * as React from "react"; import * as DataModels from "../../../../Contracts/DataModels"; -import * as monaco from "monaco-editor"; -import { isDirty, isIndexTransforming } from "../SettingsUtils"; -import { MessageBar, MessageBarType, Stack } from "office-ui-fabric-react"; +import { loadMonaco, monaco } from "../../../LazyMonaco"; import { indexingPolicynUnsavedWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils"; +import { isDirty, isIndexTransforming } from "../SettingsUtils"; import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent"; export interface IndexingPolicyComponentProps { @@ -84,9 +84,9 @@ export class IndexingPolicyComponent extends React.Component< return false; }; - private createIndexingPolicyEditor = (): void => { + private async createIndexingPolicyEditor(): Promise { const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4); - + const monaco = await loadMonaco(); this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, { value: value, language: "json", @@ -98,7 +98,7 @@ export class IndexingPolicyComponent extends React.Component< indexingPolicyEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this)); this.props.logIndexingPolicySuccessMessage(); } - }; + } private onEditorContentChange = (): void => { const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel(); diff --git a/src/Explorer/LazyMonaco.ts b/src/Explorer/LazyMonaco.ts new file mode 100644 index 000000000..4b1c97c52 --- /dev/null +++ b/src/Explorer/LazyMonaco.ts @@ -0,0 +1,5 @@ +import type * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +export type { monaco }; + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const loadMonaco = () => import(/* webpackChunkName: "lazy-monaco" */ "monaco-editor/esm/vs/editor/editor.api"); diff --git a/src/Explorer/Tabs/ScriptTabBase.ts b/src/Explorer/Tabs/ScriptTabBase.ts index a5a994a9b..4f270cb60 100644 --- a/src/Explorer/Tabs/ScriptTabBase.ts +++ b/src/Explorer/Tabs/ScriptTabBase.ts @@ -1,5 +1,4 @@ import * as ko from "knockout"; -import * as monaco from "monaco-editor"; import Q from "q"; import DiscardIcon from "../../../images/discard.svg"; import SaveIcon from "../../../images/save-cosmos.svg"; @@ -8,6 +7,7 @@ import editable from "../../Common/EditableUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import { loadMonaco, monaco } from "../LazyMonaco"; import TabsBase from "./TabsBase"; export default abstract class ScriptTabBase extends TabsBase implements ViewModels.WaitsForTemplate { @@ -299,38 +299,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode return !!value; } - private static _toSeverity(severity: string): monaco.MarkerSeverity { - switch (severity.toLowerCase()) { - case "error": - return monaco.MarkerSeverity.Error; - case "warning": - return monaco.MarkerSeverity.Warning; - case "info": - return monaco.MarkerSeverity.Info; - case "ignore": - default: - return monaco.MarkerSeverity.Hint; - } - } - - private static _toEditorPosition(target: number, lines: string[]): ViewModels.EditorPosition { - let cursor: number = 0; - let previousCursor: number = 0; - let i: number = 0; - while (target > cursor + lines[i].length) { - cursor += lines[i].length + 2; - i++; - } - - const editorPosition: ViewModels.EditorPosition = { - line: i + 1, - column: target - cursor + 1, - }; - - return editorPosition; - } - - protected _createBodyEditor() { + protected async _createBodyEditor() { const id = this.editorId; const container = document.getElementById(id); const options = { @@ -341,7 +310,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode }; container.innerHTML = ""; - + const monaco = await loadMonaco(); const editor = monaco.editor.create(container, options); this.editor(editor); @@ -353,32 +322,4 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode const editorModel = this.editor().getModel(); this.editorContent(editorModel.getValue()); } - - private _setModelMarkers(errors: ViewModels.QueryError[]) { - const markers: monaco.editor.IMarkerData[] = errors.map((e) => this._toMarker(e)); - const editorModel = this.editor().getModel(); - monaco.editor.setModelMarkers(editorModel, this.tabId, markers); - } - - private _resetModelMarkers() { - const queryEditorModel = this.editor().getModel(); - monaco.editor.setModelMarkers(queryEditorModel, this.tabId, []); - } - - private _toMarker(error: ViewModels.QueryError): monaco.editor.IMarkerData { - const editorModel = this.editor().getModel(); - const lines: string[] = editorModel.getLinesContent(); - const start: ViewModels.EditorPosition = ScriptTabBase._toEditorPosition(Number(error.start), lines); - const end: ViewModels.EditorPosition = ScriptTabBase._toEditorPosition(Number(error.end), lines); - - return { - severity: ScriptTabBase._toSeverity(error.severity), - message: error.message, - startLineNumber: start.line, - startColumn: start.column, - endLineNumber: end.line, - endColumn: end.column, - code: error.code, - }; - } } diff --git a/webpack.config.js b/webpack.config.js index 8f3e1ba53..12c980224 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable no-undef */ require("dotenv/config"); const path = require("path"); const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin"); @@ -83,7 +85,8 @@ const typescriptRule = { exclude: /node_modules/, }; -module.exports = function (env = {}, argv = {}) { +// eslint-disable-next-line @typescript-eslint/no-unused-vars +module.exports = function (_env = {}, argv = {}) { const mode = argv.mode || "development"; const rules = [fontRule, lessRule, imagesRule, cssRule, htmlRule, typescriptRule]; const envVars = { @@ -211,6 +214,7 @@ module.exports = function (env = {}, argv = {}) { util: true, tls: "empty", net: "empty", + fs: "empty", }, output: { chunkFilename: "[name].[chunkhash:6].js", @@ -264,7 +268,7 @@ module.exports = function (env = {}, argv = {}) { target: "https://main.documentdb.ext.azure.com", changeOrigin: true, logLevel: "debug", - bypass: function (req, res, proxyOptions) { + bypass: (req, res) => { if (req.method === "OPTIONS") { res.statusCode = 200; res.send(); @@ -304,8 +308,5 @@ module.exports = function (env = {}, argv = {}) { }, }, stats: "minimal", - node: { - fs: "empty", - }, }; }; From 127784abdda38c76ca1b4351222326c876489a61 Mon Sep 17 00:00:00 2001 From: Jordi Bunster Date: Tue, 27 Apr 2021 08:14:07 -0700 Subject: [PATCH 2/8] Bypass Knockout and adapters in GalleryTab (#728) --- ...lleryAndNotebookViewerComponentAdapter.tsx | 29 --------- src/Explorer/Explorer.tsx | 43 ++++++------- src/Explorer/Tabs/GalleryTab.tsx | 60 ++++--------------- 3 files changed, 35 insertions(+), 97 deletions(-) delete mode 100644 src/Explorer/Controls/NotebookGallery/GalleryAndNotebookViewerComponentAdapter.tsx diff --git a/src/Explorer/Controls/NotebookGallery/GalleryAndNotebookViewerComponentAdapter.tsx b/src/Explorer/Controls/NotebookGallery/GalleryAndNotebookViewerComponentAdapter.tsx deleted file mode 100644 index dde238c1e..000000000 --- a/src/Explorer/Controls/NotebookGallery/GalleryAndNotebookViewerComponentAdapter.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import ko from "knockout"; -import * as React from "react"; -import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; -import { - GalleryAndNotebookViewerComponentProps, - GalleryAndNotebookViewerComponent, -} from "./GalleryAndNotebookViewerComponent"; - -export class GalleryAndNotebookViewerComponentAdapter implements ReactAdapter { - private key: string; - public parameters: ko.Observable; - - constructor(private props: GalleryAndNotebookViewerComponentProps) { - this.reset(); - this.parameters = ko.observable(Date.now()); - } - - public renderComponent(): JSX.Element { - return ; - } - - public reset(): void { - this.key = `GalleryAndNotebookViewerComponent-${Date.now()}`; - } - - public triggerRender(): void { - window.requestAnimationFrame(() => this.parameters(Date.now())); - } -} diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 041d1b61c..5b3fcebc3 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -73,7 +73,6 @@ import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel"; import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; -import type { GalleryTabOptions } from "./Tabs/GalleryTab"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import QueryTablesTab from "./Tabs/QueryTablesTab"; import { TabsManager } from "./Tabs/TabsManager"; @@ -1916,33 +1915,35 @@ export default class Explorer { const title = "Gallery"; const hashLocation = "gallery"; const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default; - - const galleryTabOptions: GalleryTabOptions = { - account: userContext.databaseAccount, - container: this, - junoClient: this.notebookManager?.junoClient, - selectedTab: selectedTab || GalleryTabKind.PublicGallery, - notebookUrl, - galleryItem, - isFavorite, - tabKind: ViewModels.CollectionTabKind.Gallery, - title: title, - tabPath: title, - hashLocation: hashLocation, - onUpdateTabsButtons: this.onUpdateTabsButtons, - isTabsContentExpanded: ko.observable(true), - onLoadStartKey: null, - }; - const galleryTab = this.tabsManager .getTabs(ViewModels.CollectionTabKind.Gallery) .find((tab) => tab.hashLocation() == hashLocation); if (galleryTab instanceof GalleryTab) { this.tabsManager.activateTab(galleryTab); - galleryTab.reset(galleryTabOptions); } else { - this.tabsManager.activateNewTab(new GalleryTab(galleryTabOptions)); + this.tabsManager.activateNewTab( + new GalleryTab( + { + tabKind: ViewModels.CollectionTabKind.Gallery, + title: title, + tabPath: title, + hashLocation: hashLocation, + onUpdateTabsButtons: this.onUpdateTabsButtons, + onLoadStartKey: null, + isTabsContentExpanded: ko.observable(true), + }, + { + account: userContext.databaseAccount, + container: this, + junoClient: this.notebookManager?.junoClient, + selectedTab: selectedTab || GalleryTabKind.PublicGallery, + notebookUrl, + galleryItem, + isFavorite, + } + ) + ); } } diff --git a/src/Explorer/Tabs/GalleryTab.tsx b/src/Explorer/Tabs/GalleryTab.tsx index 496b13cf8..32e809eeb 100644 --- a/src/Explorer/Tabs/GalleryTab.tsx +++ b/src/Explorer/Tabs/GalleryTab.tsx @@ -1,13 +1,14 @@ -import { DatabaseAccount } from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; -import { IGalleryItem, JunoClient } from "../../Juno/JunoClient"; -import { GalleryAndNotebookViewerComponentProps } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponent"; -import { GalleryAndNotebookViewerComponentAdapter } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponentAdapter"; -import { GalleryTab as GalleryViewerTab, SortBy } from "../Controls/NotebookGallery/GalleryViewerComponent"; -import Explorer from "../Explorer"; +import React from "react"; +import type { DatabaseAccount } from "../../Contracts/DataModels"; +import type { TabOptions } from "../../Contracts/ViewModels"; +import type { IGalleryItem, JunoClient } from "../../Juno/JunoClient"; +import { GalleryAndNotebookViewerComponent as GalleryViewer } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponent"; +import type { GalleryTab as GalleryViewerTab } from "../Controls/NotebookGallery/GalleryViewerComponent"; +import { SortBy } from "../Controls/NotebookGallery/GalleryViewerComponent"; +import type Explorer from "../Explorer"; import TabsBase from "./TabsBase"; -export interface GalleryTabOptions extends ViewModels.TabOptions { +interface Props { account: DatabaseAccount; container: Explorer; junoClient: JunoClient; @@ -17,51 +18,16 @@ export interface GalleryTabOptions extends ViewModels.TabOptions { isFavorite?: boolean; } -/** - * Notebook gallery tab - */ export default class GalleryTab extends TabsBase { - public readonly html = '
'; - private container: Explorer; - private galleryAndNotebookViewerComponentProps: GalleryAndNotebookViewerComponentProps; - public galleryAndNotebookViewerComponentAdapter: GalleryAndNotebookViewerComponentAdapter; - - constructor(options: GalleryTabOptions) { + constructor(options: TabOptions, private props: Props) { super(options); - this.container = options.container; - - this.galleryAndNotebookViewerComponentProps = { - container: options.container, - junoClient: options.junoClient, - notebookUrl: options.notebookUrl, - galleryItem: options.galleryItem, - isFavorite: options.isFavorite, - selectedTab: options.selectedTab, - sortBy: SortBy.MostRecent, - searchText: undefined, - }; - this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter( - this.galleryAndNotebookViewerComponentProps - ); } - public reset(options: GalleryTabOptions) { - this.container = options.container; - - this.galleryAndNotebookViewerComponentProps.container = options.container; - this.galleryAndNotebookViewerComponentProps.junoClient = options.junoClient; - this.galleryAndNotebookViewerComponentProps.notebookUrl = options.notebookUrl; - this.galleryAndNotebookViewerComponentProps.galleryItem = options.galleryItem; - this.galleryAndNotebookViewerComponentProps.isFavorite = options.isFavorite; - this.galleryAndNotebookViewerComponentProps.selectedTab = options.selectedTab; - this.galleryAndNotebookViewerComponentProps.sortBy = SortBy.MostViewed; - this.galleryAndNotebookViewerComponentProps.searchText = undefined; - - this.galleryAndNotebookViewerComponentAdapter.reset(); - this.galleryAndNotebookViewerComponentAdapter.triggerRender(); + public render() { + return ; } public getContainer(): Explorer { - return this.container; + return this.props.container; } } From 5cf16d01b517b1ca5078420fd31d29dce14fc587 Mon Sep 17 00:00:00 2001 From: Jordi Bunster Date: Tue, 27 Apr 2021 08:14:21 -0700 Subject: [PATCH 3/8] use ES6 Map if we can (#602) --- .eslintignore | 4 - src/Common/ArrayHashMap.ts | 71 ++---------- src/Common/HashMap.test.ts | 70 ------------ src/Common/HashMap.ts | 45 -------- src/Common/ObjectCache.test.ts | 2 +- src/Common/ObjectCache.ts | 59 +++------- .../SettingsComponent.test.tsx.snap | 104 ++++-------------- .../GraphExplorerComponent/D3ForceGraph.ts | 11 +- .../GraphExplorerComponent/EdgeInfoCache.ts | 11 +- .../GremlinClient.test.ts | 10 +- .../GraphExplorerComponent/GremlinClient.ts | 9 +- .../Panes/CassandraAddCollectionPane.ts | 5 +- .../__snapshots__/SettingsPane.test.tsx.snap | 52 ++------- .../StringInputPane.test.tsx.snap | 52 ++------- .../UploadItemsPane.test.tsx.snap | 26 +---- ...eteDatabaseConfirmationPanel.test.tsx.snap | 26 +---- src/Explorer/Tabs/MongoShellTab.ts | 5 +- src/Explorer/Tabs/QueryTab.ts | 17 ++- src/Explorer/Tree/ResourceTreeAdapter.tsx | 4 +- tsconfig.strict.json | 1 - 20 files changed, 106 insertions(+), 478 deletions(-) delete mode 100644 src/Common/HashMap.test.ts delete mode 100644 src/Common/HashMap.ts diff --git a/.eslintignore b/.eslintignore index af7128111..2e9564de7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,7 +5,6 @@ src/Api/Apis.ts src/AuthType.ts src/Bindings/BindingHandlersRegisterer.ts src/Bindings/ReactBindingHandler.ts -src/Common/ArrayHashMap.ts src/Common/Constants.ts src/Common/CosmosClient.test.ts src/Common/CosmosClient.ts @@ -13,15 +12,12 @@ src/Common/DataAccessUtilityBase.test.ts src/Common/DataAccessUtilityBase.ts src/Common/EditableUtility.ts src/Common/HashMap.test.ts -src/Common/HashMap.ts src/Common/Logger.test.ts src/Common/MessageHandler.test.ts src/Common/MessageHandler.ts src/Common/MongoProxyClient.test.ts src/Common/MongoUtility.ts src/Common/NotificationsClientBase.ts -src/Common/ObjectCache.test.ts -src/Common/ObjectCache.ts src/Common/QueriesClient.ts src/Common/Splitter.ts src/Config.ts diff --git a/src/Common/ArrayHashMap.ts b/src/Common/ArrayHashMap.ts index eef791373..8b1600d6c 100644 --- a/src/Common/ArrayHashMap.ts +++ b/src/Common/ArrayHashMap.ts @@ -1,49 +1,9 @@ -import { HashMap } from "./HashMap"; - /** * Hash map of arrays which allows to: * - push an item by key: add to array and create array if needed * - remove item by key: remove from array and delete array if needed */ - -export class ArrayHashMap { - private store: HashMap; - - constructor() { - this.store = new HashMap(); - } - - public has(key: string): boolean { - return this.store.has(key); - } - - public get(key: string): T[] { - return this.store.get(key); - } - - public size(): number { - return this.store.size(); - } - - public clear(): void { - this.store.clear(); - } - - public keys(): string[] { - return this.store.keys(); - } - - public delete(key: string): boolean { - return this.store.delete(key); - } - - public forEach(key: string, iteratorFct: (value: T) => void) { - const values = this.store.get(key); - if (values) { - values.forEach((value) => iteratorFct(value)); - } - } - +export class ArrayHashMap extends Map { /** * Insert item into array. * If no array, create one. @@ -52,16 +12,8 @@ export class ArrayHashMap { * @param item */ public push(key: string, item: T): void { - let itemsArray: T[] = this.store.get(key); - if (!itemsArray) { - itemsArray = [item]; - this.store.set(key, itemsArray); - return; - } - - if (itemsArray.indexOf(item) === -1) { - itemsArray.push(item); - } + const array = this.get(key); + array ? array.includes(item) || array.push(item) : this.set(key, [item]); } /** @@ -70,18 +22,11 @@ export class ArrayHashMap { * @param key * @param itemToRemove */ - public remove(key: string, itemToRemove: T) { - if (!this.store.has(key)) { - return; - } - - const itemsArray = this.store.get(key); - const index = itemsArray.indexOf(itemToRemove); - if (index >= 0) { - itemsArray.splice(index, 1); - if (itemsArray.length === 0) { - this.store.delete(key); - } + public remove(key: string, itemToRemove: T): void { + const array = this.get(key); + if (array) { + const remaining = array.filter((item) => item !== itemToRemove); + remaining.length ? this.set(key, remaining) : this.delete(key); } } } diff --git a/src/Common/HashMap.test.ts b/src/Common/HashMap.test.ts deleted file mode 100644 index b3bdaa225..000000000 --- a/src/Common/HashMap.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { HashMap } from "./HashMap"; - -describe("HashMap", () => { - it("should test if key/val exists", () => { - const map = new HashMap(); - map.set("a", 123); - - expect(map.has("a")).toBe(true); - expect(map.has("b")).toBe(false); - }); - - it("should get object back", () => { - const map = new HashMap(); - map.set("a", "123"); - map.set("a", "456"); - - expect(map.get("a")).toBe("456"); - expect(map.get("a")).not.toBe("123"); - }); - - it("should return the right size", () => { - const map = new HashMap(); - map.set("a", "123"); - map.set("b", "456"); - - expect(map.size()).toBe(2); - }); - - it("should be iterable", () => { - const map = new HashMap(); - map.set("a", 1); - map.set("b", 10); - map.set("c", 100); - map.set("d", 1000); - - let i = 0; - map.forEach((key: string, value: number) => { - i += value; - }); - expect(i).toBe(1111); - }); - - it("should be deleted", () => { - const map = new HashMap(); - map.set("a", 1); - map.set("b", 10); - - expect(map.delete("a")).toBe(true); - expect(map.delete("c")).toBe(false); - expect(map.has("a")).toBe(false); - expect(map.has("b")).toBe(true); - }); - - it("should clear", () => { - const map = new HashMap(); - map.set("a", 1); - map.clear(); - expect(map.size()).toBe(0); - expect(map.has("a")).toBe(false); - }); - - it("should return all keys", () => { - const map = new HashMap(); - map.set("a", 1); - map.set("b", 1); - expect(map.keys()).toEqual(["a", "b"]); - map.clear(); - expect(map.keys().length).toBe(0); - }); -}); diff --git a/src/Common/HashMap.ts b/src/Common/HashMap.ts deleted file mode 100644 index 0a55b08c5..000000000 --- a/src/Common/HashMap.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Simple hashmap implementation that doesn't rely on ES6 Map nor polyfills - */ -export class HashMap { - constructor(private container: { [key: string]: T } = {}) {} - - public has(key: string): boolean { - return this.container.hasOwnProperty(key); - } - - public set(key: string, value: T): void { - this.container[key] = value; - } - - public get(key: string): T { - return this.container[key]; - } - - public size(): number { - return Object.keys(this.container).length; - } - - public delete(key: string): boolean { - if (this.has(key)) { - delete this.container[key]; - return true; - } - - return false; - } - - public clear(): void { - this.container = {}; - } - - public keys(): string[] { - return Object.keys(this.container); - } - - public forEach(iteratorFct: (key: string, value: T) => void) { - for (const k in this.container) { - iteratorFct(k, this.container[k]); - } - } -} diff --git a/src/Common/ObjectCache.test.ts b/src/Common/ObjectCache.test.ts index 3a9f8f17a..7f6b5f7a7 100644 --- a/src/Common/ObjectCache.test.ts +++ b/src/Common/ObjectCache.test.ts @@ -7,7 +7,7 @@ describe("Object cache", () => { cache.set("b", 2); cache.set("c", 3); cache.set("d", 4); - expect(cache.size()).toBe(2); + expect(cache.size).toBe(2); }); it("should remove first added element to keep size at limit", () => { diff --git a/src/Common/ObjectCache.ts b/src/Common/ObjectCache.ts index 9149aba9b..0c6326087 100644 --- a/src/Common/ObjectCache.ts +++ b/src/Common/ObjectCache.ts @@ -1,56 +1,27 @@ -import { HashMap } from "./HashMap"; - -export class ObjectCache extends HashMap { - private keyQueue: string[]; // Last touched key FIFO to purge cache if too big. - private maxNbElements: number; - - public constructor(maxNbElements: number) { +export class ObjectCache extends Map { + constructor(private limit: number) { super(); - this.keyQueue = []; - this.maxNbElements = maxNbElements; - this.clear(); } - public clear(): void { - super.clear(); - this.keyQueue = []; + public get(key: string): T | undefined { + return this.touch(key); } - public get(key: string): T { - this.markKeyAsTouched(key); - return super.get(key); - } - - public set(key: string, value: T): void { - super.set(key, value); - - this.markKeyAsTouched(key); - - if (super.size() > this.maxNbElements && key !== this.keyQueue[0]) { - this.reduceCacheSize(); + public set(key: string, value: T): this { + if (this.size === this.limit) { + this.delete(this.keys().next().value); } + + return this.touch(key, value), this; } - /** - * Invalidate elements to keep the total number below the limit - */ - private reduceCacheSize(): void { - // remove a key - const oldKey = this.keyQueue.shift(); - if (oldKey) { - super.delete(oldKey); + private touch(key: string, value = super.get(key)) { + // Map keeps (re) insertion order according to ES6 spec + if (value) { + this.delete(key); + super.set(key, value); } - } - /** - * Bubble up this key as new. - * @param key - */ - private markKeyAsTouched(key: string) { - const n = this.keyQueue.indexOf(key); - if (n > -1) { - this.keyQueue.splice(n, 1); - } - this.keyQueue.push(key); + return value; } } diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 81c72cf44..5018a7821 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -199,9 +199,7 @@ exports[`SettingsComponent renders 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -378,9 +376,7 @@ exports[`SettingsComponent renders 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -494,21 +490,9 @@ exports[`SettingsComponent renders 1`] = ` "resourceTree": ResourceTreeAdapter { "container": [Circular], "copyNotebook": [Function], - "databaseCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsDatabaseIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, + "databaseCollectionIdMap": Map {}, + "koSubsCollectionIdMap": Map {}, + "koSubsDatabaseIdMap": Map {}, "parameters": [Function], }, "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { @@ -731,9 +715,7 @@ exports[`SettingsComponent renders 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -910,9 +892,7 @@ exports[`SettingsComponent renders 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -1026,21 +1006,9 @@ exports[`SettingsComponent renders 1`] = ` "resourceTree": ResourceTreeAdapter { "container": [Circular], "copyNotebook": [Function], - "databaseCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsDatabaseIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, + "databaseCollectionIdMap": Map {}, + "koSubsCollectionIdMap": Map {}, + "koSubsDatabaseIdMap": Map {}, "parameters": [Function], }, "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { @@ -1276,9 +1244,7 @@ exports[`SettingsComponent renders 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -1455,9 +1421,7 @@ exports[`SettingsComponent renders 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -1571,21 +1535,9 @@ exports[`SettingsComponent renders 1`] = ` "resourceTree": ResourceTreeAdapter { "container": [Circular], "copyNotebook": [Function], - "databaseCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsDatabaseIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, + "databaseCollectionIdMap": Map {}, + "koSubsCollectionIdMap": Map {}, + "koSubsDatabaseIdMap": Map {}, "parameters": [Function], }, "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { @@ -1808,9 +1760,7 @@ exports[`SettingsComponent renders 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -1987,9 +1937,7 @@ exports[`SettingsComponent renders 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -2103,21 +2051,9 @@ exports[`SettingsComponent renders 1`] = ` "resourceTree": ResourceTreeAdapter { "container": [Circular], "copyNotebook": [Function], - "databaseCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsDatabaseIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, + "databaseCollectionIdMap": Map {}, + "koSubsCollectionIdMap": Map {}, + "koSubsDatabaseIdMap": Map {}, "parameters": [Function], }, "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { diff --git a/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.ts b/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.ts index 31a01f0a3..393e92b84 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.ts @@ -11,7 +11,6 @@ import * as ko from "knockout"; import Q from "q"; import _ from "underscore"; import * as Constants from "../../../Common/Constants"; -import { HashMap } from "../../../Common/HashMap"; import { NeighborType } from "../../../Contracts/ViewModels"; import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import { GraphConfig } from "../../Tabs/GraphTab"; @@ -195,8 +194,8 @@ export class D3ForceGraph implements GraphRenderer { * Count edges and store in a hashmap: vertex id <--> number of links * @param linkSelection */ - public static countEdges(links: D3Link[]): HashMap { - const countMap = new HashMap(); + public static countEdges(links: D3Link[]): Map { + const countMap = new Map(); links.forEach((l: D3Link) => { let val = countMap.get(l.inV) || 0; val += 1; @@ -407,7 +406,7 @@ export class D3ForceGraph implements GraphRenderer { const rootId = graph.findRootNodeId(); // Remember nodes current position - const posMap = new HashMap(); + const posMap = new Map(); this.simulation.nodes().forEach((d: D3Node) => { if (d.x == undefined || d.y == undefined) { return; @@ -501,8 +500,8 @@ export class D3ForceGraph implements GraphRenderer { if (!nodes || nodes.length === 0) { return; } - const nodeFinalPositionMap = new HashMap(); + const nodeFinalPositionMap = new Map(); const viewCenter = this.viewCenter; const nonFixedNodes = _.filter(nodes, (node: D3Node) => { return !node._isFixedPosition && node.x === viewCenter.x && node.y === viewCenter.y; @@ -559,7 +558,7 @@ export class D3ForceGraph implements GraphRenderer { newNodes.selectAll(".loadmore").attr("visibility", "hidden").transition().delay(600).attr("visibility", "visible"); } - private restartSimulation(graph: GraphData, posMap: HashMap) { + private restartSimulation(graph: GraphData, posMap: Map) { if (!graph) { return; } diff --git a/src/Explorer/Graph/GraphExplorerComponent/EdgeInfoCache.ts b/src/Explorer/Graph/GraphExplorerComponent/EdgeInfoCache.ts index 681682102..10e255eb9 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/EdgeInfoCache.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/EdgeInfoCache.ts @@ -1,5 +1,5 @@ import { ObjectCache } from "../../../Common/ObjectCache"; -import { GremlinVertex, GraphData } from "./GraphData"; +import { GraphData, GremlinVertex } from "./GraphData"; /** * Remember vertex edge information @@ -10,9 +10,8 @@ export class EdgeInfoCache extends ObjectCache { * @param vertex */ public addVertex(vertex: GremlinVertex): void { - let v: GremlinVertex; - if (super.has(vertex.id)) { - v = super.get(vertex.id); + let v = super.get(vertex.id); + if (super.has(vertex.id) && v) { GraphData.addEdgeInfoToVertex(v, vertex); v._outEdgeIds = vertex._outEdgeIds; v._inEdgeIds = vertex._inEdgeIds; @@ -29,8 +28,8 @@ export class EdgeInfoCache extends ObjectCache { * @param id */ public mergeEdgeInfo(target: GremlinVertex): void { - if (super.has(target.id)) { - const cachedVertex = super.get(target.id); + const cachedVertex = super.get(target.id); + if (super.has(target.id) && cachedVertex) { GraphData.addEdgeInfoToVertex(target, cachedVertex); target._outEdgeIds = cachedVertex._outEdgeIds; target._inEdgeIds = cachedVertex._inEdgeIds; diff --git a/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts b/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts index 86c918521..5f1619ab7 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts @@ -1,7 +1,7 @@ import * as sinon from "sinon"; -import { GremlinClient, GremlinClientParameters } from "./GremlinClient"; -import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as Logger from "../../../Common/Logger"; +import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; +import { GremlinClient, GremlinClientParameters } from "./GremlinClient"; describe("Gremlin Client", () => { const emptyParams: GremlinClientParameters = { @@ -70,7 +70,7 @@ describe("Gremlin Client", () => { gremlinClient.execute("fake query"); gremlinClient.execute("fake query"); gremlinClient.execute("fake query"); - expect(gremlinClient.pendingResults.size()).toBe(3); + expect(gremlinClient.pendingResults.size).toBe(3); }); it("should clean up pending request ids after success", async () => { @@ -89,7 +89,7 @@ describe("Gremlin Client", () => { return requestId; }); await gremlinClient.execute("fake query"); - expect(gremlinClient.pendingResults.size()).toBe(0); + expect(gremlinClient.pendingResults.size).toBe(0); }); it("should log and display error out on unknown requestId", () => { @@ -247,7 +247,7 @@ describe("Gremlin Client", () => { sinon.stub(gremlinClient.client, "executeGremlinQuery").callsFake((query: string): string => requestId); gremlinClient.execute("fake query").finally(() => { try { - expect(gremlinClient.pendingResults.size()).toBe(0); + expect(gremlinClient.pendingResults.size).toBe(0); done(); } catch (e) { done(e); diff --git a/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts b/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts index ee022b873..575412675 100644 --- a/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts +++ b/src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts @@ -4,7 +4,6 @@ import * as Q from "q"; import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils"; -import { HashMap } from "../../../Common/HashMap"; import { logConsoleInfo } from "../../../Utils/NotificationConsoleUtils"; import { GremlinSimpleClient, Result } from "./GremlinSimpleClient"; @@ -30,7 +29,7 @@ interface PendingResultData { export class GremlinClient { public client: GremlinSimpleClient; - public pendingResults: HashMap; // public for testing purposes + public pendingResults: Map; // public for testing purposes private maxResultSize: number; private static readonly PENDING_REQUEST_TIMEOUT_MS = 6 /* minutes */ * 60 /* seconds */ * 1000 /* ms */; private static readonly TIMEOUT_ERROR_MSG = `Pending request timed out (${GremlinClient.PENDING_REQUEST_TIMEOUT_MS} ms)`; @@ -38,7 +37,7 @@ export class GremlinClient { public initialize(params: GremlinClientParameters) { this.destroy(); - this.pendingResults = new HashMap(); + this.pendingResults = new Map(); this.maxResultSize = params.maxResultSize; this.client = new GremlinSimpleClient({ @@ -68,9 +67,9 @@ export class GremlinClient { // Fail all pending requests if no request id (fatal) if (!requestId) { - this.pendingResults.keys().forEach((reqId: string) => { + for (const reqId of this.pendingResults.keys()) { this.abortPendingRequest(reqId, errorMessage, null); - }); + } } } else { this.abortPendingRequest(requestId, errorMessage, result.requestCharge); diff --git a/src/Explorer/Panes/CassandraAddCollectionPane.ts b/src/Explorer/Panes/CassandraAddCollectionPane.ts index 10d644723..d3a2d2098 100644 --- a/src/Explorer/Panes/CassandraAddCollectionPane.ts +++ b/src/Explorer/Panes/CassandraAddCollectionPane.ts @@ -2,7 +2,6 @@ import * as ko from "knockout"; import * as _ from "underscore"; import * as Constants from "../../Common/Constants"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import { HashMap } from "../../Common/HashMap"; import { configContext, Platform } from "../../ConfigContext"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; @@ -51,7 +50,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { public ruToolTipText: ko.Computed; public canConfigureThroughput: ko.PureComputed; - private keyspaceOffers: HashMap; + private keyspaceOffers: Map; constructor(options: ViewModels.PaneOptions) { super(options); @@ -60,7 +59,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase { this.keyspaceCreateNew = ko.observable(true); this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText()); this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled()); - this.keyspaceOffers = new HashMap(); + this.keyspaceOffers = new Map(); this.keyspaceIds = ko.observableArray(); this.keyspaceHasSharedOffer = ko.observable(false); this.keyspaceThroughput = ko.observable(); diff --git a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap index 8344457ad..5915ca240 100644 --- a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap +++ b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap @@ -175,9 +175,7 @@ exports[`Settings Pane should render Default properly 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -354,9 +352,7 @@ exports[`Settings Pane should render Default properly 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -470,21 +466,9 @@ exports[`Settings Pane should render Default properly 1`] = ` "resourceTree": ResourceTreeAdapter { "container": [Circular], "copyNotebook": [Function], - "databaseCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsDatabaseIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, + "databaseCollectionIdMap": Map {}, + "koSubsCollectionIdMap": Map {}, + "koSubsDatabaseIdMap": Map {}, "parameters": [Function], }, "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { @@ -830,9 +814,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -1009,9 +991,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -1125,21 +1105,9 @@ exports[`Settings Pane should render Gremlin properly 1`] = ` "resourceTree": ResourceTreeAdapter { "container": [Circular], "copyNotebook": [Function], - "databaseCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsDatabaseIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, + "databaseCollectionIdMap": Map {}, + "koSubsCollectionIdMap": Map {}, + "koSubsDatabaseIdMap": Map {}, "parameters": [Function], }, "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index eb4b1bfa1..e2ddc5c49 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -178,9 +178,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -357,9 +355,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -473,21 +469,9 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "resourceTree": ResourceTreeAdapter { "container": [Circular], "copyNotebook": [Function], - "databaseCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsDatabaseIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, + "databaseCollectionIdMap": Map {}, + "koSubsCollectionIdMap": Map {}, + "koSubsDatabaseIdMap": Map {}, "parameters": [Function], }, "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { @@ -709,9 +693,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -888,9 +870,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -1004,21 +984,9 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "resourceTree": ResourceTreeAdapter { "container": [Circular], "copyNotebook": [Function], - "databaseCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsDatabaseIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, + "databaseCollectionIdMap": Map {}, + "koSubsCollectionIdMap": Map {}, + "koSubsDatabaseIdMap": Map {}, "parameters": [Function], }, "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { diff --git a/src/Explorer/Panes/UploadItemsPane/__snapshots__/UploadItemsPane.test.tsx.snap b/src/Explorer/Panes/UploadItemsPane/__snapshots__/UploadItemsPane.test.tsx.snap index 8d4b6203c..9bb261343 100644 --- a/src/Explorer/Panes/UploadItemsPane/__snapshots__/UploadItemsPane.test.tsx.snap +++ b/src/Explorer/Panes/UploadItemsPane/__snapshots__/UploadItemsPane.test.tsx.snap @@ -175,9 +175,7 @@ exports[`Upload Items Pane should render Default properly 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -354,9 +352,7 @@ exports[`Upload Items Pane should render Default properly 1`] = ` "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -470,21 +466,9 @@ exports[`Upload Items Pane should render Default properly 1`] = ` "resourceTree": ResourceTreeAdapter { "container": [Circular], "copyNotebook": [Function], - "databaseCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsDatabaseIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, + "databaseCollectionIdMap": Map {}, + "koSubsCollectionIdMap": Map {}, + "koSubsDatabaseIdMap": Map {}, "parameters": [Function], }, "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { diff --git a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap index ac01075ef..e4d8030b5 100644 --- a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap @@ -176,9 +176,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -355,9 +353,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database "keyspaceHasSharedOffer": [Function], "keyspaceId": [Function], "keyspaceIds": [Function], - "keyspaceOffers": HashMap { - "container": Object {}, - }, + "keyspaceOffers": Map {}, "keyspaceThroughput": [Function], "maxThroughputRU": [Function], "minThroughputRU": [Function], @@ -475,21 +471,9 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database "resourceTree": ResourceTreeAdapter { "container": [Circular], "copyNotebook": [Function], - "databaseCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsCollectionIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, - "koSubsDatabaseIdMap": ArrayHashMap { - "store": HashMap { - "container": Object {}, - }, - }, + "databaseCollectionIdMap": Map {}, + "koSubsCollectionIdMap": Map {}, + "koSubsDatabaseIdMap": Map {}, "parameters": [Function], }, "resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken { diff --git a/src/Explorer/Tabs/MongoShellTab.ts b/src/Explorer/Tabs/MongoShellTab.ts index 4ae74947f..15cceff5b 100644 --- a/src/Explorer/Tabs/MongoShellTab.ts +++ b/src/Explorer/Tabs/MongoShellTab.ts @@ -1,6 +1,5 @@ import * as ko from "knockout"; import * as Constants from "../../Common/Constants"; -import { HashMap } from "../../Common/HashMap"; import { configContext, Platform } from "../../ConfigContext"; import * as ViewModels from "../../Contracts/ViewModels"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; @@ -17,11 +16,11 @@ export default class MongoShellTab extends TabsBase { public url: ko.Computed; private _container: Explorer; private _runtimeEndpoint: string; - private _logTraces: HashMap; + private _logTraces: Map; constructor(options: ViewModels.TabOptions) { super(options); - this._logTraces = new HashMap(); + this._logTraces = new Map(); this._container = options.collection.container; this.url = ko.computed(() => { const account = userContext.databaseAccount; diff --git a/src/Explorer/Tabs/QueryTab.ts b/src/Explorer/Tabs/QueryTab.ts index 976befa21..bf85f257c 100644 --- a/src/Explorer/Tabs/QueryTab.ts +++ b/src/Explorer/Tabs/QueryTab.ts @@ -5,7 +5,6 @@ import * as Constants from "../../Common/Constants"; import { queryDocuments } from "../../Common/dataAccess/queryDocuments"; import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; -import { HashMap } from "../../Common/HashMap"; import * as HeadersUtility from "../../Common/HeadersUtility"; import { MinimalQueryIterator } from "../../Common/IteratorUtilities"; import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter"; @@ -47,7 +46,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem public splitter: Splitter; public isPreferredApiMongoDB: boolean; - public queryMetrics: ko.Observable>; + public queryMetrics: ko.Observable>; public aggregatedQueryMetrics: ko.Observable; public activityId: ko.Observable; public roundTrips: ko.Observable; @@ -91,10 +90,8 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem this.isPreferredApiMongoDB = false; this.aggregatedQueryMetrics = ko.observable(); this._resetAggregateQueryMetrics(); - this.queryMetrics = ko.observable>(new HashMap()); - this.queryMetrics.subscribe((metrics: HashMap) => - this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics)) - ); + this.queryMetrics = ko.observable>(new Map()); + this.queryMetrics.subscribe((metrics) => this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics))); this.isQueryMetricsEnabled = ko.computed(() => { return userContext.apiType === "SQL" || false; }); @@ -364,13 +361,13 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem this.queryMetrics.valueHasMutated(); } - private _aggregateQueryMetrics(metricsMap: HashMap): DataModels.QueryMetrics { + private _aggregateQueryMetrics(metricsMap: Map): DataModels.QueryMetrics { if (!metricsMap) { return null; } const aggregatedMetrics: DataModels.QueryMetrics = this.aggregatedQueryMetrics(); - metricsMap.forEach((partitionKeyRangeId: string, queryMetrics: DataModels.QueryMetrics) => { + metricsMap.forEach((queryMetrics) => { if (queryMetrics) { aggregatedMetrics.documentLoadTime = queryMetrics.documentLoadTime && @@ -510,7 +507,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem return null; } - const queryMetrics: HashMap = this.queryMetrics(); + const queryMetrics = this.queryMetrics(); let csvData: string = ""; const columnHeaders: string = [ @@ -528,7 +525,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem "Document write time (ms)", ].join(",") + "\n"; csvData = csvData + columnHeaders; - queryMetrics.forEach((partitionKeyRangeId: string, queryMetric: DataModels.QueryMetrics) => { + queryMetrics.forEach((queryMetric, partitionKeyRangeId) => { const partitionKeyRangeData: string = [ partitionKeyRangeId, diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 184319ac9..870045a18 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -930,7 +930,7 @@ export class ResourceTreeAdapter implements ReactAdapter { } private cleanupDatabasesKoSubs(): void { - this.koSubsDatabaseIdMap.keys().forEach((databaseId: string) => { + for (const databaseId of this.koSubsDatabaseIdMap.keys()) { this.koSubsDatabaseIdMap.get(databaseId).forEach((sub: ko.Subscription) => sub.dispose()); this.koSubsDatabaseIdMap.delete(databaseId); @@ -939,7 +939,7 @@ export class ResourceTreeAdapter implements ReactAdapter { .get(databaseId) .forEach((collectionId: string) => this.cleanupKoSubsForCollection(databaseId, collectionId)); } - }); + } } private cleanupCollectionsKoSubs(databaseId: string, existingCollectionIds: string[]): void { diff --git a/tsconfig.strict.json b/tsconfig.strict.json index 481ce85d6..d69793c12 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -14,7 +14,6 @@ "./src/Common/DeleteFeedback.ts", "./src/Common/DocumentUtility.ts", "./src/Common/EnvironmentUtility.ts", - "./src/Common/HashMap.ts", "./src/Common/HeadersUtility.test.ts", "./src/Common/HeadersUtility.ts", "./src/Common/Logger.ts", From 10c4dd0f1953abd3f858a27d61b1a80b30490d04 Mon Sep 17 00:00:00 2001 From: Jordi Bunster Date: Tue, 27 Apr 2021 09:05:25 -0700 Subject: [PATCH 4/8] This is creating a warning in tests (#731) --- src/Explorer/Tabs/Tabs.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Explorer/Tabs/Tabs.tsx b/src/Explorer/Tabs/Tabs.tsx index 91028c501..9c4de6477 100644 --- a/src/Explorer/Tabs/Tabs.tsx +++ b/src/Explorer/Tabs/Tabs.tsx @@ -12,11 +12,15 @@ export const Tabs = ({ tabs, activeTab }: { tabs: readonly Tab[]; activeTab: Tab
- {...tabs.map((tab) => )} + {tabs.map((tab) => ( + + ))}
From e8b79d62605be17c55da827991bff47a69f60dc6 Mon Sep 17 00:00:00 2001 From: Tanuj Mittal Date: Tue, 27 Apr 2021 12:52:52 -0400 Subject: [PATCH 5/8] Use postRobot to listen for GitHub OAuth messages (#729) --- src/GitHub/GitHubConnector.ts | 25 ++++++++++++++----------- src/GitHub/GitHubOAuthService.test.ts | 27 ++++----------------------- src/GitHub/GitHubOAuthService.ts | 21 +++++++++++---------- 3 files changed, 29 insertions(+), 44 deletions(-) diff --git a/src/GitHub/GitHubConnector.ts b/src/GitHub/GitHubConnector.ts index 866f86f51..5c9cbff37 100644 --- a/src/GitHub/GitHubConnector.ts +++ b/src/GitHub/GitHubConnector.ts @@ -1,3 +1,5 @@ +import postRobot from "post-robot"; + export interface IGitHubConnectorParams { state: string; code: string; @@ -6,25 +8,26 @@ export interface IGitHubConnectorParams { export const GitHubConnectorMsgType = "GitHubConnectorMsgType"; export class GitHubConnector { - public start(params: URLSearchParams, window: Window & typeof globalThis) { - window.postMessage( + public async start(params: URLSearchParams, window: Window & typeof globalThis): Promise { + await postRobot.send( + window, + GitHubConnectorMsgType, { - type: GitHubConnectorMsgType, - data: { - state: params.get("state"), - code: params.get("code"), - } as IGitHubConnectorParams, - }, - window.location.origin + state: params.get("state"), + code: params.get("code"), + } as IGitHubConnectorParams, + { + domain: window.location.origin, + } ); } } var connector = new GitHubConnector(); -window.addEventListener("load", () => { +window.addEventListener("load", async () => { const openerWindow = window.opener; if (openerWindow) { - connector.start(new URLSearchParams(document.location.search), openerWindow); + await connector.start(new URLSearchParams(document.location.search), openerWindow); window.close(); } }); diff --git a/src/GitHub/GitHubOAuthService.test.ts b/src/GitHub/GitHubOAuthService.test.ts index 9a2b092af..43f510c16 100644 --- a/src/GitHub/GitHubOAuthService.test.ts +++ b/src/GitHub/GitHubOAuthService.test.ts @@ -1,12 +1,12 @@ import ko from "knockout"; import { HttpStatusCodes } from "../Common/Constants"; import * as DataModels from "../Contracts/DataModels"; -import { JunoClient } from "../Juno/JunoClient"; -import { GitHubConnector, IGitHubConnectorParams } from "./GitHubConnector"; -import { GitHubOAuthService } from "./GitHubOAuthService"; +import Explorer from "../Explorer/Explorer"; import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import NotebookManager from "../Explorer/Notebook/NotebookManager"; -import Explorer from "../Explorer/Explorer"; +import { JunoClient } from "../Juno/JunoClient"; +import { IGitHubConnectorParams } from "./GitHubConnector"; +import { GitHubOAuthService } from "./GitHubOAuthService"; const sampleDatabaseAccount: DataModels.DatabaseAccount = { id: "id", @@ -85,25 +85,6 @@ describe("GitHubOAuthService", () => { expect(newParams.get("state")).not.toEqual(initialParams.get("state")); }); - it("finishOAuth is called whenever GitHubConnector is started", async () => { - const finishOAuthCallback = jest.fn().mockImplementation(); - gitHubOAuthService.finishOAuth = finishOAuthCallback; - - const params: IGitHubConnectorParams = { - state: "state", - code: "code", - }; - const searchParams = new URLSearchParams({ ...params }); - - const gitHubConnector = new GitHubConnector(); - gitHubConnector.start(searchParams, window); - - // GitHubConnector uses Window.postMessage and there's no good way to know when the message has received - await new Promise((resolve) => setTimeout(resolve, 100)); - - expect(finishOAuthCallback).toBeCalledWith(params); - }); - it("finishOAuth updates token", async () => { const data = { key: "value" }; const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.OK, data }); diff --git a/src/GitHub/GitHubOAuthService.ts b/src/GitHub/GitHubOAuthService.ts index 779d70bd7..6f800e66d 100644 --- a/src/GitHub/GitHubOAuthService.ts +++ b/src/GitHub/GitHubOAuthService.ts @@ -1,24 +1,25 @@ import ko from "knockout"; +import postRobot from "post-robot"; import { HttpStatusCodes } from "../Common/Constants"; import { handleError } from "../Common/ErrorHandlingUtils"; import { configContext } from "../ConfigContext"; import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent"; import { JunoClient } from "../Juno/JunoClient"; -import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation"; import { logConsoleInfo } from "../Utils/NotificationConsoleUtils"; import { GitHubConnectorMsgType, IGitHubConnectorParams } from "./GitHubConnector"; -window.addEventListener("message", (event: MessageEvent) => { - if (isInvalidParentFrameOrigin(event)) { - return; - } - - const msg = event.data; - if (msg.type === GitHubConnectorMsgType) { - const params = msg.data as IGitHubConnectorParams; +postRobot.on( + GitHubConnectorMsgType, + { + domain: window.location.origin, + }, + (event) => { + // Typescript definition for event is wrong. So read params by casting to + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const params = (event as any).data as IGitHubConnectorParams; window.dataExplorer.notebookManager?.gitHubOAuthService.finishOAuth(params); } -}); +); export interface IGitHubOAuthToken { // API properties From 154db1dcd59614caf9fe48992e0e05d4d16567bd Mon Sep 17 00:00:00 2001 From: Jordi Bunster Date: Tue, 27 Apr 2021 15:27:17 -0700 Subject: [PATCH 6/8] Get our previously strict files a bit tighter (#604) Now they meet noUnusedParameters --- src/Bindings/ReactBindingHandler.ts | 8 +------- src/Common/Splitter.ts | 6 ++---- .../Controls/InputTypeahead/InputTypeahead.ts | 6 +++--- .../NotebookComponent/__mocks__/rx-jupyter.ts | 2 +- .../Notebook/NotebookContentClient.ts | 2 +- src/Explorer/Tree/AccessibleVerticalList.ts | 2 +- ...ookWorkspaceResourceProviderMockClients.ts | 20 +++++++++---------- src/Terminal/index.ts | 2 +- tsconfig.strict.json | 3 ++- 9 files changed, 22 insertions(+), 29 deletions(-) diff --git a/src/Bindings/ReactBindingHandler.ts b/src/Bindings/ReactBindingHandler.ts index bbd6c5b76..297b8a242 100644 --- a/src/Bindings/ReactBindingHandler.ts +++ b/src/Bindings/ReactBindingHandler.ts @@ -22,13 +22,7 @@ export interface ReactAdapter { export class Registerer { public static register(): void { ko.bindingHandlers.react = { - init: ( - element: any, - wrappedValueAccessor: () => any, - allBindings?: ko.AllBindings, - viewModel?: any, - bindingContext?: ko.BindingContext - ) => { + init: (element: any, wrappedValueAccessor: () => any) => { const adapter: ReactAdapter = wrappedValueAccessor(); if (adapter.setElement) { diff --git a/src/Common/Splitter.ts b/src/Common/Splitter.ts index 5785aa8ec..cf2518812 100644 --- a/src/Common/Splitter.ts +++ b/src/Common/Splitter.ts @@ -73,7 +73,7 @@ export class Splitter { $(this.leftSide).resizable(splitterOptions); } - private onResizeStart: JQueryUI.ResizableEvent = (e: Event, ui: JQueryUI.ResizableUIParams) => { + private onResizeStart: JQueryUI.ResizableEvent = () => { if (this.direction === SplitterDirection.Vertical) { $(".ui-resizable-helper").height("100%"); } else { @@ -82,9 +82,7 @@ export class Splitter { $("iframe").css("pointer-events", "none"); }; - private onResizeStop: JQueryUI.ResizableEvent = (e: Event, ui: JQueryUI.ResizableUIParams) => { - $("iframe").css("pointer-events", "auto"); - }; + private onResizeStop: JQueryUI.ResizableEvent = () => $("iframe").css("pointer-events", "auto"); public collapseLeft() { this.lastX = $(this.splitter).position().left; diff --git a/src/Explorer/Controls/InputTypeahead/InputTypeahead.ts b/src/Explorer/Controls/InputTypeahead/InputTypeahead.ts index 14d9ca8dd..03a3b0d87 100644 --- a/src/Explorer/Controls/InputTypeahead/InputTypeahead.ts +++ b/src/Explorer/Controls/InputTypeahead/InputTypeahead.ts @@ -128,21 +128,21 @@ class InputTypeaheadViewModel { }, }, callback: { - onClick: (node: any, a: any, item: OnClickItem, event: any) => { + onClick: (_node: unknown, _a: unknown, item: OnClickItem) => { cache.selection = item; if (params.selection) { params.selection(item); } }, - onResult(node: any, query: any, result: any, resultCount: any, resultCountPerGroup: any) { + onResult(_node: unknown, query: any) { cache.inputValue = query; if (params.inputValue) { params.inputValue(query); } }, }, - template: (query: string, item: any) => { + template: (_query: string, item: any) => { // Don't display id if caption *IS* the id return item.caption === item.value ? "{{caption}}" diff --git a/src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts b/src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts index b16feaf4c..312637fce 100644 --- a/src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts +++ b/src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts @@ -11,7 +11,7 @@ let fakeAjaxResponse: AjaxResponse = { responseType: "json", }; export const sessions = { - create: (serverConfig: unknown, body: object): Observable => of(fakeAjaxResponse), + create: (): Observable => of(fakeAjaxResponse), __setResponse: (response: AjaxResponse) => { fakeAjaxResponse = response; }, diff --git a/src/Explorer/Notebook/NotebookContentClient.ts b/src/Explorer/Notebook/NotebookContentClient.ts index 6679a7e1d..e7f14b112 100644 --- a/src/Explorer/Notebook/NotebookContentClient.ts +++ b/src/Explorer/Notebook/NotebookContentClient.ts @@ -220,7 +220,7 @@ export class NotebookContentClient { return this.contentProvider .remove(this.getServerConfig(), path) .toPromise() - .then((xhr: AjaxResponse) => path); + .then(() => path); } /** diff --git a/src/Explorer/Tree/AccessibleVerticalList.ts b/src/Explorer/Tree/AccessibleVerticalList.ts index 9a0ce3943..be701bb93 100644 --- a/src/Explorer/Tree/AccessibleVerticalList.ts +++ b/src/Explorer/Tree/AccessibleVerticalList.ts @@ -23,7 +23,7 @@ export class AccessibleVerticalList { this.onSelect = onSelect; } - public onKeyDown = (source: any, event: KeyboardEvent): boolean => { + public onKeyDown = (_src: unknown, event: KeyboardEvent): boolean => { const targetContainer: Element = event.target; if (this.items == null || this.items.length === 0) { // no items so this should be a noop diff --git a/src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts b/src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts index cdb5705dc..f6416fe1c 100644 --- a/src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts +++ b/src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts @@ -2,11 +2,11 @@ import { IResourceProviderClient } from "../ResourceProvider/IResourceProviderCl import { NotebookWorkspace } from "../Contracts/DataModels"; export class NotebookWorkspaceSettingsProviderClient implements IResourceProviderClient { - public async deleteAsync(url: string, apiVersion: string): Promise { + public async deleteAsync(_url: string, _apiVersion: string): Promise { throw new Error("Not yet implemented"); } - public async postAsync(url: string, body: any, apiVersion: string): Promise { + public async postAsync(_url: string, _body: any, _apiVersion: string): Promise { return Promise.resolve({ notebookServerEndpoint: "http://localhost:8888", username: "", @@ -14,37 +14,37 @@ export class NotebookWorkspaceSettingsProviderClient implements IResourceProvide }); } - public async getAsync(url: string, apiVersion: string): Promise { + public async getAsync(): Promise { throw new Error("Not yet implemented"); } - public async putAsync(url: string, body: any, apiVersion: string): Promise { + public async putAsync(): Promise { throw new Error("Not yet implemented"); } - public async patchAsync(url: string, apiVersion: string): Promise { + public async patchAsync(): Promise { throw new Error("Not yet implemented"); } } export class NotebookWorkspaceResourceProviderClient implements IResourceProviderClient { - public async deleteAsync(url: string, apiVersion: string): Promise { + public async deleteAsync(): Promise { throw new Error("Not yet implemented"); } - public async postAsync(url: string, body: any, apiVersion: string): Promise { + public async postAsync(): Promise { throw new Error("Not yet implemented"); } - public async getAsync(url: string, apiVersion: string): Promise { + public async getAsync(): Promise { throw new Error("Not yet implemented"); } - public async putAsync(url: string, body: any, apiVersion: string): Promise { + public async putAsync(): Promise { throw new Error("Not yet implemented"); } - public async patchAsync(url: string, body: any, apiVersion: string): Promise { + public async patchAsync(): Promise { throw new Error("Not yet implemented"); } } diff --git a/src/Terminal/index.ts b/src/Terminal/index.ts index d9cd73898..eb272a1a5 100644 --- a/src/Terminal/index.ts +++ b/src/Terminal/index.ts @@ -9,7 +9,7 @@ import { HttpHeaders, TerminalQueryParams } from "../Common/Constants"; const getUrlVars = (): { [key: string]: string } => { const vars: { [key: string]: string } = {}; - window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, (m, key, value): string => { + window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, (_m, key, value): string => { vars[key] = decodeURIComponent(value); return value; }); diff --git a/tsconfig.strict.json b/tsconfig.strict.json index d69793c12..6d5634918 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -4,7 +4,8 @@ "noEmit": true, "strictNullChecks": true, "strict": true, - "noUnusedLocals": true + "noUnusedLocals": true, + "noUnusedParameters": true }, "files": [ "./src/AuthType.ts", From 8f3cb7282b6749903280d1b7070d3cc3e6ac1912 Mon Sep 17 00:00:00 2001 From: Hardikkumar Nai <80053762+hardiknai-techm@users.noreply.github.com> Date: Wed, 28 Apr 2021 06:10:03 +0530 Subject: [PATCH 7/8] Migrate Publish Notebook Pane to React (#641) Co-authored-by: Steve Faulkner --- .../Cards/GalleryCardComponent.tsx | 314 ++++++++--------- .../CodeOfConductComponent.tsx | 123 ------- .../__snapshots__/index.test.tsx.snap} | 0 .../index.test.tsx} | 8 +- .../CodeOfConductComponent/index.tsx | 110 ++++++ .../GalleryViewerComponent.tsx | 3 +- src/Explorer/Explorer.tsx | 3 - src/Explorer/Notebook/NotebookManager.tsx | 22 +- .../PublishNotebookPane.test.tsx} | 9 +- .../PublishNotebookPane.tsx | 205 +++++++++++ .../PublishNotebookPaneComponent.tsx | 297 ++++++++++++++++ .../PublishNotebookPane.test.tsx.snap} | 8 +- .../styled.less} | 0 .../Panes/PublishNotebookPaneAdapter.tsx | 244 ------------- .../Panes/PublishNotebookPaneComponent.tsx | 326 ------------------ src/Main.tsx | 3 - src/Utils/UserUtils.ts | 12 +- 17 files changed, 802 insertions(+), 885 deletions(-) delete mode 100644 src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx rename src/Explorer/Controls/NotebookGallery/{__snapshots__/CodeOfConductComponent.test.tsx.snap => CodeOfConductComponent/__snapshots__/index.test.tsx.snap} (100%) rename src/Explorer/Controls/NotebookGallery/{CodeOfConductComponent.test.tsx => CodeOfConductComponent/index.test.tsx} (84%) create mode 100644 src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.tsx rename src/Explorer/Panes/{PublishNotebookPaneComponent.test.tsx => PublishNotebookPane/PublishNotebookPane.test.tsx} (78%) create mode 100644 src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.tsx create mode 100644 src/Explorer/Panes/PublishNotebookPane/PublishNotebookPaneComponent.tsx rename src/Explorer/Panes/{__snapshots__/PublishNotebookPaneComponent.test.tsx.snap => PublishNotebookPane/__snapshots__/PublishNotebookPane.test.tsx.snap} (92%) rename src/Explorer/Panes/{PublishNotebookPaneComponent.less => PublishNotebookPane/styled.less} (100%) delete mode 100644 src/Explorer/Panes/PublishNotebookPaneAdapter.tsx delete mode 100644 src/Explorer/Panes/PublishNotebookPaneComponent.tsx diff --git a/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.tsx b/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.tsx index 2f81cbb2b..7930f2ed6 100644 --- a/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.tsx +++ b/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.tsx @@ -1,25 +1,25 @@ import { Card } from "@uifabric/react-cards"; import { + BaseButton, + Button, FontWeights, Icon, IconButton, Image, ImageFit, - Persona, - Text, Link, - BaseButton, - Button, LinkBase, + Persona, Separator, - TooltipHost, Spinner, SpinnerSize, + Text, + TooltipHost, } from "office-ui-fabric-react"; -import * as React from "react"; +import React, { FunctionComponent, useState } from "react"; +import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg"; import { IGalleryItem } from "../../../../Juno/JunoClient"; import * as FileSystemUtil from "../../../Notebook/FileSystemUtil"; -import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg"; export interface GalleryCardComponentProps { data: IGalleryItem; @@ -34,166 +34,48 @@ export interface GalleryCardComponentProps { onDeleteClick: (beforeDelete: () => void, afterDelete: () => void) => void; } -interface GalleryCardComponentState { - isDeletingPublishedNotebook: boolean; -} +export const GalleryCardComponent: FunctionComponent = ({ + data, + isFavorite, + showDownload, + showDelete, + onClick, + onTagClick, + onFavoriteClick, + onUnfavoriteClick, + onDownloadClick, + onDeleteClick, +}: GalleryCardComponentProps) => { + const CARD_WIDTH = 256; + const cardImageHeight = 144; + const cardDescriptionMaxChars = 80; + const cardItemGapBig = 10; + const cardItemGapSmall = 8; + const cardDeleteSpinnerHeight = 360; + const smallTextLineHeight = 18; -export class GalleryCardComponent extends React.Component { - public static readonly CARD_WIDTH = 256; - private static readonly cardImageHeight = 144; - public static readonly cardHeightToWidthRatio = - GalleryCardComponent.cardImageHeight / GalleryCardComponent.CARD_WIDTH; - private static readonly cardDescriptionMaxChars = 80; - private static readonly cardItemGapBig = 10; - private static readonly cardItemGapSmall = 8; - private static readonly cardDeleteSpinnerHeight = 360; - private static readonly smallTextLineHeight = 18; + const [isDeletingPublishedNotebook, setIsDeletingPublishedNotebook] = useState(false); - constructor(props: GalleryCardComponentProps) { - super(props); - this.state = { - isDeletingPublishedNotebook: false, - }; - } + const cardButtonsVisible = isFavorite !== undefined || showDownload || showDelete; + const options: Intl.DateTimeFormatOptions = { + year: "numeric", + month: "short", + day: "numeric", + }; + const dateString = new Date(data.created).toLocaleString("default", options); + const cardTitle = FileSystemUtil.stripExtension(data.name, "ipynb"); - public render(): JSX.Element { - const cardButtonsVisible = this.props.isFavorite !== undefined || this.props.showDownload || this.props.showDelete; - const options: Intl.DateTimeFormatOptions = { - year: "numeric", - month: "short", - day: "numeric", - }; - const dateString = new Date(this.props.data.created).toLocaleString("default", options); - const cardTitle = FileSystemUtil.stripExtension(this.props.data.name, "ipynb"); - - return ( - this.onClick(event, this.props.onClick)} - > - {this.state.isDeletingPublishedNotebook && ( - - - - )} - {!this.state.isDeletingPublishedNotebook && ( - <> - - - - - - {`${cardTitle} - - - - - {this.props.data.tags ? ( - this.props.data.tags.map((tag, index, array) => ( - - this.onClick(event, () => this.props.onTagClick(tag))}>{tag} - {index === array.length - 1 ? <> : ", "} - - )) - ) : ( -
- )} -
- - - {cardTitle} - - - - {this.renderTruncatedDescription()} - - - - {this.props.data.views !== undefined && - this.generateIconText("RedEye", this.props.data.views.toString())} - {this.props.data.downloads !== undefined && - this.generateIconText("Download", this.props.data.downloads.toString())} - {this.props.data.favorites !== undefined && - this.generateIconText("Heart", this.props.data.favorites.toString())} - -
- - {cardButtonsVisible && ( - - - - - {this.props.isFavorite !== undefined && - this.generateIconButtonWithTooltip( - this.props.isFavorite ? "HeartFill" : "Heart", - this.props.isFavorite ? "Unfavorite" : "Favorite", - "left", - this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick - )} - - {this.props.showDownload && - this.generateIconButtonWithTooltip("Download", "Download", "left", this.props.onDownloadClick)} - - {this.props.showDelete && - this.generateIconButtonWithTooltip("Delete", "Remove", "right", () => - this.props.onDeleteClick( - () => this.setState({ isDeletingPublishedNotebook: true }), - () => this.setState({ isDeletingPublishedNotebook: false }) - ) - )} - - - )} - - )} -
- ); - } - - private renderTruncatedDescription = (): string => { - let truncatedDescription = this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars); - if (this.props.data.description.length > GalleryCardComponent.cardDescriptionMaxChars) { + const renderTruncatedDescription = (): string => { + let truncatedDescription = data.description.substr(0, cardDescriptionMaxChars); + if (data.description.length > cardDescriptionMaxChars) { truncatedDescription = `${truncatedDescription} ...`; } return truncatedDescription; }; - private generateIconText = (iconName: string, text: string): JSX.Element => { + const generateIconText = (iconName: string, text: string): JSX.Element => { return ( - + {text} ); @@ -203,7 +85,7 @@ export class GalleryCardComponent extends React.Component this.onClick(event, activate)} + onClick={(event) => handlerOnClick(event, activate)} /> ); }; - private onClick = ( + const handlerOnClick = ( event: | React.MouseEvent | React.MouseEvent< @@ -239,4 +121,112 @@ export class GalleryCardComponent extends React.Component handlerOnClick(event, onClick)} + > + {isDeletingPublishedNotebook && ( + + + + )} + {!isDeletingPublishedNotebook && ( + <> + + + + + + {`${cardTitle} + + + + + {data.tags ? ( + data.tags.map((tag, index, array) => ( + + handlerOnClick(event, () => onTagClick(tag))}>{tag} + {index === array.length - 1 ? <> : ", "} + + )) + ) : ( +
+ )} +
+ + + {cardTitle} + + + + {renderTruncatedDescription()} + + + + {data.views !== undefined && generateIconText("RedEye", data.views.toString())} + {data.downloads !== undefined && generateIconText("Download", data.downloads.toString())} + {data.favorites !== undefined && generateIconText("Heart", data.favorites.toString())} + +
+ + {cardButtonsVisible && ( + + + + + {isFavorite !== undefined && + generateIconButtonWithTooltip( + isFavorite ? "HeartFill" : "Heart", + isFavorite ? "Unfavorite" : "Favorite", + "left", + isFavorite ? onUnfavoriteClick : onFavoriteClick + )} + + {showDownload && generateIconButtonWithTooltip("Download", "Download", "left", onDownloadClick)} + + {showDelete && + generateIconButtonWithTooltip("Delete", "Remove", "right", () => + onDeleteClick( + () => setIsDeletingPublishedNotebook(true), + () => setIsDeletingPublishedNotebook(false) + ) + )} + + + )} + + )} + + ); +}; diff --git a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx deleted file mode 100644 index b59daa6ed..000000000 --- a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import * as React from "react"; -import { JunoClient } from "../../../Juno/JunoClient"; -import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants"; -import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react"; -import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils"; -import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor"; -import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; - -export interface CodeOfConductComponentProps { - junoClient: JunoClient; - onAcceptCodeOfConduct: (result: boolean) => void; -} - -interface CodeOfConductComponentState { - readCodeOfConduct: boolean; -} - -export class CodeOfConductComponent extends React.Component { - private viewCodeOfConductTraced: boolean; - private descriptionPara1: string; - private descriptionPara2: string; - private descriptionPara3: string; - private link1: { label: string; url: string }; - - constructor(props: CodeOfConductComponentProps) { - super(props); - - this.state = { - readCodeOfConduct: false, - }; - - this.descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct"; - this.descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB."; - this.descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the "; - this.link1 = { label: "code of conduct.", url: CodeOfConductEndpoints.codeOfConduct }; - } - - private async acceptCodeOfConduct(): Promise { - const startKey = traceStart(Action.NotebooksGalleryAcceptCodeOfConduct); - - try { - const response = await this.props.junoClient.acceptCodeOfConduct(); - if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { - throw new Error(`Received HTTP ${response.status} when accepting code of conduct`); - } - - traceSuccess(Action.NotebooksGalleryAcceptCodeOfConduct, {}, startKey); - - this.props.onAcceptCodeOfConduct(response.data); - } catch (error) { - traceFailure( - Action.NotebooksGalleryAcceptCodeOfConduct, - { - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }, - startKey - ); - - handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct"); - } - } - - private onChangeCheckbox = (): void => { - this.setState({ readCodeOfConduct: !this.state.readCodeOfConduct }); - }; - - public render(): JSX.Element { - if (!this.viewCodeOfConductTraced) { - this.viewCodeOfConductTraced = true; - trace(Action.NotebooksGalleryViewCodeOfConduct); - } - - return ( - - - {this.descriptionPara1} - - - - {this.descriptionPara2} - - - - - {this.descriptionPara3} - - {this.link1.label} - - - - - - - - - - await this.acceptCodeOfConduct()} - tabIndex={0} - className="genericPaneSubmitBtn" - text="Continue" - disabled={!this.state.readCodeOfConduct} - /> - - - ); - } -} diff --git a/src/Explorer/Controls/NotebookGallery/__snapshots__/CodeOfConductComponent.test.tsx.snap b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/__snapshots__/index.test.tsx.snap similarity index 100% rename from src/Explorer/Controls/NotebookGallery/__snapshots__/CodeOfConductComponent.test.tsx.snap rename to src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/__snapshots__/index.test.tsx.snap diff --git a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.test.tsx b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.test.tsx similarity index 84% rename from src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.test.tsx rename to src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.test.tsx index baf990a4a..fb913adaa 100644 --- a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.test.tsx +++ b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.test.tsx @@ -1,9 +1,9 @@ -jest.mock("../../../Juno/JunoClient"); +jest.mock("../../../../Juno/JunoClient"); import { shallow } from "enzyme"; import React from "react"; -import { CodeOfConductComponent, CodeOfConductComponentProps } from "./CodeOfConductComponent"; -import { JunoClient } from "../../../Juno/JunoClient"; -import { HttpStatusCodes } from "../../../Common/Constants"; +import { CodeOfConductComponent, CodeOfConductComponentProps } from "."; +import { HttpStatusCodes } from "../../../../Common/Constants"; +import { JunoClient } from "../../../../Juno/JunoClient"; describe("CodeOfConductComponent", () => { let codeOfConductProps: CodeOfConductComponentProps; diff --git a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.tsx b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.tsx new file mode 100644 index 000000000..71aefd3cf --- /dev/null +++ b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.tsx @@ -0,0 +1,110 @@ +import { Checkbox, Link, PrimaryButton, Stack, Text } from "office-ui-fabric-react"; +import React, { FunctionComponent, useEffect, useState } from "react"; +import { CodeOfConductEndpoints, HttpStatusCodes } from "../../../../Common/Constants"; +import { getErrorMessage, getErrorStack, handleError } from "../../../../Common/ErrorHandlingUtils"; +import { JunoClient } from "../../../../Juno/JunoClient"; +import { Action } from "../../../../Shared/Telemetry/TelemetryConstants"; +import { trace, traceFailure, traceStart, traceSuccess } from "../../../../Shared/Telemetry/TelemetryProcessor"; + +export interface CodeOfConductComponentProps { + junoClient: JunoClient; + onAcceptCodeOfConduct: (result: boolean) => void; +} + +export const CodeOfConductComponent: FunctionComponent = ({ + junoClient, + onAcceptCodeOfConduct, +}: CodeOfConductComponentProps) => { + const descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct"; + const descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB."; + const descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the "; + const link1: { label: string; url: string } = { + label: "code of conduct.", + url: CodeOfConductEndpoints.codeOfConduct, + }; + + const [readCodeOfConduct, setReadCodeOfConduct] = useState(false); + + const acceptCodeOfConduct = async (): Promise => { + const startKey = traceStart(Action.NotebooksGalleryAcceptCodeOfConduct); + + try { + const response = await junoClient.acceptCodeOfConduct(); + if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { + throw new Error(`Received HTTP ${response.status} when accepting code of conduct`); + } + + traceSuccess(Action.NotebooksGalleryAcceptCodeOfConduct, {}, startKey); + + onAcceptCodeOfConduct(response.data); + } catch (error) { + traceFailure( + Action.NotebooksGalleryAcceptCodeOfConduct, + { + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey + ); + + handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct"); + } + }; + + const onChangeCheckbox = (): void => { + setReadCodeOfConduct(!readCodeOfConduct); + }; + + useEffect(() => { + trace(Action.NotebooksGalleryViewCodeOfConduct); + }, []); + + return ( + + + {descriptionPara1} + + + + {descriptionPara2} + + + + + {descriptionPara3} + + {link1.label} + + + + + + + + + + await acceptCodeOfConduct()} + tabIndex={0} + className="genericPaneSubmitBtn" + text="Continue" + disabled={!readCodeOfConduct} + /> + + + ); +}; diff --git a/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx index 2831ec866..db332a55b 100644 --- a/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx +++ b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx @@ -34,6 +34,7 @@ import { CodeOfConductComponent } from "./CodeOfConductComponent"; import "./GalleryViewerComponent.less"; import { InfoComponent } from "./InfoComponent/InfoComponent"; +const CARD_WIDTH = 256; export interface GalleryViewerComponentProps { container?: Explorer; junoClient: JunoClient; @@ -643,7 +644,7 @@ export class GalleryViewerComponent extends React.Component { if (itemIndex === 0) { - this.columnCount = Math.floor(visibleRect.width / GalleryCardComponent.CARD_WIDTH) || this.columnCount; + this.columnCount = Math.floor(visibleRect.width / CARD_WIDTH) || this.columnCount; this.rowCount = GalleryViewerComponent.rowsPerPage; } diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 5b3fcebc3..644f5980b 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -6,7 +6,6 @@ import React from "react"; import _ from "underscore"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; -import { ReactAdapter } from "../Bindings/ReactBindingHandler"; import * as Constants from "../Common/Constants"; import { ExplorerMetrics } from "../Common/Constants"; import { readCollection } from "../Common/dataAccess/readCollection"; @@ -174,7 +173,6 @@ export default class Explorer { public graphStylingPane: GraphStylingPane; public cassandraAddCollectionPane: CassandraAddCollectionPane; public gitHubReposPane: ContextualPaneBase; - public publishNotebookPaneAdapter: ReactAdapter; // features public isGitHubPaneEnabled: ko.Observable; @@ -1410,7 +1408,6 @@ export default class Explorer { ): Promise { if (this.notebookManager) { await this.notebookManager.openPublishNotebookPane(name, content, parentDomElement); - this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter; this.isPublishNotebookPaneEnabled(true); } } diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index 7d8c9a1d5..cba1b0ccd 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -2,7 +2,7 @@ * Contains all notebook related stuff meant to be dynamically loaded by explorer */ -import type { ImmutableNotebook } from "@nteract/commutable"; +import { ImmutableNotebook } from "@nteract/commutable"; import type { IContentProvider } from "@nteract/core"; import ko from "knockout"; import React from "react"; @@ -22,7 +22,7 @@ import Explorer from "../Explorer"; import { ContextualPaneBase } from "../Panes/ContextualPaneBase"; import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { GitHubReposPane } from "../Panes/GitHubReposPane"; -import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter"; +import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider"; import { NotebookContainerClient } from "./NotebookContainerClient"; @@ -53,7 +53,6 @@ export default class NotebookManager { private gitHubClient: GitHubClient; public gitHubReposPane: ContextualPaneBase; - public publishNotebookPaneAdapter: PublishNotebookPaneAdapter; public initialize(params: NotebookManagerOptions): void { this.params = params; @@ -91,8 +90,6 @@ export default class NotebookManager { this.notebookContentProvider ); - this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient); - this.gitHubOAuthService.getTokenObservable().subscribe((token) => { this.gitHubClient.setToken(token?.access_token); @@ -123,7 +120,20 @@ export default class NotebookManager { content: NotebookPaneContent, parentDomElement: HTMLElement ): Promise { - await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement); + const explorer = this.params.container; + explorer.openSidePanel( + "New Collection", + + ); } public openCopyNotebookPane(name: string, content: string): void { diff --git a/src/Explorer/Panes/PublishNotebookPaneComponent.test.tsx b/src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.test.tsx similarity index 78% rename from src/Explorer/Panes/PublishNotebookPaneComponent.test.tsx rename to src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.test.tsx index 7870793bc..c80c2f2c3 100644 --- a/src/Explorer/Panes/PublishNotebookPaneComponent.test.tsx +++ b/src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.test.tsx @@ -8,14 +8,15 @@ describe("PublishNotebookPaneComponent", () => { notebookName: "SampleNotebook.ipynb", notebookDescription: "sample description", notebookTags: "tag1, tag2", + imageSrc: "https://i.ytimg.com/vi/E_lByLdKeKY/maxresdefault.jpg", notebookAuthor: "CosmosDB", notebookCreatedDate: "2020-07-17T00:00:00Z", notebookObject: undefined, notebookParentDomElement: undefined, - onChangeName: undefined, - onChangeDescription: undefined, - onChangeTags: undefined, - onChangeImageSrc: undefined, + setNotebookName: undefined, + setNotebookDescription: undefined, + setNotebookTags: undefined, + setImageSrc: undefined, onError: undefined, clearFormError: undefined, }; diff --git a/src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.tsx b/src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.tsx new file mode 100644 index 000000000..7bbd3eaec --- /dev/null +++ b/src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.tsx @@ -0,0 +1,205 @@ +import { ImmutableNotebook, toJS } from "@nteract/commutable"; +import React, { FunctionComponent, useEffect, useState } from "react"; +import { HttpStatusCodes } from "../../../Common/Constants"; +import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils"; +import { JunoClient } from "../../../Juno/JunoClient"; +import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; +import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor"; +import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; +import { CodeOfConductComponent } from "../../Controls/NotebookGallery/CodeOfConductComponent"; +import { GalleryTab } from "../../Controls/NotebookGallery/GalleryViewerComponent"; +import Explorer from "../../Explorer"; +import * as FileSystemUtil from "../../Notebook/FileSystemUtil"; +import { + GenericRightPaneComponent, + GenericRightPaneProps, +} from "../GenericRightPaneComponent/GenericRightPaneComponent"; +import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent"; + +export interface PublishNotebookPaneAProps { + explorer: Explorer; + closePanel: () => void; + openNotificationConsole: () => void; + junoClient: JunoClient; + name: string; + author: string; + notebookContent: string | ImmutableNotebook; + parentDomElement: HTMLElement; +} +export const PublishNotebookPane: FunctionComponent = ({ + explorer: container, + junoClient, + closePanel, + name, + author, + notebookContent, + parentDomElement, +}: PublishNotebookPaneAProps): JSX.Element => { + const [isCodeOfConductAccepted, setIsCodeOfConductAccepted] = useState(false); + const [content, setContent] = useState(""); + const [formError, setFormError] = useState(""); + const [formErrorDetail, setFormErrorDetail] = useState(""); + const [isExecuting, setIsExecuting] = useState(); + + const [notebookName, setNotebookName] = useState(name); + const [notebookDescription, setNotebookDescription] = useState(""); + const [notebookTags, setNotebookTags] = useState(""); + const [imageSrc, setImageSrc] = useState(); + + const CodeOfConductAccepted = async () => { + try { + const response = await junoClient.isCodeOfConductAccepted(); + if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { + throw new Error(`Received HTTP ${response.status} when accepting code of conduct`); + } + setIsCodeOfConductAccepted(response.data); + } catch (error) { + handleError( + error, + "PublishNotebookPaneAdapter/isCodeOfConductAccepted", + "Failed to check if code of conduct was accepted" + ); + } + }; + const [notebookObject, setNotebookObject] = useState(); + useEffect(() => { + CodeOfConductAccepted(); + let newContent; + if (typeof notebookContent === "string") { + newContent = notebookContent as string; + } else { + newContent = JSON.stringify(toJS(notebookContent)); + setNotebookObject(notebookContent); + } + setContent(newContent); + }, []); + + const submit = async (): Promise => { + const clearPublishingMessage = NotificationConsoleUtils.logConsoleProgress(`Publishing ${name} to gallery`); + setIsExecuting(true); + + let startKey: number; + + if (!notebookName || !notebookDescription || !author || !imageSrc) { + setFormError(`Failed to publish ${notebookName} to gallery`); + setFormErrorDetail("Name, description, author and cover image are required"); + createFormError(formError, formErrorDetail, "PublishNotebookPaneAdapter/submit"); + setIsExecuting(false); + return; + } + + try { + startKey = traceStart(Action.NotebooksGalleryPublish, {}); + + const response = await junoClient.publishNotebook( + notebookName, + notebookDescription, + notebookTags?.split(","), + author, + imageSrc, + content + ); + + const data = response.data; + if (data) { + let isPublishPending = false; + + if (data.pendingScanJobIds?.length > 0) { + isPublishPending = true; + NotificationConsoleUtils.logConsoleInfo( + `Content of ${name} is currently being scanned for illegal content. It will not be available in the public gallery until the review is complete (may take a few days).` + ); + } else { + NotificationConsoleUtils.logConsoleInfo(`Published ${notebookName} to gallery`); + container.openGallery(GalleryTab.Published); + } + + traceSuccess( + Action.NotebooksGalleryPublish, + { + notebookId: data.id, + isPublishPending, + }, + startKey + ); + } + } catch (error) { + traceFailure( + Action.NotebooksGalleryPublish, + { + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey + ); + + const errorMessage = getErrorMessage(error); + setFormError(`Failed to publish ${FileSystemUtil.stripExtension(notebookName, "ipynb")} to gallery`); + setFormErrorDetail(`${errorMessage}`); + handleError(errorMessage, "PublishNotebookPaneAdapter/submit", formError); + return; + } finally { + clearPublishingMessage(); + setIsExecuting(false); + } + + closePanel(); + }; + + const createFormError = (formError: string, formErrorDetail: string, area: string): void => { + setFormError(formError); + setFormErrorDetail(formErrorDetail); + handleError(formErrorDetail, area, formError); + }; + + const clearFormError = (): void => { + setFormError(""); + setFormErrorDetail(""); + }; + + const props: GenericRightPaneProps = { + container: container, + formError: formError, + formErrorDetail: formErrorDetail, + id: "publishnotebookpane", + isExecuting: isExecuting, + title: "Publish to gallery", + submitButtonText: "Publish", + onSubmit: () => submit(), + onClose: closePanel, + isSubmitButtonHidden: !isCodeOfConductAccepted, + }; + + const publishNotebookPaneProps: PublishNotebookPaneProps = { + notebookDescription, + notebookTags, + imageSrc, + notebookName, + notebookAuthor: author, + notebookCreatedDate: new Date().toISOString(), + notebookObject: notebookObject, + notebookParentDomElement: parentDomElement, + onError: createFormError, + clearFormError: clearFormError, + setNotebookName, + setNotebookDescription, + setNotebookTags, + setImageSrc, + }; + return ( + + {!isCodeOfConductAccepted ? ( +
+ { + setIsCodeOfConductAccepted(isAccepted); + }} + /> +
+ ) : ( + + )} +
+ ); +}; diff --git a/src/Explorer/Panes/PublishNotebookPane/PublishNotebookPaneComponent.tsx b/src/Explorer/Panes/PublishNotebookPane/PublishNotebookPaneComponent.tsx new file mode 100644 index 000000000..f55e17fe4 --- /dev/null +++ b/src/Explorer/Panes/PublishNotebookPane/PublishNotebookPaneComponent.tsx @@ -0,0 +1,297 @@ +import { ImmutableNotebook } from "@nteract/commutable"; +import Html2Canvas from "html2canvas"; +import { Dropdown, IDropdownProps, ITextFieldProps, Stack, Text, TextField } from "office-ui-fabric-react"; +import React, { FunctionComponent, useState } from "react"; +import { GalleryCardComponent } from "../../Controls/NotebookGallery/Cards/GalleryCardComponent"; +import * as FileSystemUtil from "../../Notebook/FileSystemUtil"; +import { NotebookUtil } from "../../Notebook/NotebookUtil"; +import "./styled.less"; + +export interface PublishNotebookPaneProps { + notebookName: string; + notebookAuthor: string; + notebookTags: string; + imageSrc: string; + notebookDescription: string; + notebookCreatedDate: string; + notebookObject: ImmutableNotebook; + notebookParentDomElement?: HTMLElement; + onError: (formError: string, formErrorDetail: string, area: string) => void; + clearFormError: () => void; + setNotebookName: (newValue: string) => void; + setNotebookDescription: (newValue: string) => void; + setNotebookTags: (newValue: string) => void; + setImageSrc: (newValue: string) => void; +} + +enum ImageTypes { + Url = "URL", + CustomImage = "Custom Image", + TakeScreenshot = "Take Screenshot", + UseFirstDisplayOutput = "Use First Display Output", +} + +export const PublishNotebookPaneComponent: FunctionComponent = ({ + notebookName, + notebookTags, + imageSrc, + notebookDescription, + notebookAuthor, + notebookCreatedDate, + notebookObject, + notebookParentDomElement, + onError, + clearFormError, + setNotebookName, + setNotebookDescription, + setNotebookTags, + setImageSrc, +}: PublishNotebookPaneProps) => { + const [type, setType] = useState(ImageTypes.CustomImage); + const CARD_WIDTH = 256; + const cardImageHeight = 144; + const cardHeightToWidthRatio = cardImageHeight / CARD_WIDTH; + + const maxImageSizeInMib = 1.5; + + const descriptionPara1 = + "When published, this notebook will appear in the Azure Cosmos DB notebooks public gallery. Make sure you have removed any sensitive data or output before publishing."; + + const descriptionPara2 = `Would you like to publish and share "${FileSystemUtil.stripExtension( + notebookName, + "ipynb" + )}" to the gallery?`; + + const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url]; + const thumbnailSelectorProps: IDropdownProps = { + label: "Cover image", + defaultSelectedKey: ImageTypes.CustomImage, + ariaLabel: "Cover image", + options: options.map((value: string) => ({ text: value, key: value })), + onChange: async (event, options) => { + setImageSrc(""); + clearFormError(); + if (options.text === ImageTypes.TakeScreenshot) { + try { + await takeScreenshot(notebookParentDomElement, screenshotErrorHandler); + } catch (error) { + screenshotErrorHandler(error); + } + } else if (options.text === ImageTypes.UseFirstDisplayOutput) { + try { + await takeScreenshot(findFirstOutput(), firstOutputErrorHandler); + } catch (error) { + firstOutputErrorHandler(error); + } + } + setType(options.text); + }, + }; + + const thumbnailUrlProps: ITextFieldProps = { + label: "Cover image url", + ariaLabel: "Cover image url", + required: true, + onChange: (event, newValue) => { + setImageSrc(newValue); + }, + }; + + const screenshotErrorHandler = (error: Error) => { + const formError = "Failed to take screen shot"; + const formErrorDetail = `${error}`; + const area = "PublishNotebookPaneComponent/takeScreenshot"; + onError(formError, formErrorDetail, area); + }; + + const firstOutputErrorHandler = (error: Error) => { + const formError = "Failed to capture first output"; + const formErrorDetail = `${error}`; + const area = "PublishNotebookPaneComponent/UseFirstOutput"; + onError(formError, formErrorDetail, area); + }; + + if (notebookParentDomElement) { + options.push(ImageTypes.TakeScreenshot); + if (notebookObject) { + options.push(ImageTypes.UseFirstDisplayOutput); + } + } + + const imageToBase64 = (file: File, updateImageSrc: (result: string) => void) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + updateImageSrc(reader.result.toString()); + }; + + reader.onerror = (error) => { + const formError = `Failed to convert ${file.name} to base64 format`; + const formErrorDetail = `${error}`; + const area = "PublishNotebookPaneComponent/selectImageFile"; + onError(formError, formErrorDetail, area); + }; + }; + + const takeScreenshot = (target: HTMLElement, onError: (error: Error) => void): void => { + const updateImageSrcWithScreenshot = (canvasUrl: string): void => { + setImageSrc(canvasUrl); + }; + + target.scrollIntoView(); + Html2Canvas(target, { + useCORS: true, + allowTaint: true, + scale: 1, + logging: true, + }) + .then((canvas) => { + //redraw canvas to fit Card Cover Image dimensions + const originalImageData = canvas.toDataURL(); + const requiredHeight = parseInt(canvas.style.width.split("px")[0]) * cardHeightToWidthRatio; + canvas.height = requiredHeight; + const context = canvas.getContext("2d"); + const image = new Image(); + image.src = originalImageData; + image.onload = () => { + context.drawImage(image, 0, 0); + updateImageSrcWithScreenshot(canvas.toDataURL()); + }; + }) + .catch((error) => { + onError(error); + }); + }; + + const renderThumbnailSelectors = (type: string) => { + switch (type) { + case ImageTypes.Url: + return ; + case ImageTypes.CustomImage: + return ( + { + const file = event.target.files[0]; + if (file.size / 1024 ** 2 > maxImageSizeInMib) { + event.target.value = ""; + const formError = `Failed to upload ${file.name}`; + const formErrorDetail = `Image is larger than ${maxImageSizeInMib} MiB. Please Choose a different image.`; + const area = "PublishNotebookPaneComponent/selectImageFile"; + + onError(formError, formErrorDetail, area); + setImageSrc(""); + return; + } else { + clearFormError(); + } + imageToBase64(file, (result: string) => { + setImageSrc(result); + }); + }} + /> + ); + default: + return <>; + } + }; + + const findFirstOutput = (): HTMLElement => { + const indexOfFirstCodeCellWithDisplay = NotebookUtil.findFirstCodeCellWithDisplay(notebookObject); + const cellOutputDomElements = notebookParentDomElement.querySelectorAll(".nteract-cell-outputs"); + return cellOutputDomElements[indexOfFirstCodeCellWithDisplay]; + }; + + return ( +
+ + + {descriptionPara1} + + + + {descriptionPara2} + + + + { + const notebookName = newValue + ".ipynb"; + setNotebookName(notebookName); + }} + /> + + + + { + setNotebookDescription(newValue); + }} + /> + + + + { + setNotebookTags(newValue); + }} + /> + + + + + + + {renderThumbnailSelectors(type)} + + + Preview + + + undefined} + onTagClick={undefined} + onFavoriteClick={undefined} + onUnfavoriteClick={undefined} + onDownloadClick={undefined} + onDeleteClick={undefined} + /> + + +
+ ); +}; diff --git a/src/Explorer/Panes/__snapshots__/PublishNotebookPaneComponent.test.tsx.snap b/src/Explorer/Panes/PublishNotebookPane/__snapshots__/PublishNotebookPane.test.tsx.snap similarity index 92% rename from src/Explorer/Panes/__snapshots__/PublishNotebookPaneComponent.test.tsx.snap rename to src/Explorer/Panes/PublishNotebookPane/__snapshots__/PublishNotebookPane.test.tsx.snap index 4986c1ef9..646cc18c6 100644 --- a/src/Explorer/Panes/__snapshots__/PublishNotebookPaneComponent.test.tsx.snap +++ b/src/Explorer/Panes/PublishNotebookPane/__snapshots__/PublishNotebookPane.test.tsx.snap @@ -88,7 +88,7 @@ exports[`PublishNotebookPaneComponent renders 1`] = ` Object { "author": "CosmosDB", "created": "2020-07-17T00:00:00Z", - "description": "", + "description": "sample description", "downloads": undefined, "favorites": undefined, "gitSha": undefined, @@ -99,12 +99,14 @@ exports[`PublishNotebookPaneComponent renders 1`] = ` "pendingScanJobIds": undefined, "policyViolations": undefined, "tags": Array [ - "", + "tag1", + " tag2", ], - "thumbnailUrl": undefined, + "thumbnailUrl": "https://i.ytimg.com/vi/E_lByLdKeKY/maxresdefault.jpg", "views": undefined, } } + onClick={[Function]} showDelete={false} showDownload={false} /> diff --git a/src/Explorer/Panes/PublishNotebookPaneComponent.less b/src/Explorer/Panes/PublishNotebookPane/styled.less similarity index 100% rename from src/Explorer/Panes/PublishNotebookPaneComponent.less rename to src/Explorer/Panes/PublishNotebookPane/styled.less diff --git a/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx b/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx deleted file mode 100644 index ee7927d69..000000000 --- a/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import { toJS } from "@nteract/commutable"; -import { ImmutableNotebook } from "@nteract/commutable/src"; -import ko from "knockout"; -import * as React from "react"; -import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; -import { HttpStatusCodes } from "../../Common/Constants"; -import { getErrorMessage, getErrorStack, handleError } from "../../Common/ErrorHandlingUtils"; -import { JunoClient } from "../../Juno/JunoClient"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor"; -import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; -import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent"; -import { GalleryTab } from "../Controls/NotebookGallery/GalleryViewerComponent"; -import Explorer from "../Explorer"; -import * as FileSystemUtil from "../Notebook/FileSystemUtil"; -import { - GenericRightPaneComponent, - GenericRightPaneProps, -} from "./GenericRightPaneComponent/GenericRightPaneComponent"; -import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent"; - -export class PublishNotebookPaneAdapter implements ReactAdapter { - parameters: ko.Observable; - private isOpened: boolean; - private isExecuting: boolean; - private formError: string; - private formErrorDetail: string; - - private name: string; - private author: string; - private content: string; - private description: string; - private tags: string; - private imageSrc: string; - private notebookObject: ImmutableNotebook; - private parentDomElement: HTMLElement; - private isCodeOfConductAccepted: boolean; - - constructor(private container: Explorer, private junoClient: JunoClient) { - this.parameters = ko.observable(Date.now()); - this.reset(); - this.triggerRender(); - } - - public renderComponent(): JSX.Element { - if (!this.isOpened) { - return undefined; - } - - const props: GenericRightPaneProps = { - container: this.container, - formError: this.formError, - formErrorDetail: this.formErrorDetail, - id: "publishnotebookpane", - isExecuting: this.isExecuting, - title: "Publish to gallery", - submitButtonText: "Publish", - onClose: () => this.close(), - onSubmit: () => this.submit(), - isSubmitButtonHidden: !this.isCodeOfConductAccepted, - }; - - const publishNotebookPaneProps: PublishNotebookPaneProps = { - notebookName: this.name, - notebookDescription: "", - notebookTags: "", - notebookAuthor: this.author, - notebookCreatedDate: new Date().toISOString(), - notebookObject: this.notebookObject, - notebookParentDomElement: this.parentDomElement, - onChangeName: (newValue: string) => (this.name = newValue), - onChangeDescription: (newValue: string) => (this.description = newValue), - onChangeTags: (newValue: string) => (this.tags = newValue), - onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue), - onError: this.createFormError, - clearFormError: this.clearFormError, - }; - - return ( - - {!this.isCodeOfConductAccepted ? ( -
- { - this.isCodeOfConductAccepted = true; - this.triggerRender(); - }} - /> -
- ) : ( - - )} -
- ); - } - - public triggerRender(): void { - window.requestAnimationFrame(() => this.parameters(Date.now())); - } - - public async open( - name: string, - author: string, - notebookContent: string | ImmutableNotebook, - parentDomElement: HTMLElement - ): Promise { - try { - const response = await this.junoClient.isCodeOfConductAccepted(); - if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { - throw new Error(`Received HTTP ${response.status} when accepting code of conduct`); - } - - this.isCodeOfConductAccepted = response.data; - } catch (error) { - handleError( - error, - "PublishNotebookPaneAdapter/isCodeOfConductAccepted", - "Failed to check if code of conduct was accepted" - ); - } - - this.name = name; - this.author = author; - if (typeof notebookContent === "string") { - this.content = notebookContent as string; - } else { - this.content = JSON.stringify(toJS(notebookContent)); - this.notebookObject = notebookContent; - } - this.parentDomElement = parentDomElement; - - this.isOpened = true; - this.triggerRender(); - } - - public close(): void { - this.reset(); - this.triggerRender(); - } - - public async submit(): Promise { - const clearPublishingMessage = NotificationConsoleUtils.logConsoleProgress(`Publishing ${this.name} to gallery`); - this.isExecuting = true; - this.triggerRender(); - - let startKey: number; - - if (!this.name || !this.description || !this.author || !this.imageSrc) { - const formError = `Failed to publish ${this.name} to gallery`; - const formErrorDetail = "Name, description, author and cover image are required"; - this.createFormError(formError, formErrorDetail, "PublishNotebookPaneAdapter/submit"); - this.isExecuting = false; - return; - } - - try { - startKey = traceStart(Action.NotebooksGalleryPublish, {}); - - const response = await this.junoClient.publishNotebook( - this.name, - this.description, - this.tags?.split(","), - this.author, - this.imageSrc, - this.content - ); - - const data = response.data; - if (data) { - let isPublishPending = false; - - if (data.pendingScanJobIds?.length > 0) { - isPublishPending = true; - NotificationConsoleUtils.logConsoleInfo( - `Content of ${this.name} is currently being scanned for illegal content. It will not be available in the public gallery until the review is complete (may take a few days).` - ); - } else { - NotificationConsoleUtils.logConsoleInfo(`Published ${this.name} to gallery`); - this.container.openGallery(GalleryTab.Published); - } - - traceSuccess( - Action.NotebooksGalleryPublish, - { - notebookId: data.id, - isPublishPending, - }, - startKey - ); - } - } catch (error) { - traceFailure( - Action.NotebooksGalleryPublish, - { - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }, - startKey - ); - - const errorMessage = getErrorMessage(error); - this.formError = `Failed to publish ${FileSystemUtil.stripExtension(this.name, "ipynb")} to gallery`; - this.formErrorDetail = `${errorMessage}`; - handleError(errorMessage, "PublishNotebookPaneAdapter/submit", this.formError); - return; - } finally { - clearPublishingMessage(); - this.isExecuting = false; - this.triggerRender(); - } - - this.close(); - } - - private createFormError = (formError: string, formErrorDetail: string, area: string): void => { - this.formError = formError; - this.formErrorDetail = formErrorDetail; - handleError(formErrorDetail, area, formError); - this.triggerRender(); - }; - - private clearFormError = (): void => { - this.formError = undefined; - this.formErrorDetail = undefined; - this.triggerRender(); - }; - - private reset = (): void => { - this.isOpened = false; - this.isExecuting = false; - this.formError = undefined; - this.formErrorDetail = undefined; - this.name = undefined; - this.author = undefined; - this.content = undefined; - this.description = undefined; - this.tags = undefined; - this.imageSrc = undefined; - this.notebookObject = undefined; - this.parentDomElement = undefined; - this.isCodeOfConductAccepted = undefined; - }; -} diff --git a/src/Explorer/Panes/PublishNotebookPaneComponent.tsx b/src/Explorer/Panes/PublishNotebookPaneComponent.tsx deleted file mode 100644 index 1075ae5c2..000000000 --- a/src/Explorer/Panes/PublishNotebookPaneComponent.tsx +++ /dev/null @@ -1,326 +0,0 @@ -import { ITextFieldProps, Stack, Text, TextField, Dropdown, IDropdownProps } from "office-ui-fabric-react"; -import * as React from "react"; -import { GalleryCardComponent } from "../Controls/NotebookGallery/Cards/GalleryCardComponent"; -import * as FileSystemUtil from "../Notebook/FileSystemUtil"; -import "./PublishNotebookPaneComponent.less"; -import Html2Canvas from "html2canvas"; -import { ImmutableNotebook } from "@nteract/commutable/src"; -import { NotebookUtil } from "../Notebook/NotebookUtil"; - -export interface PublishNotebookPaneProps { - notebookName: string; - notebookDescription: string; - notebookTags: string; - notebookAuthor: string; - notebookCreatedDate: string; - notebookObject: ImmutableNotebook; - notebookParentDomElement?: HTMLElement; - onChangeName: (newValue: string) => void; - onChangeDescription: (newValue: string) => void; - onChangeTags: (newValue: string) => void; - onChangeImageSrc: (newValue: string) => void; - onError: (formError: string, formErrorDetail: string, area: string) => void; - clearFormError: () => void; -} - -interface PublishNotebookPaneState { - type: string; - notebookName: string; - notebookDescription: string; - notebookTags: string; - imageSrc: string; -} - -enum ImageTypes { - Url = "URL", - CustomImage = "Custom Image", - TakeScreenshot = "Take Screenshot", - UseFirstDisplayOutput = "Use First Display Output", -} - -export class PublishNotebookPaneComponent extends React.Component { - private static readonly maxImageSizeInMib = 1.5; - private descriptionPara1: string; - private descriptionPara2: string; - private nameProps: ITextFieldProps; - private descriptionProps: ITextFieldProps; - private tagsProps: ITextFieldProps; - private thumbnailUrlProps: ITextFieldProps; - private thumbnailSelectorProps: IDropdownProps; - private imageToBase64: (file: File, updateImageSrc: (result: string) => void) => void; - private takeScreenshot: (target: HTMLElement, onError: (error: Error) => void) => void; - - constructor(props: PublishNotebookPaneProps) { - super(props); - - this.state = { - type: ImageTypes.CustomImage, - notebookName: props.notebookName, - notebookDescription: "", - notebookTags: "", - imageSrc: undefined, - }; - - this.imageToBase64 = (file: File, updateImageSrc: (result: string) => void) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - updateImageSrc(reader.result.toString()); - }; - - const onError = this.props.onError; - reader.onerror = (error) => { - const formError = `Failed to convert ${file.name} to base64 format`; - const formErrorDetail = `${error}`; - const area = "PublishNotebookPaneComponent/selectImageFile"; - onError(formError, formErrorDetail, area); - }; - }; - - this.takeScreenshot = (target: HTMLElement, onError: (error: Error) => void): void => { - const updateImageSrcWithScreenshot = (canvasUrl: string): void => { - this.props.onChangeImageSrc(canvasUrl); - this.setState({ imageSrc: canvasUrl }); - }; - - target.scrollIntoView(); - Html2Canvas(target, { - useCORS: true, - allowTaint: true, - scale: 1, - logging: true, - }) - .then((canvas) => { - //redraw canvas to fit Card Cover Image dimensions - const originalImageData = canvas.toDataURL(); - const requiredHeight = - parseInt(canvas.style.width.split("px")[0]) * GalleryCardComponent.cardHeightToWidthRatio; - canvas.height = requiredHeight; - const context = canvas.getContext("2d"); - const image = new Image(); - image.src = originalImageData; - image.onload = () => { - context.drawImage(image, 0, 0); - updateImageSrcWithScreenshot(canvas.toDataURL()); - }; - }) - .catch((error) => { - onError(error); - }); - }; - - this.descriptionPara1 = - "When published, this notebook will appear in the Azure Cosmos DB notebooks public gallery. Make sure you have removed any sensitive data or output before publishing."; - - this.descriptionPara2 = `Would you like to publish and share "${FileSystemUtil.stripExtension( - this.props.notebookName, - "ipynb" - )}" to the gallery?`; - - this.thumbnailUrlProps = { - label: "Cover image url", - ariaLabel: "Cover image url", - required: true, - onChange: (event, newValue) => { - this.props.onChangeImageSrc(newValue); - this.setState({ imageSrc: newValue }); - }, - }; - - const screenshotErrorHandler = (error: Error) => { - const formError = "Failed to take screen shot"; - const formErrorDetail = `${error}`; - const area = "PublishNotebookPaneComponent/takeScreenshot"; - this.props.onError(formError, formErrorDetail, area); - }; - - const firstOutputErrorHandler = (error: Error) => { - const formError = "Failed to capture first output"; - const formErrorDetail = `${error}`; - const area = "PublishNotebookPaneComponent/UseFirstOutput"; - this.props.onError(formError, formErrorDetail, area); - }; - - const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url]; - - if (this.props.notebookParentDomElement) { - options.push(ImageTypes.TakeScreenshot); - if (this.props.notebookObject) { - options.push(ImageTypes.UseFirstDisplayOutput); - } - } - - this.thumbnailSelectorProps = { - label: "Cover image", - defaultSelectedKey: ImageTypes.CustomImage, - ariaLabel: "Cover image", - options: options.map((value: string) => ({ text: value, key: value })), - onChange: async (event, options) => { - this.setState({ imageSrc: undefined }); - this.props.onChangeImageSrc(undefined); - this.props.clearFormError(); - if (options.text === ImageTypes.TakeScreenshot) { - try { - await this.takeScreenshot(this.props.notebookParentDomElement, screenshotErrorHandler); - } catch (error) { - screenshotErrorHandler(error); - } - } else if (options.text === ImageTypes.UseFirstDisplayOutput) { - try { - await this.takeScreenshot(this.findFirstOutput(), firstOutputErrorHandler); - } catch (error) { - firstOutputErrorHandler(error); - } - } - this.setState({ type: options.text }); - }, - }; - - this.nameProps = { - label: "Name", - ariaLabel: "Name", - defaultValue: FileSystemUtil.stripExtension(this.props.notebookName, "ipynb"), - required: true, - onChange: (event, newValue) => { - const notebookName = newValue + ".ipynb"; - this.props.onChangeName(notebookName); - this.setState({ notebookName }); - }, - }; - - this.descriptionProps = { - label: "Description", - ariaLabel: "Description", - multiline: true, - rows: 3, - required: true, - onChange: (event, newValue) => { - this.props.onChangeDescription(newValue); - this.setState({ notebookDescription: newValue }); - }, - }; - - this.tagsProps = { - label: "Tags", - ariaLabel: "Tags", - placeholder: "Optional tag 1, Optional tag 2", - onChange: (event, newValue) => { - this.props.onChangeTags(newValue); - this.setState({ notebookTags: newValue }); - }, - }; - } - - private renderThumbnailSelectors(type: string) { - switch (type) { - case ImageTypes.Url: - return ; - case ImageTypes.CustomImage: - return ( - { - const file = event.target.files[0]; - if (file.size / 1024 ** 2 > PublishNotebookPaneComponent.maxImageSizeInMib) { - event.target.value = ""; - const formError = `Failed to upload ${file.name}`; - const formErrorDetail = `Image is larger than ${PublishNotebookPaneComponent.maxImageSizeInMib} MiB. Please Choose a different image.`; - const area = "PublishNotebookPaneComponent/selectImageFile"; - - this.props.onError(formError, formErrorDetail, area); - this.props.onChangeImageSrc(undefined); - this.setState({ imageSrc: undefined }); - return; - } else { - this.props.clearFormError(); - } - this.imageToBase64(file, (result: string) => { - this.props.onChangeImageSrc(result); - this.setState({ imageSrc: result }); - }); - }} - /> - ); - default: - return <>; - } - } - - private findFirstOutput(): HTMLElement { - const indexOfFirstCodeCellWithDisplay = NotebookUtil.findFirstCodeCellWithDisplay(this.props.notebookObject); - const cellOutputDomElements = this.props.notebookParentDomElement.querySelectorAll( - ".nteract-cell-outputs" - ); - return cellOutputDomElements[indexOfFirstCodeCellWithDisplay]; - } - - public render(): JSX.Element { - return ( -
- - - {this.descriptionPara1} - - - - {this.descriptionPara2} - - - - - - - - - - - - - - - - - - - {this.renderThumbnailSelectors(this.state.type)} - - - Preview - - - - - -
- ); - } -} diff --git a/src/Main.tsx b/src/Main.tsx index 8d06015af..8970ffbc8 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -235,9 +235,6 @@ const App: React.FunctionComponent = () => {
- -
- {showDialog && }
); diff --git a/src/Utils/UserUtils.ts b/src/Utils/UserUtils.ts index 78270c37b..991211dff 100644 --- a/src/Utils/UserUtils.ts +++ b/src/Utils/UserUtils.ts @@ -1,8 +1,8 @@ -import { decryptJWTToken } from "./AuthorizationUtils"; import { userContext } from "../UserContext"; +import { decryptJWTToken } from "./AuthorizationUtils"; -export function getFullName(): string { - const authToken = userContext.authorizationToken; - const props = decryptJWTToken(authToken); - return props.name; -} +export const getFullName = (): string => { + const { authorizationToken } = userContext; + const { name } = decryptJWTToken(authorizationToken); + return name; +}; From 5a019eb431558532cdc62137600e3d66be93bfc3 Mon Sep 17 00:00:00 2001 From: Steve Faulkner Date: Tue, 27 Apr 2021 20:50:01 -0500 Subject: [PATCH 8/8] Remove Explorer.isPreferredAPIMongo (#557) Co-authored-by: hardiknai-techm --- src/Common/QueriesClient.ts | 8 +++--- src/Explorer/ContextMenuButtonFactory.ts | 2 +- .../Controls/Settings/SettingsComponent.tsx | 10 +++---- .../SubSettingsComponent.tsx | 2 +- .../SettingsComponent.test.tsx.snap | 4 --- .../ContainerSampleGenerator.test.ts | 1 - src/Explorer/Explorer.tsx | 26 ------------------- .../CommandBar/CommandBarComponentAdapter.tsx | 1 - .../CommandBarComponentButtonFactory.test.ts | 20 +++++++++----- .../CommandBarComponentButtonFactory.tsx | 8 +++--- src/Explorer/Panes/AddCollectionPane.html | 9 +++---- src/Explorer/Panes/AddCollectionPane.ts | 24 ++++++++--------- .../__snapshots__/SettingsPane.test.tsx.snap | 2 -- .../StringInputPane.test.tsx.snap | 2 -- .../UploadItemsPane.test.tsx.snap | 1 - ...eteDatabaseConfirmationPanel.test.tsx.snap | 1 - src/Explorer/SplashScreen/SplashScreen.tsx | 2 +- src/Explorer/Tabs/DocumentsTab.test.ts | 11 +++++--- src/Explorer/Tabs/DocumentsTab.ts | 5 ++-- src/Explorer/Tree/Collection.test.ts | 3 --- src/Explorer/Tree/Collection.ts | 12 +++------ src/RouteHandlers/TabRouteHandler.ts | 20 +++----------- src/UserContext.ts | 6 +++-- 23 files changed, 64 insertions(+), 116 deletions(-) diff --git a/src/Common/QueriesClient.ts b/src/Common/QueriesClient.ts index b7146fdbf..9d89f89b0 100644 --- a/src/Common/QueriesClient.ts +++ b/src/Common/QueriesClient.ts @@ -5,16 +5,16 @@ import * as ViewModels from "../Contracts/ViewModels"; import Explorer from "../Explorer/Explorer"; import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentId from "../Explorer/Tree/DocumentId"; +import { userContext } from "../UserContext"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as QueryUtils from "../Utils/QueryUtils"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; -import { userContext } from "../UserContext"; -import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage"; import { createCollection } from "./dataAccess/createCollection"; -import { handleError } from "./ErrorHandlingUtils"; import { createDocument } from "./dataAccess/createDocument"; import { deleteDocument } from "./dataAccess/deleteDocument"; import { queryDocuments } from "./dataAccess/queryDocuments"; +import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage"; +import { handleError } from "./ErrorHandlingUtils"; export class QueriesClient { private static readonly PartitionKey: DataModels.PartitionKey = { @@ -211,7 +211,7 @@ export class QueriesClient { } private fetchQueriesQuery(): string { - if (this.container.isPreferredApiMongoDB()) { + if (userContext.apiType === "Mongo") { return QueriesClient.FetchMongoQuery; } return QueriesClient.FetchQuery; diff --git a/src/Explorer/ContextMenuButtonFactory.ts b/src/Explorer/ContextMenuButtonFactory.ts index 8337ec700..db950ad46 100644 --- a/src/Explorer/ContextMenuButtonFactory.ts +++ b/src/Explorer/ContextMenuButtonFactory.ts @@ -63,7 +63,7 @@ export class ResourceTreeContextMenuButtonFactory { }); } - if (container.isPreferredApiMongoDB()) { + if (userContext.apiType === "Mongo") { items.push({ iconSrc: AddSqlQueryIcon, onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null), diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 4c151eb51..ec1ee7727 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -136,15 +136,13 @@ export class SettingsComponent extends React.Component => { if ( - this.container.isPreferredApiMongoDB() && + userContext.apiType === "Mongo" && this.container.isEnableMongoCapabilityPresent() && this.container.databaseAccount() ) { @@ -1002,7 +1000,7 @@ export class SettingsComponent extends React.Component, }); - } else if (this.container.isPreferredApiMongoDB()) { + } else if (userContext.apiType === "Mongo") { const mongoIndexTabContext = this.getMongoIndexTabContent(mongoIndexingPolicyComponentProps); if (mongoIndexTabContext) { tabs.push({ diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx index 14947f81c..4a73e93e6 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/SubSettingsComponent.tsx @@ -323,7 +323,7 @@ export class SubSettingsComponent extends React.Component { const createExplorerStub = (database: ViewModels.Database): Explorer => { const explorerStub = {} as Explorer; explorerStub.databases = ko.observableArray([database]); - explorerStub.isPreferredApiMongoDB = ko.computed(() => false); explorerStub.canExceedMaximumValue = ko.computed(() => false); explorerStub.findDatabaseWithId = () => database; explorerStub.refreshAllDatabases = () => Q.resolve(); diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 644f5980b..875c275db 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -118,11 +118,6 @@ export default class Explorer { * Use userContext.apiType instead * */ public defaultExperience: ko.Observable; - /** - * @deprecated - * Compare a string with userContext.apiType instead: userContext.apiType === "Mongo" - * */ - public isPreferredApiMongoDB: ko.Computed; public isFixedCollectionWithSharedThroughputSupported: ko.Computed; /** * @deprecated @@ -409,27 +404,6 @@ export default class Explorer { ) !== undefined ); - this.isPreferredApiMongoDB = ko.computed(() => { - const defaultExperience = (this.defaultExperience && this.defaultExperience()) || ""; - if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.MongoDB.toLowerCase()) { - return true; - } - - if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.ApiForMongoDB.toLowerCase()) { - return true; - } - - if ( - this.databaseAccount && - this.databaseAccount() && - this.databaseAccount().kind.toLowerCase() === Constants.AccountKind.MongoDB - ) { - return true; - } - - return false; - }); - this.isEnableMongoCapabilityPresent = ko.computed(() => { const capabilities = this.databaseAccount && this.databaseAccount()?.properties?.capabilities; if (!capabilities) { diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx index 917470dd4..b45f5dc9d 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx @@ -29,7 +29,6 @@ export class CommandBarComponentAdapter implements ReactAdapter { // These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates const toWatch = [ - container.isPreferredApiMongoDB, container.deleteCollectionText, container.deleteDatabaseText, container.addCollectionText, diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts index 33fa59e0e..b692bdd38 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts @@ -23,7 +23,6 @@ describe("CommandBarComponentButtonFactory tests", () => { }, } as DatabaseAccount, }); - mockExplorer.isPreferredApiMongoDB = ko.computed(() => false); mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSynapseLinkUpdating = ko.observable(false); @@ -67,7 +66,6 @@ describe("CommandBarComponentButtonFactory tests", () => { }, } as DatabaseAccount, }); - mockExplorer.isPreferredApiMongoDB = ko.computed(() => false); mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSparkEnabled = ko.observable(true); mockExplorer.isSynapseLinkUpdating = ko.observable(false); @@ -128,6 +126,7 @@ describe("CommandBarComponentButtonFactory tests", () => { beforeAll(() => { mockExplorer = {} as Explorer; + mockExplorer.addDatabaseText = ko.observable("mockText"); mockExplorer.addCollectionText = ko.observable("mockText"); updateUserContext({ databaseAccount: { @@ -143,16 +142,25 @@ describe("CommandBarComponentButtonFactory tests", () => { mockExplorer.isServerlessEnabled = ko.computed(() => false); }); + afterAll(() => { + updateUserContext({ + apiType: "SQL", + }); + }); + beforeEach(() => { - mockExplorer.isPreferredApiMongoDB = ko.computed(() => true); + updateUserContext({ + apiType: "Mongo", + }); mockExplorer.isNotebookEnabled = ko.observable(false); mockExplorer.isNotebooksEnabledForAccount = ko.observable(false); mockExplorer.isRunningOnNationalCloud = ko.observable(false); }); it("Mongo Api not available - button should be hidden", () => { - mockExplorer.isPreferredApiMongoDB = ko.computed(() => false); - + updateUserContext({ + apiType: "SQL", + }); const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer); const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel); expect(openMongoShellBtn).toBeUndefined(); @@ -222,7 +230,6 @@ describe("CommandBarComponentButtonFactory tests", () => { }, } as DatabaseAccount, }); - mockExplorer.isPreferredApiMongoDB = ko.computed(() => false); mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSparkEnabled = ko.observable(true); @@ -321,7 +328,6 @@ describe("CommandBarComponentButtonFactory tests", () => { }, } as DatabaseAccount, }); - mockExplorer.isPreferredApiMongoDB = ko.computed(() => false); mockExplorer.isSynapseLinkUpdating = ko.observable(false); mockExplorer.isSparkEnabled = ko.observable(true); diff --git a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx index 3f3533f66..151620d4a 100644 --- a/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx +++ b/src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.tsx @@ -70,7 +70,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto buttons.push(createEnableNotebooksButton(container)); } - if (container.isPreferredApiMongoDB()) { + if (userContext.apiType === "Mongo") { buttons.push(createOpenMongoTerminalButton(container)); } @@ -97,7 +97,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto } const isSupportedOpenQueryApi = - userContext.apiType === "SQL" || container.isPreferredApiMongoDB() || userContext.apiType === "Gremlin"; + userContext.apiType === "SQL" || userContext.apiType === "Mongo" || userContext.apiType === "Gremlin"; const isSupportedOpenQueryFromDiskApi = userContext.apiType === "SQL" || userContext.apiType === "Gremlin"; if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) { const openQueryBtn = createOpenQueryButton(container); @@ -133,7 +133,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto export function createContextCommandBarButtons(container: Explorer): CommandButtonComponentProps[] { const buttons: CommandButtonComponentProps[] = []; - if (!container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()) { + if (!container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") { const label = "New Shell"; const newMongoShellBtn: CommandButtonComponentProps = { iconSrc: HostedTerminalIcon, @@ -145,7 +145,7 @@ export function createContextCommandBarButtons(container: Explorer): CommandButt commandButtonLabel: label, ariaLabel: label, hasPopup: true, - disabled: container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB(), + disabled: container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo", }; buttons.push(newMongoShellBtn); } diff --git a/src/Explorer/Panes/AddCollectionPane.html b/src/Explorer/Panes/AddCollectionPane.html index 774915458..8f917cafd 100644 --- a/src/Explorer/Panes/AddCollectionPane.html +++ b/src/Explorer/Panes/AddCollectionPane.html @@ -251,10 +251,7 @@
-
+
* Storage capacity @@ -312,7 +309,7 @@
diff --git a/src/Explorer/Panes/AddCollectionPane.ts b/src/Explorer/Panes/AddCollectionPane.ts index ba2f93884..65e9e5f02 100644 --- a/src/Explorer/Panes/AddCollectionPane.ts +++ b/src/Explorer/Panes/AddCollectionPane.ts @@ -119,7 +119,7 @@ export default class AddCollectionPane extends ContextualPaneBase { this.isPreferredApiTable = options.isPreferredApiTable; this.partitionKey = ko.observable(); this.partitionKey.subscribe((newPartitionKey: string) => { - if (this.container.isPreferredApiMongoDB() || !newPartitionKey || newPartitionKey[0] === "/") { + if (userContext.apiType === "Mongo" || !newPartitionKey || newPartitionKey[0] === "/") { return; } @@ -354,7 +354,7 @@ export default class AddCollectionPane extends ContextualPaneBase { // TODO: Create derived classes for Tables and Mongo to replace the If statements below this.partitionKeyName = ko.computed(() => { - if (this.container && !!this.container.isPreferredApiMongoDB()) { + if (userContext.apiType === "Mongo") { return "Shard key"; } @@ -364,7 +364,7 @@ export default class AddCollectionPane extends ContextualPaneBase { this.lowerCasePartitionKeyName = ko.computed(() => this.partitionKeyName().toLowerCase()); this.partitionKeyPlaceholder = ko.computed(() => { - if (this.container && !!this.container.isPreferredApiMongoDB()) { + if (userContext.apiType === "Mongo") { return "e.g., address.zipCode"; } @@ -376,7 +376,7 @@ export default class AddCollectionPane extends ContextualPaneBase { }); this.uniqueKeysPlaceholder = ko.pureComputed(() => { - if (this.container && !!this.container.isPreferredApiMongoDB()) { + if (userContext.apiType === "Mongo") { return "Comma separated paths e.g. firstName,address.zipCode"; } @@ -396,11 +396,7 @@ export default class AddCollectionPane extends ContextualPaneBase { return false; } - if ( - this.container.isPreferredApiMongoDB() && - !this.isUnlimitedStorageSelected() && - this.databaseHasSharedOffer() - ) { + if (userContext.apiType === "Mongo" && !this.isUnlimitedStorageSelected() && this.databaseHasSharedOffer()) { return false; } @@ -589,7 +585,7 @@ export default class AddCollectionPane extends ContextualPaneBase { return true; } - if (this.container.isPreferredApiMongoDB()) { + if (userContext.apiType === "Mongo") { return true; } @@ -728,6 +724,10 @@ export default class AddCollectionPane extends ContextualPaneBase { } } + private isMongo(): boolean { + return userContext.apiType === "Mongo"; + } + private _onDatabasesChange(newDatabaseIds: ViewModels.Database[]) { this.databaseIds(newDatabaseIds?.map((database: ViewModels.Database) => database.id())); } @@ -810,7 +810,7 @@ export default class AddCollectionPane extends ContextualPaneBase { let indexingPolicy: DataModels.IndexingPolicy; let createMongoWildcardIndex: boolean; // todo - remove mongo indexing policy ticket # 616274 - if (this.container.isPreferredApiMongoDB() && this.container.isEnableMongoCapabilityPresent()) { + if (userContext.apiType === "Mongo" && this.container.isEnableMongoCapabilityPresent()) { createMongoWildcardIndex = this.shouldCreateMongoWildcardIndex(); } else if (this.showIndexingOptionsForSharedThroughput()) { if (this.useIndexingForSharedThroughput()) { @@ -1145,7 +1145,7 @@ export default class AddCollectionPane extends ContextualPaneBase { let transform = (value: string) => { return value; }; - if (this.container.isPreferredApiMongoDB()) { + if (userContext.apiType === "Mongo") { transform = (value: string) => { return this._convertShardKeyToPartitionKey(value); }; diff --git a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap index 5915ca240..69f4ef1b7 100644 --- a/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap +++ b/src/Explorer/Panes/SettingsPane/__snapshots__/SettingsPane.test.tsx.snap @@ -434,7 +434,6 @@ exports[`Settings Pane should render Default properly 1`] = ` "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], - "isPreferredApiMongoDB": [Function], "isPublishNotebookPaneEnabled": [Function], "isResourceTokenCollectionNodeSelected": [Function], "isRightPanelV2Enabled": [Function], @@ -1073,7 +1072,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = ` "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], - "isPreferredApiMongoDB": [Function], "isPublishNotebookPaneEnabled": [Function], "isResourceTokenCollectionNodeSelected": [Function], "isRightPanelV2Enabled": [Function], diff --git a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap index e2ddc5c49..fbdc9fc80 100644 --- a/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap +++ b/src/Explorer/Panes/StringInputPane/__snapshots__/StringInputPane.test.tsx.snap @@ -437,7 +437,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], - "isPreferredApiMongoDB": [Function], "isPublishNotebookPaneEnabled": [Function], "isResourceTokenCollectionNodeSelected": [Function], "isRightPanelV2Enabled": [Function], @@ -952,7 +951,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = ` "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], - "isPreferredApiMongoDB": [Function], "isPublishNotebookPaneEnabled": [Function], "isResourceTokenCollectionNodeSelected": [Function], "isRightPanelV2Enabled": [Function], diff --git a/src/Explorer/Panes/UploadItemsPane/__snapshots__/UploadItemsPane.test.tsx.snap b/src/Explorer/Panes/UploadItemsPane/__snapshots__/UploadItemsPane.test.tsx.snap index 9bb261343..fc59040bf 100644 --- a/src/Explorer/Panes/UploadItemsPane/__snapshots__/UploadItemsPane.test.tsx.snap +++ b/src/Explorer/Panes/UploadItemsPane/__snapshots__/UploadItemsPane.test.tsx.snap @@ -434,7 +434,6 @@ exports[`Upload Items Pane should render Default properly 1`] = ` "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], - "isPreferredApiMongoDB": [Function], "isPublishNotebookPaneEnabled": [Function], "isResourceTokenCollectionNodeSelected": [Function], "isRightPanelV2Enabled": [Function], diff --git a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap index e4d8030b5..88da6e77a 100644 --- a/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap +++ b/src/Explorer/Panes/__snapshots__/DeleteDatabaseConfirmationPanel.test.tsx.snap @@ -437,7 +437,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database "isMongoIndexingEnabled": [Function], "isNotebookEnabled": [Function], "isNotebooksEnabledForAccount": [Function], - "isPreferredApiMongoDB": [Function], "isPublishNotebookPaneEnabled": [Function], "isResourceTokenCollectionNodeSelected": [Function], "isRightPanelV2Enabled": [Function], diff --git a/src/Explorer/SplashScreen/SplashScreen.tsx b/src/Explorer/SplashScreen/SplashScreen.tsx index 85186f5c6..ca67d86dd 100644 --- a/src/Explorer/SplashScreen/SplashScreen.tsx +++ b/src/Explorer/SplashScreen/SplashScreen.tsx @@ -237,7 +237,7 @@ export class SplashScreen extends React.Component { title: "New SQL Query", description: null, }); - } else if (this.container.isPreferredApiMongoDB()) { + } else if (userContext.apiType === "Mongo") { items.push({ iconSrc: NewQueryIcon, onClick: () => { diff --git a/src/Explorer/Tabs/DocumentsTab.test.ts b/src/Explorer/Tabs/DocumentsTab.test.ts index 1d470f23a..8ebb6a8ad 100644 --- a/src/Explorer/Tabs/DocumentsTab.test.ts +++ b/src/Explorer/Tabs/DocumentsTab.test.ts @@ -1,10 +1,11 @@ import * as ko from "knockout"; -import * as ViewModels from "../../Contracts/ViewModels"; import * as Constants from "../../Common/Constants"; -import DocumentsTab from "./DocumentsTab"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { updateUserContext } from "../../UserContext"; +import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import Explorer from "../Explorer"; import DocumentId from "../Tree/DocumentId"; -import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; +import DocumentsTab from "./DocumentsTab"; describe("Documents tab", () => { describe("buildQuery", () => { @@ -25,7 +26,6 @@ describe("Documents tab", () => { describe("showPartitionKey", () => { const explorer = new Explorer(); - const mongoExplorer = new Explorer(); mongoExplorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB); @@ -124,6 +124,9 @@ describe("Documents tab", () => { }); it("should be false for Mongo accounts with system partitionKey", () => { + updateUserContext({ + apiType: "Mongo", + }); const documentsTab = new DocumentsTab({ collection: mongoCollectionWithSystemPartitionKey, partitionKey: null, diff --git a/src/Explorer/Tabs/DocumentsTab.ts b/src/Explorer/Tabs/DocumentsTab.ts index 9f6cafcd4..1df03ae65 100644 --- a/src/Explorer/Tabs/DocumentsTab.ts +++ b/src/Explorer/Tabs/DocumentsTab.ts @@ -21,6 +21,7 @@ import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; +import { userContext } from "../../UserContext"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import * as QueryUtils from "../../Utils/QueryUtils"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; @@ -71,9 +72,7 @@ export default class DocumentsTab extends TabsBase { constructor(options: ViewModels.DocumentsTabOptions) { super(options); - this.isPreferredApiMongoDB = !!this.collection - ? this.collection.container.isPreferredApiMongoDB() - : options.isPreferredApiMongoDB; + this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB; this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id"; diff --git a/src/Explorer/Tree/Collection.test.ts b/src/Explorer/Tree/Collection.test.ts index d55342a0d..7e185ef4a 100644 --- a/src/Explorer/Tree/Collection.test.ts +++ b/src/Explorer/Tree/Collection.test.ts @@ -31,9 +31,6 @@ describe("Collection", () => { function generateMockCollectionWithDataModel(data: DataModels.Collection): Collection { const mockContainer = {} as Explorer; - mockContainer.isPreferredApiMongoDB = ko.computed(() => { - return false; - }); mockContainer.isDatabaseNodeOrNoneSelected = () => { return false; diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index 4335b56fe..6b129c053 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -127,16 +127,12 @@ export default class Collection implements ViewModels.Collection { this.partitionKey.paths[0]) || null; - if (!!container.isPreferredApiMongoDB() && this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) { + if (userContext.apiType === "Mongo" && this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) { this.partitionKeyProperty = this.partitionKeyProperty.replace(/["]+/g, ""); } // TODO #10738269 : Add this logic in a derived class for Mongo - if ( - !!container.isPreferredApiMongoDB() && - this.partitionKeyProperty && - this.partitionKeyProperty.indexOf("$v") > -1 - ) { + if (userContext.apiType === "Mongo" && this.partitionKeyProperty && this.partitionKeyProperty.indexOf("$v") > -1) { // From $v.shard.$v.key.$v > shard.key this.partitionKeyProperty = this.partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, ""); this.partitionKeyPropertyHeader = "/" + this.partitionKeyProperty; @@ -1123,7 +1119,7 @@ export default class Collection implements ViewModels.Collection { } else if (userContext.apiType === "Gremlin") { this.onGraphDocumentsClick(); return; - } else if (this.container.isPreferredApiMongoDB()) { + } else if (userContext.apiType === "Mongo") { this.onMongoDBDocumentsClick(); return; } @@ -1141,7 +1137,7 @@ export default class Collection implements ViewModels.Collection { return "Rows"; } else if (userContext.apiType === "Gremlin") { return "Graph"; - } else if (this.container.isPreferredApiMongoDB()) { + } else if (userContext.apiType === "Mongo") { return "Documents"; } diff --git a/src/RouteHandlers/TabRouteHandler.ts b/src/RouteHandlers/TabRouteHandler.ts index 06a8d03ef..6795fae8b 100644 --- a/src/RouteHandlers/TabRouteHandler.ts +++ b/src/RouteHandlers/TabRouteHandler.ts @@ -175,10 +175,7 @@ export class TabRouteHandler { databaseId, collectionId ); - collection && - collection.container && - collection.container.isPreferredApiMongoDB() && - collection.onMongoDBDocumentsClick(); + userContext.apiType === "Mongo" && collection.onMongoDBDocumentsClick(); }); } @@ -188,10 +185,7 @@ export class TabRouteHandler { databaseId, collectionId ); - collection && - collection.container && - collection.container.isPreferredApiMongoDB() && - collection.onSchemaAnalyzerClick(); + collection && userContext.apiType === "Mongo" && collection.onSchemaAnalyzerClick(); }); } @@ -228,10 +222,7 @@ export class TabRouteHandler { if (!!matchingTab) { matchingTab.onTabClick(); } else { - collection && - collection.container && - collection.container.isPreferredApiMongoDB() && - collection.onNewMongoQueryClick(collection, null); + userContext.apiType === "Mongo" && collection.onNewMongoQueryClick(collection, null); } }); } @@ -250,10 +241,7 @@ export class TabRouteHandler { if (!!matchingTab) { matchingTab.onTabClick(); } else { - collection && - collection.container && - collection.container.isPreferredApiMongoDB() && - collection.onNewMongoShellClick(); + userContext.apiType === "Mongo" && collection.onNewMongoShellClick(); } }); } diff --git a/src/UserContext.ts b/src/UserContext.ts index 46dfb4f0a..fc5a02d2c 100644 --- a/src/UserContext.ts +++ b/src/UserContext.ts @@ -21,7 +21,7 @@ interface UserContext { readonly quotaId?: string; // API Type is not yet provided by ARM. You need to manually inspect all the capabilities+kind so we abstract that logic in userContext // This is coming in a future Cosmos ARM API version as a prperty on databaseAccount - readonly apiType?: ApiType; + apiType?: ApiType; readonly isTryCosmosDBSubscription?: boolean; readonly portalEnv?: PortalEnv; readonly features: Features; @@ -47,8 +47,10 @@ const userContext: UserContext = { }; function updateUserContext(newContext: Partial): void { + if (newContext.databaseAccount) { + newContext.apiType = apiType(newContext.databaseAccount); + } Object.assign(userContext, newContext); - Object.assign(userContext, { apiType: apiType(userContext.databaseAccount) }); } function apiType(account: DatabaseAccount | undefined): ApiType {