Prep Schema Analyzer for flighting (#760)

* Prepare for flighting Schema Analyzer

* Rename SchemaAnalyzerComponent -> SchemaAnalyzer

* Only show Schema option if notebooks enabled
This commit is contained in:
Tanuj Mittal 2021-05-13 10:34:09 -07:00 committed by GitHub
parent d7c62ac7f1
commit 404b1fc0f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 502 additions and 185 deletions

View File

@ -1,10 +1,15 @@
.schema-analyzer-cell-outputs { .schema-analyzer-cell-outputs {
padding: 10px; padding: 10px 2px;
} }
// Mimic FluentUI8's DocumentCard style
.schema-analyzer-cell-output { .schema-analyzer-cell-output {
margin-bottom: 20px; margin-bottom: 20px;
padding: 10px; padding: 14px 20px;
border-radius: 2px; border: 1px solid rgb(237, 235, 233);
box-shadow: rgba(0, 0, 0, 13%) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 11%) 0px 0.3px 0.9px 0px; }
.schema-analyzer-cell-output:hover {
border-color: rgb(200, 198, 196);
box-shadow: inset 0 0 0 1px rgb(200, 198, 196)
} }

View File

@ -94,6 +94,7 @@ export class Flights {
public static readonly MongoIndexEditor = "mongoindexeditor"; public static readonly MongoIndexEditor = "mongoindexeditor";
public static readonly MongoIndexing = "mongoindexing"; public static readonly MongoIndexing = "mongoindexing";
public static readonly AutoscaleTest = "autoscaletest"; public static readonly AutoscaleTest = "autoscaletest";
public static readonly SchemaAnalyzer = "schemaanalyzer";
} }
export class AfecFeatures { export class AfecFeatures {

View File

@ -163,7 +163,6 @@ export default class Explorer {
public isMongoIndexingEnabled: ko.Observable<boolean>; public isMongoIndexingEnabled: ko.Observable<boolean>;
public canExceedMaximumValue: ko.Computed<boolean>; public canExceedMaximumValue: ko.Computed<boolean>;
public isAutoscaleDefaultEnabled: ko.Observable<boolean>; public isAutoscaleDefaultEnabled: ko.Observable<boolean>;
public isSchemaEnabled: ko.Computed<boolean>; public isSchemaEnabled: ko.Computed<boolean>;
// Notebooks // Notebooks
@ -1048,6 +1047,9 @@ export default class Explorer {
if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) { if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) {
this.isMongoIndexingEnabled(true); this.isMongoIndexingEnabled(true);
} }
if (flights.indexOf(Constants.Flights.SchemaAnalyzer) !== -1) {
userContext.features.enableSchemaAnalyzer = true;
}
} }
public findSelectedCollection(): ViewModels.Collection { public findSelectedCollection(): ViewModels.Collection {

View File

@ -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<FileType> };
}
// 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<AjaxResponse> {
return this.errorResponse("Not implemented", "remove");
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public get(_config: ServerConfig, uri: string): Observable<AjaxResponse> {
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<AjaxResponse> {
return this.errorResponse("Not implemented", "update");
}
public create(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "create");
}
public save<FT extends FileType>(
_config: ServerConfig, // eslint-disable-line @typescript-eslint/no-unused-vars
uri: string,
model: Partial<IContent<FT>>
): Observable<AjaxResponse> {
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<AjaxResponse> {
return this.errorResponse("Not implemented", "listCheckpoints");
}
public createCheckpoint(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "createCheckpoint");
}
public deleteCheckpoint(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "deleteCheckpoint");
}
public restoreFromCheckpoint(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "restoreFromCheckpoint");
}
private errorResponse(message: string, functionName: string): Observable<AjaxResponse> {
const error = new InMemoryContentProviderError(message);
Logger.logError(error.message, `InMemoryContentProvider/${functionName}`, error.errno);
return of(this.createErrorAjaxResponse(error));
}
private createSuccessAjaxResponse(status: number, content: IContent<FileType>): 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",
};
}
}

View File

@ -0,0 +1,15 @@
// memory://<path>
// 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}`;
}

View File

@ -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 { Observable } from "rxjs";
import { AjaxResponse } from "rxjs/ajax"; import { AjaxResponse } from "rxjs/ajax";
import { GitHubContentProvider } from "../../../GitHub/GitHubContentProvider"; import { GitHubContentProvider } from "../../../GitHub/GitHubContentProvider";
import * as GitHubUtils from "../../../Utils/GitHubUtils"; import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { InMemoryContentProvider } from "./ContentProviders/InMemoryContentProvider";
import * as InMemoryContentProviderUtils from "./ContentProviders/InMemoryContentProviderUtils";
export class NotebookContentProvider implements IContentProvider { 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<AjaxResponse> { public remove(serverConfig: ServerConfig, path: string): Observable<AjaxResponse> {
return this.getContentProvider(path).remove(serverConfig, path); return this.getContentProvider(path).remove(serverConfig, path);
@ -60,6 +66,10 @@ export class NotebookContentProvider implements IContentProvider {
} }
private getContentProvider(path: string): IContentProvider { private getContentProvider(path: string): IContentProvider {
if (InMemoryContentProviderUtils.fromContentUri(path)) {
return this.inMemoryContentProvider;
}
if (GitHubUtils.fromContentUri(path)) { if (GitHubUtils.fromContentUri(path)) {
return this.gitHubContentProvider; return this.gitHubContentProvider;
} }

View File

@ -22,13 +22,14 @@ import { getFullName } from "../../Utils/UserUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { ContextualPaneBase } from "../Panes/ContextualPaneBase"; import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
// import { GitHubReposPane } from "../Panes/GitHubReposPane";
import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane"; import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { InMemoryContentProvider } from "./NotebookComponent/ContentProviders/InMemoryContentProvider";
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider"; import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
import { SnapshotRequest } from "./NotebookComponent/types"; import { SnapshotRequest } from "./NotebookComponent/types";
import { NotebookContainerClient } from "./NotebookContainerClient"; import { NotebookContainerClient } from "./NotebookContainerClient";
import { NotebookContentClient } from "./NotebookContentClient"; import { NotebookContentClient } from "./NotebookContentClient";
import { SchemaAnalyzerNotebook } from "./SchemaAnalyzer/SchemaAnalyzerUtils";
type NotebookPaneContent = string | ImmutableNotebook; type NotebookPaneContent = string | ImmutableNotebook;
@ -50,6 +51,7 @@ export default class NotebookManager {
public notebookClient: NotebookContainerClient; public notebookClient: NotebookContainerClient;
public notebookContentClient: NotebookContentClient; public notebookContentClient: NotebookContentClient;
private inMemoryContentProvider: InMemoryContentProvider;
private gitHubContentProvider: GitHubContentProvider; private gitHubContentProvider: GitHubContentProvider;
public gitHubOAuthService: GitHubOAuthService; public gitHubOAuthService: GitHubOAuthService;
public gitHubClient: GitHubClient; public gitHubClient: GitHubClient;
@ -63,12 +65,20 @@ export default class NotebookManager {
this.gitHubOAuthService = new GitHubOAuthService(this.junoClient); this.gitHubOAuthService = new GitHubOAuthService(this.junoClient);
this.gitHubClient = new GitHubClient(this.onGitHubClientError); this.gitHubClient = new GitHubClient(this.onGitHubClientError);
this.inMemoryContentProvider = new InMemoryContentProvider({
[SchemaAnalyzerNotebook.path]: {
readonly: true,
content: SchemaAnalyzerNotebook,
},
});
this.gitHubContentProvider = new GitHubContentProvider({ this.gitHubContentProvider = new GitHubContentProvider({
gitHubClient: this.gitHubClient, gitHubClient: this.gitHubClient,
promptForCommitMsg: this.promptForCommitMsg, promptForCommitMsg: this.promptForCommitMsg,
}); });
this.notebookContentProvider = new NotebookContentProvider( this.notebookContentProvider = new NotebookContentProvider(
this.inMemoryContentProvider,
this.gitHubContentProvider, this.gitHubContentProvider,
contents.JupyterContentProvider contents.JupyterContentProvider
); );

View File

@ -1,4 +1,4 @@
.schemaAnalyzerComponent { .schemaAnalyzer {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;

View File

@ -1,22 +1,26 @@
import { FontIcon, PrimaryButton, Spinner, SpinnerSize, Stack, Text, TextField } from "@fluentui/react"; import { Spinner, SpinnerSize, Stack } from "@fluentui/react";
import { ImmutableOutput } from "@nteract/commutable"; import { ImmutableExecuteResult, ImmutableOutput } from "@nteract/commutable";
import { actions, AppState, ContentRef, KernelRef, selectors } from "@nteract/core"; import { actions, AppState, ContentRef, KernelRef, selectors } from "@nteract/core";
import Immutable from "immutable"; import Immutable from "immutable";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "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 loadTransform from "../NotebookComponent/loadTransform";
import SandboxOutputs from "../NotebookRenderer/outputs/SandboxOutputs"; 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; contentRef: ContentRef;
kernelRef: KernelRef; kernelRef: KernelRef;
databaseId: string; databaseId: string;
collectionId: string; collectionId: string;
} }
interface SchemaAnalyzerComponentDispatchProps { interface SchemaAnalyzerDispatchProps {
runCell: (contentRef: ContentRef, cellId: string) => void; runCell: (contentRef: ContentRef, cellId: string) => void;
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void; addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void;
updateCell: (text: string, id: string, contentRef: ContentRef) => void; updateCell: (text: string, id: string, contentRef: ContentRef) => void;
@ -24,25 +28,23 @@ interface SchemaAnalyzerComponentDispatchProps {
type OutputType = "rich" | "json"; type OutputType = "rich" | "json";
interface SchemaAnalyzerComponentState { interface SchemaAnalyzerState {
outputType: OutputType; outputType: OutputType;
filter?: string;
isFiltering: boolean; isFiltering: boolean;
sampleSize: string;
} }
type SchemaAnalyzerComponentProps = SchemaAnalyzerComponentPureProps & type SchemaAnalyzerProps = SchemaAnalyzerPureProps & StateProps & SchemaAnalyzerDispatchProps;
StateProps &
SchemaAnalyzerComponentDispatchProps;
export class SchemaAnalyzerComponent extends React.Component< export class SchemaAnalyzer extends React.Component<SchemaAnalyzerProps, SchemaAnalyzerState> {
SchemaAnalyzerComponentProps, private clickAnalyzeTelemetryStartKey: number;
SchemaAnalyzerComponentState
> { constructor(props: SchemaAnalyzerProps) {
constructor(props: SchemaAnalyzerComponentProps) {
super(props); super(props);
this.state = { this.state = {
outputType: "rich", outputType: "rich",
isFiltering: false, isFiltering: false,
sampleSize: DefaultSampleSize,
}; };
} }
@ -50,34 +52,59 @@ export class SchemaAnalyzerComponent extends React.Component<
loadTransform(this.props); loadTransform(this.props);
} }
private onFilterTextFieldChange = ( private onAnalyzeButtonClick = (filter: string = DefaultFilter, sampleSize: string = this.state.sampleSize) => {
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
this.setState({
filter: newValue,
});
};
private onAnalyzeButtonClick = () => {
const query = { const query = {
command: "listSchema", command: "listSchema",
database: this.props.databaseId, database: this.props.databaseId,
collection: this.props.collectionId, collection: this.props.collectionId,
outputType: this.state.outputType, outputType: this.state.outputType,
filter: this.state.filter, filter,
sampleSize,
}; };
if (this.state.filter) {
this.setState({ this.setState({
isFiltering: true, isFiltering: true,
}); });
}
this.props.updateCell(JSON.stringify(query), this.props.firstCellId, this.props.contentRef); 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); this.props.runCell(this.props.contentRef, this.props.firstCellId);
}; };
private traceClickAnalyzeComplete = (kernelStatus: string, outputs: Immutable.List<ImmutableOutput>) => {
/**
* 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 { render(): JSX.Element {
const { firstCellId: id, contentRef, kernelStatus, outputs } = this.props; const { firstCellId: id, contentRef, kernelStatus, outputs } = this.props;
if (!id) { if (!id) {
@ -86,31 +113,22 @@ export class SchemaAnalyzerComponent extends React.Component<
const isKernelBusy = kernelStatus === "busy"; const isKernelBusy = kernelStatus === "busy";
const isKernelIdle = kernelStatus === "idle"; 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 ( return (
<div className="schemaAnalyzerComponent"> <div className="schemaAnalyzer">
<Stack horizontalAlign="center" tokens={{ childrenGap: 20, padding: 20 }}> <Stack tokens={{ childrenGap: 20, padding: 20 }}>
<Stack.Item grow styles={{ root: { display: "contents" } }}> <SchemaAnalyzerHeader
<Stack horizontal tokens={{ childrenGap: 20 }} styles={{ root: { width: "100%" } }}> isKernelIdle={isKernelIdle}
<Stack.Item grow align="end"> isKernelBusy={isKernelBusy}
<TextField onSampleSizeUpdated={(sampleSize) => this.setState({ sampleSize })}
value={this.state.filter} onAnalyzeButtonClick={this.onAnalyzeButtonClick}
onChange={this.onFilterTextFieldChange}
label="Filter"
placeholder="{ field: 'value' }"
disabled={!isKernelIdle}
/> />
</Stack.Item>
<Stack.Item align="end">
<PrimaryButton
text={isKernelBusy ? "Analyzing..." : "Analyze"}
onClick={this.onAnalyzeButtonClick}
disabled={!isKernelIdle}
/>
</Stack.Item>
</Stack>
</Stack.Item>
{showSchemaOutput ? ( {showSchemaOutput ? (
<SandboxOutputs <SandboxOutputs
@ -120,32 +138,13 @@ export class SchemaAnalyzerComponent extends React.Component<
outputClassName="schema-analyzer-cell-output" outputClassName="schema-analyzer-cell-output"
/> />
) : this.state.isFiltering ? ( ) : this.state.isFiltering ? (
<Stack.Item> <Spinner styles={{ root: { marginTop: 40 } }} size={SpinnerSize.large} />
{isKernelBusy && <Spinner styles={{ root: { marginTop: 40 } }} size={SpinnerSize.large} />}
</Stack.Item>
) : ( ) : (
<> <SchemaAnalyzerSplashScreen
<Stack.Item> isKernelIdle={isKernelIdle}
<FontIcon iconName="Chart" style={{ fontSize: 100, color: "#43B1E5", marginTop: 40 }} /> isKernelBusy={isKernelBusy}
</Stack.Item> onAnalyzeButtonClick={this.onAnalyzeButtonClick}
<Stack.Item>
<Text variant="xxLarge">Explore your schema</Text>
</Stack.Item>
<Stack.Item>
<Text variant="large">
Quickly visualize your schema to infer the frequency, types and ranges of fields in your data set.
</Text>
</Stack.Item>
<Stack.Item>
<PrimaryButton
styles={{ root: { fontSize: 18, padding: 30 } }}
text={isKernelBusy ? "Analyzing..." : "Analyze Schema"}
onClick={this.onAnalyzeButtonClick}
disabled={kernelStatus !== "idle"}
/> />
</Stack.Item>
<Stack.Item>{isKernelBusy && <Spinner size={SpinnerSize.large} />}</Stack.Item>
</>
)} )}
</Stack> </Stack>
</div> </div>
@ -229,4 +228,4 @@ const makeMapDispatchToProps = () => {
return mapDispatchToProps; return mapDispatchToProps;
}; };
export default connect(makeMapStateToProps, makeMapDispatchToProps)(SchemaAnalyzerComponent); export default connect(makeMapStateToProps, makeMapDispatchToProps)(SchemaAnalyzer);

View File

@ -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 (
<Provider store={this.getStore()}>
<SchemaAnalyzer {...props} />;
</Provider>
);
}
}

View File

@ -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<string>(DefaultFilter);
const [sampleSize, setSampleSize] = React.useState<string>(DefaultSampleSize);
return (
<Stack horizontal tokens={{ childrenGap: 10 }}>
<Stack.Item grow>
<TextField
value={filter}
onChange={(event, newValue) => setFilter(newValue)}
label="Filter"
placeholder={FilterPlaceholder}
disabled={!isKernelIdle}
/>
</Stack.Item>
<Stack.Item>
<TextField
value={sampleSize}
onChange={(event, newValue) => {
const num = Number(newValue);
if (!newValue || (num >= MinSampleSize && num <= MaxSampleSize)) {
setSampleSize(newValue);
onSampleSizeUpdated(newValue);
}
}}
label="Sample size"
onRenderLabel={onSampleSizeWrapDefaultLabelRenderer}
placeholder={SampleSizePlaceholder}
disabled={!isKernelIdle}
/>
</Stack.Item>
<Stack.Item align="end">
<PrimaryButton
text={isKernelBusy ? "Analyzing..." : "Analyze"}
onClick={() => {
const sampleSizeToUse = sampleSize || DefaultSampleSize;
setSampleSize(sampleSizeToUse);
onAnalyzeButtonClick(filter, sampleSizeToUse);
}}
disabled={!isKernelIdle}
styles={{ root: { width: 120 } }}
/>
</Stack.Item>
<Stack.Item align="end">
<DefaultButton
text="Reset"
disabled={!isKernelIdle}
onClick={() => {
setFilter(DefaultFilter);
setSampleSize(DefaultSampleSize);
}}
/>
</Stack.Item>
</Stack>
);
};
const onSampleSizeWrapDefaultLabelRenderer = (
props: ITextFieldProps,
defaultRender: IRenderFunction<ITextFieldProps>
): JSX.Element => {
return (
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
<span>{defaultRender(props)}</span>
<TooltipHost content={`Number of documents to sample between ${MinSampleSize} and ${MaxSampleSize}`}>
<Icon iconName="Info" ariaLabel="Info" />
</TooltipHost>
</Stack>
);
};

View File

@ -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 (
<Stack horizontalAlign="center" tokens={{ childrenGap: 20, padding: 20 }}>
<Stack.Item>
<FontIcon iconName="Chart" style={{ fontSize: 100, color: "#43B1E5", marginTop: 40 }} />
</Stack.Item>
<Stack.Item>
<Text variant="xxLarge">Explore your schema</Text>
</Stack.Item>
<Stack.Item>
<Text variant="large">
Quickly visualize your schema to infer the frequency, types and ranges of fields in your data set.
</Text>
</Stack.Item>
<Stack.Item>
<PrimaryButton
styles={{ root: { fontSize: 18, padding: 30 } }}
text={isKernelBusy ? "Analyzing..." : "Analyze Schema"}
onClick={() => onAnalyzeButtonClick()}
disabled={!isKernelIdle}
/>
</Stack.Item>
<Stack.Item>{isKernelBusy && <Spinner size={SpinnerSize.large} />}</Stack.Item>
</Stack>
);
};

View File

@ -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",
};

View File

@ -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 (
<Provider store={this.getStore()}>
<SchemaAnalyzerComponent {...props} />;
</Provider>
);
}
}

View File

@ -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"; import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
export default class SchemaAnalyzerTab extends NotebookTabBase { export default class SchemaAnalyzerTab extends NotebookTabBase {
public readonly html = '<div data-bind="react:schemaAnalyzerComponentAdapter" style="height: 100%"></div>'; public readonly html = '<div data-bind="react:schemaAnalyzerAdapter" style="height: 100%"></div>';
private schemaAnalyzerComponentAdapter: SchemaAnalyzerComponentAdapter; private schemaAnalyzerAdapter: SchemaAnalyzerAdapter;
constructor(options: NotebookTabBaseOptions) { constructor(options: NotebookTabBaseOptions) {
super(options); super(options);
this.schemaAnalyzerComponentAdapter = new SchemaAnalyzerComponentAdapter( this.schemaAnalyzerAdapter = new SchemaAnalyzerAdapter(
{ {
contentRef: undefined, contentRef: undefined,
notebookClient: NotebookTabBase.clientManager, 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 { protected buildCommandBarOptions(): void {
this.updateNavbarWithTabsButtons(); this.updateNavbarWithTabsButtons();
} }

View File

@ -511,7 +511,7 @@ export default class Collection implements ViewModels.Collection {
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer); this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default; const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default;
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Mongo Schema node", description: "Schema node",
databaseName: this.databaseId, databaseName: this.databaseId,
collectionName: this.id(), collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,

View File

@ -1,5 +1,5 @@
import * as ko from "knockout";
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import * as ko from "knockout";
import * as React from "react"; import * as React from "react";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import DeleteIcon from "../../../images/delete.svg"; import DeleteIcon from "../../../images/delete.svg";
@ -273,7 +273,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection), 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({ children.push({
label: "Schema (Preview)", label: "Schema (Preview)",
onClick: collection.onSchemaAnalyzerClick.bind(collection), onClick: collection.onSchemaAnalyzerClick.bind(collection),

View File

@ -8,7 +8,7 @@ export type Features = {
readonly enableReactPane: boolean; readonly enableReactPane: boolean;
readonly enableRightPanelV2: boolean; readonly enableRightPanelV2: boolean;
readonly enableSchema: boolean; readonly enableSchema: boolean;
readonly enableSchemaAnalyzer: boolean; enableSchemaAnalyzer: boolean;
readonly enableSDKoperations: boolean; readonly enableSDKoperations: boolean;
readonly enableSpark: boolean; readonly enableSpark: boolean;
readonly enableTtl: boolean; readonly enableTtl: boolean;

View File

@ -116,6 +116,7 @@ export enum Action {
NotebooksGalleryPublishedCount, NotebooksGalleryPublishedCount,
SelfServe, SelfServe,
ExpandAddCollectionPaneAdvancedSection, ExpandAddCollectionPaneAdvancedSection,
SchemaAnalyzerClickAnalyze,
} }
export const ActionModifiers = { export const ActionModifiers = {