From 404b1fc0f12c369fd6f55ed677ceaefe4a7e1324 Mon Sep 17 00:00:00 2001 From: Tanuj Mittal Date: Thu, 13 May 2021 10:34:09 -0700 Subject: [PATCH] Prep Schema Analyzer for flighting (#760) * Prepare for flighting Schema Analyzer * Rename SchemaAnalyzerComponent -> SchemaAnalyzer * Only show Schema option if notebooks enabled --- src/CellOutputViewer/CellOutputViewer.less | 13 +- src/Common/Constants.ts | 1 + src/Explorer/Explorer.tsx | 4 +- .../InMemoryContentProvider.ts | 108 ++++++++++++ .../InMemoryContentProviderUtils.ts | 15 ++ .../NotebookContentProvider.ts | 14 +- src/Explorer/Notebook/NotebookManager.tsx | 12 +- .../SchemaAnalyzer.less} | 2 +- .../SchemaAnalyzer.tsx} | 159 +++++++++--------- .../SchemaAnalyzer/SchemaAnalyzerAdapter.tsx | 48 ++++++ .../SchemaAnalyzer/SchemaAnalyzerHeader.tsx | 101 +++++++++++ .../SchemaAnalyzerSplashScreen.tsx | 39 +++++ .../SchemaAnalyzer/SchemaAnalyzerUtils.ts | 44 +++++ .../SchemaAnalyzerComponentAdapter.tsx | 88 ---------- src/Explorer/Tabs/SchemaAnalyzerTab.ts | 26 ++- src/Explorer/Tree/Collection.ts | 2 +- src/Explorer/Tree/ResourceTreeAdapter.tsx | 8 +- src/Platform/Hosted/extractFeatures.ts | 2 +- src/Shared/Telemetry/TelemetryConstants.ts | 1 + 19 files changed, 502 insertions(+), 185 deletions(-) create mode 100644 src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProvider.ts create mode 100644 src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProviderUtils.ts rename src/Explorer/Notebook/{SchemaAnalyzerComponent/SchemaAnalyzerComponent.less => SchemaAnalyzer/SchemaAnalyzer.less} (64%) rename src/Explorer/Notebook/{SchemaAnalyzerComponent/SchemaAnalyzerComponent.tsx => SchemaAnalyzer/SchemaAnalyzer.tsx} (55%) create mode 100644 src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerAdapter.tsx create mode 100644 src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerHeader.tsx create mode 100644 src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerSplashScreen.tsx create mode 100644 src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerUtils.ts delete mode 100644 src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponentAdapter.tsx diff --git a/src/CellOutputViewer/CellOutputViewer.less b/src/CellOutputViewer/CellOutputViewer.less index 2e79facc7..0be451c1a 100644 --- a/src/CellOutputViewer/CellOutputViewer.less +++ b/src/CellOutputViewer/CellOutputViewer.less @@ -1,10 +1,15 @@ .schema-analyzer-cell-outputs { - padding: 10px; + padding: 10px 2px; } +// Mimic FluentUI8's DocumentCard style .schema-analyzer-cell-output { margin-bottom: 20px; - padding: 10px; - border-radius: 2px; - box-shadow: rgba(0, 0, 0, 13%) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 11%) 0px 0.3px 0.9px 0px; + padding: 14px 20px; + border: 1px solid rgb(237, 235, 233); +} + +.schema-analyzer-cell-output:hover { + border-color: rgb(200, 198, 196); + box-shadow: inset 0 0 0 1px rgb(200, 198, 196) } \ No newline at end of file diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 097c4237f..31f537d8a 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -94,6 +94,7 @@ export class Flights { public static readonly MongoIndexEditor = "mongoindexeditor"; public static readonly MongoIndexing = "mongoindexing"; public static readonly AutoscaleTest = "autoscaletest"; + public static readonly SchemaAnalyzer = "schemaanalyzer"; } export class AfecFeatures { diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index b3451c0e7..c909c690a 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -163,7 +163,6 @@ export default class Explorer { public isMongoIndexingEnabled: ko.Observable; public canExceedMaximumValue: ko.Computed; public isAutoscaleDefaultEnabled: ko.Observable; - public isSchemaEnabled: ko.Computed; // Notebooks @@ -1048,6 +1047,9 @@ export default class Explorer { if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) { this.isMongoIndexingEnabled(true); } + if (flights.indexOf(Constants.Flights.SchemaAnalyzer) !== -1) { + userContext.features.enableSchemaAnalyzer = true; + } } public findSelectedCollection(): ViewModels.Collection { diff --git a/src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProvider.ts b/src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProvider.ts new file mode 100644 index 000000000..9d979324d --- /dev/null +++ b/src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProvider.ts @@ -0,0 +1,108 @@ +import { FileType, IContent, IContentProvider, ServerConfig } from "@nteract/core"; +import { Observable, of } from "rxjs"; +import { AjaxResponse } from "rxjs/ajax"; +import { HttpStatusCodes } from "../../../../Common/Constants"; +import { getErrorMessage } from "../../../../Common/ErrorHandlingUtils"; +import * as Logger from "../../../../Common/Logger"; + +export interface InMemoryContentProviderParams { + [path: string]: { readonly: boolean; content: IContent }; +} + +// Nteract relies on `errno` property to figure out the kind of failure +// That's why we need a custom wrapper around Error to include `errno` property +class InMemoryContentProviderError extends Error { + constructor(error: string, public errno: number = InMemoryContentProvider.SelfErrorCode) { + super(error); + } +} + +export class InMemoryContentProvider implements IContentProvider { + public static readonly SelfErrorCode = 666; + + constructor(private params: InMemoryContentProviderParams) {} + + public remove(): Observable { + return this.errorResponse("Not implemented", "remove"); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public get(_config: ServerConfig, uri: string): Observable { + const item = this.params[uri]; + if (item) { + return of(this.createSuccessAjaxResponse(HttpStatusCodes.OK, item.content)); + } + + return this.errorResponse(`${uri} not found`, "get"); + } + + public update(): Observable { + return this.errorResponse("Not implemented", "update"); + } + + public create(): Observable { + return this.errorResponse("Not implemented", "create"); + } + + public save( + _config: ServerConfig, // eslint-disable-line @typescript-eslint/no-unused-vars + uri: string, + model: Partial> + ): Observable { + const item = this.params[uri]; + if (item) { + if (!item.readonly) { + Object.assign(item.content, model); + } + return of(this.createSuccessAjaxResponse(HttpStatusCodes.OK, item.content)); + } + + return this.errorResponse(`${uri} not found`, "save"); + } + + public listCheckpoints(): Observable { + return this.errorResponse("Not implemented", "listCheckpoints"); + } + + public createCheckpoint(): Observable { + return this.errorResponse("Not implemented", "createCheckpoint"); + } + + public deleteCheckpoint(): Observable { + return this.errorResponse("Not implemented", "deleteCheckpoint"); + } + + public restoreFromCheckpoint(): Observable { + return this.errorResponse("Not implemented", "restoreFromCheckpoint"); + } + + private errorResponse(message: string, functionName: string): Observable { + const error = new InMemoryContentProviderError(message); + Logger.logError(error.message, `InMemoryContentProvider/${functionName}`, error.errno); + return of(this.createErrorAjaxResponse(error)); + } + + private createSuccessAjaxResponse(status: number, content: IContent): AjaxResponse { + return { + originalEvent: new Event("no-op"), + xhr: new XMLHttpRequest(), + request: {}, + status, + response: content ? content : undefined, + responseText: content ? JSON.stringify(content) : undefined, + responseType: "json", + }; + } + + private createErrorAjaxResponse(error: InMemoryContentProviderError): AjaxResponse { + return { + originalEvent: new Event("no-op"), + xhr: new XMLHttpRequest(), + request: {}, + status: error.errno, + response: error, + responseText: getErrorMessage(error), + responseType: "json", + }; + } +} diff --git a/src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProviderUtils.ts b/src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProviderUtils.ts new file mode 100644 index 000000000..1a4d6c150 --- /dev/null +++ b/src/Explorer/Notebook/NotebookComponent/ContentProviders/InMemoryContentProviderUtils.ts @@ -0,0 +1,15 @@ +// memory:// +// Custom scheme for in memory content +export const ContentUriPattern = /memory:\/\/([^/]*)/; + +export function fromContentUri(contentUri: string): undefined | string { + const matches = contentUri.match(ContentUriPattern); + if (matches && matches.length > 1) { + return matches[1]; + } + return undefined; +} + +export function toContentUri(path: string): string { + return `memory://${path}`; +} diff --git a/src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts b/src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts index dd16dc364..a40f30979 100644 --- a/src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts +++ b/src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts @@ -1,11 +1,17 @@ -import { ServerConfig, IContentProvider, FileType, IContent, IGetParams } from "@nteract/core"; +import { FileType, IContent, IContentProvider, IGetParams, ServerConfig } from "@nteract/core"; import { Observable } from "rxjs"; import { AjaxResponse } from "rxjs/ajax"; import { GitHubContentProvider } from "../../../GitHub/GitHubContentProvider"; import * as GitHubUtils from "../../../Utils/GitHubUtils"; +import { InMemoryContentProvider } from "./ContentProviders/InMemoryContentProvider"; +import * as InMemoryContentProviderUtils from "./ContentProviders/InMemoryContentProviderUtils"; export class NotebookContentProvider implements IContentProvider { - constructor(private gitHubContentProvider: GitHubContentProvider, private jupyterContentProvider: IContentProvider) {} + constructor( + private inMemoryContentProvider: InMemoryContentProvider, + private gitHubContentProvider: GitHubContentProvider, + private jupyterContentProvider: IContentProvider + ) {} public remove(serverConfig: ServerConfig, path: string): Observable { return this.getContentProvider(path).remove(serverConfig, path); @@ -60,6 +66,10 @@ export class NotebookContentProvider implements IContentProvider { } private getContentProvider(path: string): IContentProvider { + if (InMemoryContentProviderUtils.fromContentUri(path)) { + return this.inMemoryContentProvider; + } + if (GitHubUtils.fromContentUri(path)) { return this.gitHubContentProvider; } diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index 7be295b51..a0f09e3ea 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -22,13 +22,14 @@ import { getFullName } from "../../Utils/UserUtils"; import Explorer from "../Explorer"; import { ContextualPaneBase } from "../Panes/ContextualPaneBase"; import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; -// import { GitHubReposPane } from "../Panes/GitHubReposPane"; import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; +import { InMemoryContentProvider } from "./NotebookComponent/ContentProviders/InMemoryContentProvider"; import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider"; import { SnapshotRequest } from "./NotebookComponent/types"; import { NotebookContainerClient } from "./NotebookContainerClient"; import { NotebookContentClient } from "./NotebookContentClient"; +import { SchemaAnalyzerNotebook } from "./SchemaAnalyzer/SchemaAnalyzerUtils"; type NotebookPaneContent = string | ImmutableNotebook; @@ -50,6 +51,7 @@ export default class NotebookManager { public notebookClient: NotebookContainerClient; public notebookContentClient: NotebookContentClient; + private inMemoryContentProvider: InMemoryContentProvider; private gitHubContentProvider: GitHubContentProvider; public gitHubOAuthService: GitHubOAuthService; public gitHubClient: GitHubClient; @@ -63,12 +65,20 @@ export default class NotebookManager { this.gitHubOAuthService = new GitHubOAuthService(this.junoClient); this.gitHubClient = new GitHubClient(this.onGitHubClientError); + this.inMemoryContentProvider = new InMemoryContentProvider({ + [SchemaAnalyzerNotebook.path]: { + readonly: true, + content: SchemaAnalyzerNotebook, + }, + }); + this.gitHubContentProvider = new GitHubContentProvider({ gitHubClient: this.gitHubClient, promptForCommitMsg: this.promptForCommitMsg, }); this.notebookContentProvider = new NotebookContentProvider( + this.inMemoryContentProvider, this.gitHubContentProvider, contents.JupyterContentProvider ); diff --git a/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.less b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzer.less similarity index 64% rename from src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.less rename to src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzer.less index 1a077c600..9d42c852f 100644 --- a/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.less +++ b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzer.less @@ -1,4 +1,4 @@ -.schemaAnalyzerComponent { +.schemaAnalyzer { width: 100%; height: 100%; overflow-y: auto; diff --git a/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.tsx b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzer.tsx similarity index 55% rename from src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.tsx rename to src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzer.tsx index 063050b01..cf9810f38 100644 --- a/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.tsx +++ b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzer.tsx @@ -1,22 +1,26 @@ -import { FontIcon, PrimaryButton, Spinner, SpinnerSize, Stack, Text, TextField } from "@fluentui/react"; -import { ImmutableOutput } from "@nteract/commutable"; +import { Spinner, SpinnerSize, Stack } from "@fluentui/react"; +import { ImmutableExecuteResult, ImmutableOutput } from "@nteract/commutable"; import { actions, AppState, ContentRef, KernelRef, selectors } from "@nteract/core"; import Immutable from "immutable"; import * as React from "react"; import { connect } from "react-redux"; import { Dispatch } from "redux"; +import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; +import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor"; import loadTransform from "../NotebookComponent/loadTransform"; import SandboxOutputs from "../NotebookRenderer/outputs/SandboxOutputs"; -import "./SchemaAnalyzerComponent.less"; +import "./SchemaAnalyzer.less"; +import { DefaultFilter, DefaultSampleSize, SchemaAnalyzerHeader } from "./SchemaAnalyzerHeader"; +import { SchemaAnalyzerSplashScreen } from "./SchemaAnalyzerSplashScreen"; -interface SchemaAnalyzerComponentPureProps { +interface SchemaAnalyzerPureProps { contentRef: ContentRef; kernelRef: KernelRef; databaseId: string; collectionId: string; } -interface SchemaAnalyzerComponentDispatchProps { +interface SchemaAnalyzerDispatchProps { runCell: (contentRef: ContentRef, cellId: string) => void; addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void; updateCell: (text: string, id: string, contentRef: ContentRef) => void; @@ -24,25 +28,23 @@ interface SchemaAnalyzerComponentDispatchProps { type OutputType = "rich" | "json"; -interface SchemaAnalyzerComponentState { +interface SchemaAnalyzerState { outputType: OutputType; - filter?: string; isFiltering: boolean; + sampleSize: string; } -type SchemaAnalyzerComponentProps = SchemaAnalyzerComponentPureProps & - StateProps & - SchemaAnalyzerComponentDispatchProps; +type SchemaAnalyzerProps = SchemaAnalyzerPureProps & StateProps & SchemaAnalyzerDispatchProps; -export class SchemaAnalyzerComponent extends React.Component< - SchemaAnalyzerComponentProps, - SchemaAnalyzerComponentState -> { - constructor(props: SchemaAnalyzerComponentProps) { +export class SchemaAnalyzer extends React.Component { + private clickAnalyzeTelemetryStartKey: number; + + constructor(props: SchemaAnalyzerProps) { super(props); this.state = { outputType: "rich", isFiltering: false, + sampleSize: DefaultSampleSize, }; } @@ -50,34 +52,59 @@ export class SchemaAnalyzerComponent extends React.Component< loadTransform(this.props); } - private onFilterTextFieldChange = ( - event: React.FormEvent, - newValue?: string - ): void => { - this.setState({ - filter: newValue, - }); - }; - - private onAnalyzeButtonClick = () => { + private onAnalyzeButtonClick = (filter: string = DefaultFilter, sampleSize: string = this.state.sampleSize) => { const query = { command: "listSchema", database: this.props.databaseId, collection: this.props.collectionId, outputType: this.state.outputType, - filter: this.state.filter, + filter, + sampleSize, }; - if (this.state.filter) { - this.setState({ - isFiltering: true, - }); - } + this.setState({ + isFiltering: true, + }); this.props.updateCell(JSON.stringify(query), this.props.firstCellId, this.props.contentRef); + + this.clickAnalyzeTelemetryStartKey = traceStart(Action.SchemaAnalyzerClickAnalyze, { + database: this.props.databaseId, + collection: this.props.collectionId, + sampleSize, + }); + this.props.runCell(this.props.contentRef, this.props.firstCellId); }; + private traceClickAnalyzeComplete = (kernelStatus: string, outputs: Immutable.List) => { + /** + * CosmosMongoKernel always returns 1st output as "text/html" + * This output can be an error stack or information about how many documents were sampled + */ + let firstTextHtmlOutput: string; + if (outputs.size > 0 && outputs.get(0).output_type === "execute_result") { + const executeResult = outputs.get(0) as ImmutableExecuteResult; + firstTextHtmlOutput = executeResult.data["text/html"]; + } + + const data = { + database: this.props.databaseId, + collection: this.props.collectionId, + firstTextHtmlOutput, + sampleSize: this.state.sampleSize, + numOfOutputs: outputs.size, + kernelStatus, + }; + + // Only in cases where CosmosMongoKernel runs into an error we get a single output + if (outputs.size === 1) { + traceFailure(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey); + } else { + traceSuccess(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey); + } + }; + render(): JSX.Element { const { firstCellId: id, contentRef, kernelStatus, outputs } = this.props; if (!id) { @@ -86,31 +113,22 @@ export class SchemaAnalyzerComponent extends React.Component< const isKernelBusy = kernelStatus === "busy"; const isKernelIdle = kernelStatus === "idle"; - const showSchemaOutput = isKernelIdle && outputs.size > 0; + const showSchemaOutput = isKernelIdle && outputs?.size > 0; + + if (showSchemaOutput && this.clickAnalyzeTelemetryStartKey) { + this.traceClickAnalyzeComplete(kernelStatus, outputs); + this.clickAnalyzeTelemetryStartKey = undefined; + } return ( -
- - - - - - - - - - - +
+ + this.setState({ sampleSize })} + onAnalyzeButtonClick={this.onAnalyzeButtonClick} + /> {showSchemaOutput ? ( ) : this.state.isFiltering ? ( - - {isKernelBusy && } - + ) : ( - <> - - - - - Explore your schema - - - - Quickly visualize your schema to infer the frequency, types and ranges of fields in your data set. - - - - - - {isKernelBusy && } - + )}
@@ -229,4 +228,4 @@ const makeMapDispatchToProps = () => { return mapDispatchToProps; }; -export default connect(makeMapStateToProps, makeMapDispatchToProps)(SchemaAnalyzerComponent); +export default connect(makeMapStateToProps, makeMapDispatchToProps)(SchemaAnalyzer); diff --git a/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerAdapter.tsx b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerAdapter.tsx new file mode 100644 index 000000000..4eea4afbb --- /dev/null +++ b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerAdapter.tsx @@ -0,0 +1,48 @@ +import { actions, createContentRef, createKernelRef, KernelRef } from "@nteract/core"; +import * as React from "react"; +import { Provider } from "react-redux"; +import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; +import { + NotebookComponentBootstrapper, + NotebookComponentBootstrapperOptions, +} from "../NotebookComponent/NotebookComponentBootstrapper"; +import SchemaAnalyzer from "./SchemaAnalyzer"; +import { SchemaAnalyzerNotebook } from "./SchemaAnalyzerUtils"; + +export class SchemaAnalyzerAdapter extends NotebookComponentBootstrapper implements ReactAdapter { + public parameters: unknown; + private kernelRef: KernelRef; + + constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) { + super(options); + + if (!this.contentRef) { + this.contentRef = createContentRef(); + this.kernelRef = createKernelRef(); + + this.getStore().dispatch( + actions.fetchContent({ + filepath: SchemaAnalyzerNotebook.path, + params: {}, + kernelRef: this.kernelRef, + contentRef: this.contentRef, + }) + ); + } + } + + public renderComponent(): JSX.Element { + const props = { + contentRef: this.contentRef, + kernelRef: this.kernelRef, + databaseId: this.databaseId, + collectionId: this.collectionId, + }; + + return ( + + ; + + ); + } +} diff --git a/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerHeader.tsx b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerHeader.tsx new file mode 100644 index 000000000..2c91fdcaa --- /dev/null +++ b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerHeader.tsx @@ -0,0 +1,101 @@ +import { + DefaultButton, + Icon, + IRenderFunction, + ITextFieldProps, + PrimaryButton, + Stack, + TextField, + TooltipHost, +} from "@fluentui/react"; +import * as React from "react"; + +type SchemaAnalyzerHeaderProps = { + isKernelIdle: boolean; + isKernelBusy: boolean; + onSampleSizeUpdated: (sampleSize: string) => void; + onAnalyzeButtonClick: (filter: string, sampleSize: string) => void; +}; + +export const DefaultFilter = ""; +export const DefaultSampleSize = "1000"; +const FilterPlaceholder = "{ field: 'value' }"; +const SampleSizePlaceholder = "1000"; +const MinSampleSize = 1; +const MaxSampleSize = 5000; + +export const SchemaAnalyzerHeader = ({ + isKernelIdle, + isKernelBusy, + onSampleSizeUpdated, + onAnalyzeButtonClick, +}: SchemaAnalyzerHeaderProps): JSX.Element => { + const [filter, setFilter] = React.useState(DefaultFilter); + const [sampleSize, setSampleSize] = React.useState(DefaultSampleSize); + + return ( + + + setFilter(newValue)} + label="Filter" + placeholder={FilterPlaceholder} + disabled={!isKernelIdle} + /> + + + { + const num = Number(newValue); + if (!newValue || (num >= MinSampleSize && num <= MaxSampleSize)) { + setSampleSize(newValue); + onSampleSizeUpdated(newValue); + } + }} + label="Sample size" + onRenderLabel={onSampleSizeWrapDefaultLabelRenderer} + placeholder={SampleSizePlaceholder} + disabled={!isKernelIdle} + /> + + + { + const sampleSizeToUse = sampleSize || DefaultSampleSize; + setSampleSize(sampleSizeToUse); + onAnalyzeButtonClick(filter, sampleSizeToUse); + }} + disabled={!isKernelIdle} + styles={{ root: { width: 120 } }} + /> + + + { + setFilter(DefaultFilter); + setSampleSize(DefaultSampleSize); + }} + /> + + + ); +}; + +const onSampleSizeWrapDefaultLabelRenderer = ( + props: ITextFieldProps, + defaultRender: IRenderFunction +): JSX.Element => { + return ( + + {defaultRender(props)} + + + + + ); +}; diff --git a/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerSplashScreen.tsx b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerSplashScreen.tsx new file mode 100644 index 000000000..a479828e7 --- /dev/null +++ b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerSplashScreen.tsx @@ -0,0 +1,39 @@ +import { FontIcon, PrimaryButton, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react"; +import * as React from "react"; + +type SchemaAnalyzerSplashScreenProps = { + isKernelIdle: boolean; + isKernelBusy: boolean; + onAnalyzeButtonClick: () => void; +}; + +export const SchemaAnalyzerSplashScreen = ({ + isKernelIdle, + isKernelBusy, + onAnalyzeButtonClick, +}: SchemaAnalyzerSplashScreenProps): JSX.Element => { + return ( + + + + + + Explore your schema + + + + Quickly visualize your schema to infer the frequency, types and ranges of fields in your data set. + + + + onAnalyzeButtonClick()} + disabled={!isKernelIdle} + /> + + {isKernelBusy && } + + ); +}; diff --git a/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerUtils.ts b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerUtils.ts new file mode 100644 index 000000000..a422a6a58 --- /dev/null +++ b/src/Explorer/Notebook/SchemaAnalyzer/SchemaAnalyzerUtils.ts @@ -0,0 +1,44 @@ +import { Notebook } from "@nteract/commutable"; +import { IContent } from "@nteract/types"; +import * as InMemoryContentProviderUtils from "../NotebookComponent/ContentProviders/InMemoryContentProviderUtils"; + +const notebookName = "schema-analyzer-component-notebook.ipynb"; +const notebookPath = InMemoryContentProviderUtils.toContentUri(notebookName); +const notebook: Notebook = { + cells: [ + { + cell_type: "code", + metadata: {}, + execution_count: 0, + outputs: [], + source: "", + }, + ], + metadata: { + kernelspec: { + displayName: "Mongo", + language: "mongocli", + name: "mongo", + }, + language_info: { + file_extension: "ipynb", + mimetype: "application/json", + name: "mongo", + version: "1.0", + }, + }, + nbformat: 4, + nbformat_minor: 4, +}; + +export const SchemaAnalyzerNotebook: IContent<"notebook"> = { + name: notebookName, + path: notebookPath, + type: "notebook", + writable: true, + created: "", + last_modified: "", + mimetype: "application/x-ipynb+json", + content: notebook, + format: "json", +}; diff --git a/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponentAdapter.tsx b/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponentAdapter.tsx deleted file mode 100644 index 75b221b88..000000000 --- a/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponentAdapter.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Notebook } from "@nteract/commutable"; -import { actions, createContentRef, createKernelRef, IContent, KernelRef } from "@nteract/core"; -import * as React from "react"; -import { Provider } from "react-redux"; -import { ReactAdapter } from "../../../Bindings/ReactBindingHandler"; -import { - NotebookComponentBootstrapper, - NotebookComponentBootstrapperOptions, -} from "../NotebookComponent/NotebookComponentBootstrapper"; -import SchemaAnalyzerComponent from "./SchemaAnalyzerComponent"; - -export class SchemaAnalyzerComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter { - public parameters: unknown; - private kernelRef: KernelRef; - - constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) { - super(options); - - if (!this.contentRef) { - this.contentRef = createContentRef(); - this.kernelRef = createKernelRef(); - - const notebook: Notebook = { - cells: [ - { - cell_type: "code", - metadata: {}, - execution_count: 0, - outputs: [], - source: "", - }, - ], - metadata: { - kernelspec: { - displayName: "Mongo", - language: "mongocli", - name: "mongo", - }, - language_info: { - file_extension: "ipynb", - mimetype: "application/json", - name: "mongo", - version: "1.0", - }, - }, - nbformat: 4, - nbformat_minor: 4, - }; - - const model: IContent<"notebook"> = { - name: "schema-analyzer-component-notebook.ipynb", - path: "schema-analyzer-component-notebook.ipynb", - type: "notebook", - writable: true, - created: "", - last_modified: "", - mimetype: "application/x-ipynb+json", - content: notebook, - format: "json", - }; - - // Request fetching notebook content - this.getStore().dispatch( - actions.fetchContentFulfilled({ - filepath: model.path, - model, - kernelRef: this.kernelRef, - contentRef: this.contentRef, - }) - ); - } - } - - public renderComponent(): JSX.Element { - const props = { - contentRef: this.contentRef, - kernelRef: this.kernelRef, - databaseId: this.databaseId, - collectionId: this.collectionId, - }; - - return ( - - ; - - ); - } -} diff --git a/src/Explorer/Tabs/SchemaAnalyzerTab.ts b/src/Explorer/Tabs/SchemaAnalyzerTab.ts index 0e3aa8418..4bd0079da 100644 --- a/src/Explorer/Tabs/SchemaAnalyzerTab.ts +++ b/src/Explorer/Tabs/SchemaAnalyzerTab.ts @@ -1,13 +1,16 @@ -import { SchemaAnalyzerComponentAdapter } from "../Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponentAdapter"; +import * as Constants from "../../Common/Constants"; +import { Action } from "../../Shared/Telemetry/TelemetryConstants"; +import { traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor"; +import { SchemaAnalyzerAdapter } from "../Notebook/SchemaAnalyzer/SchemaAnalyzerAdapter"; import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase"; export default class SchemaAnalyzerTab extends NotebookTabBase { - public readonly html = '
'; - private schemaAnalyzerComponentAdapter: SchemaAnalyzerComponentAdapter; + public readonly html = '
'; + private schemaAnalyzerAdapter: SchemaAnalyzerAdapter; constructor(options: NotebookTabBaseOptions) { super(options); - this.schemaAnalyzerComponentAdapter = new SchemaAnalyzerComponentAdapter( + this.schemaAnalyzerAdapter = new SchemaAnalyzerAdapter( { contentRef: undefined, notebookClient: NotebookTabBase.clientManager, @@ -17,6 +20,21 @@ export default class SchemaAnalyzerTab extends NotebookTabBase { ); } + public onActivate(): void { + traceSuccess( + Action.Tab, + { + databaseName: this.collection?.databaseId, + collectionName: this.collection?.id, + dataExplorerArea: Constants.Areas.Tab, + tabTitle: "Schema", + }, + this.onLoadStartKey + ); + + super.onActivate(); + } + protected buildCommandBarOptions(): void { this.updateNavbarWithTabsButtons(); } diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index b7820d35d..1239b1257 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -511,7 +511,7 @@ export default class Collection implements ViewModels.Collection { this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer); const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default; TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { - description: "Mongo Schema node", + description: "Schema node", databaseName: this.databaseId, collectionName: this.id(), dataExplorerArea: Constants.Areas.ResourceTree, diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index ed4b6adbe..3d7281acc 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -1,5 +1,5 @@ -import * as ko from "knockout"; import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; +import * as ko from "knockout"; import * as React from "react"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import DeleteIcon from "../../../images/delete.svg"; @@ -273,7 +273,11 @@ export class ResourceTreeAdapter implements ReactAdapter { contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection), }); - if (userContext.apiType === "Mongo" && userContext.features.enableSchemaAnalyzer) { + if ( + userContext.apiType === "Mongo" && + this.container.isNotebookEnabled() && + userContext.features.enableSchemaAnalyzer + ) { children.push({ label: "Schema (Preview)", onClick: collection.onSchemaAnalyzerClick.bind(collection), diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index b7f62e87e..18ff57cc2 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -8,7 +8,7 @@ export type Features = { readonly enableReactPane: boolean; readonly enableRightPanelV2: boolean; readonly enableSchema: boolean; - readonly enableSchemaAnalyzer: boolean; + enableSchemaAnalyzer: boolean; readonly enableSDKoperations: boolean; readonly enableSpark: boolean; readonly enableTtl: boolean; diff --git a/src/Shared/Telemetry/TelemetryConstants.ts b/src/Shared/Telemetry/TelemetryConstants.ts index 5500118ce..058937df9 100644 --- a/src/Shared/Telemetry/TelemetryConstants.ts +++ b/src/Shared/Telemetry/TelemetryConstants.ts @@ -116,6 +116,7 @@ export enum Action { NotebooksGalleryPublishedCount, SelfServe, ExpandAddCollectionPaneAdvancedSection, + SchemaAnalyzerClickAnalyze, } export const ActionModifiers = {