Add support for Schema Analyzer (#411)

* New MongoSchemaTab

* Address feedback and updates

* Build fixes

* Rename to SchemaAnalyzer

* Format

Co-authored-by: Laurent Nguyen <laurent.nguyen@microsoft.com>
This commit is contained in:
Tanuj Mittal 2021-04-22 21:45:21 -04:00 committed by GitHub
parent 448566146f
commit 5ecc3d67b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 518 additions and 40 deletions

View File

@ -4,6 +4,7 @@
export enum TabKind { export enum TabKind {
SQLDocuments, SQLDocuments,
MongoDocuments, MongoDocuments,
SchemaAnalyzer,
TableEntities, TableEntities,
Graph, Graph,
SQLQuery, SQLQuery,

View File

@ -141,6 +141,7 @@ export interface Collection extends CollectionBase {
onTableEntitiesClick(): void; onTableEntitiesClick(): void;
onGraphDocumentsClick(): void; onGraphDocumentsClick(): void;
onMongoDBDocumentsClick(): void; onMongoDBDocumentsClick(): void;
onSchemaAnalyzerClick(): void;
openTab(): void; openTab(): void;
onSettingsClick: () => Promise<void>; onSettingsClick: () => Promise<void>;
@ -366,6 +367,7 @@ export enum CollectionTabKind {
Schema = 19, Schema = 19,
CollectionSettingsV2 = 20, CollectionSettingsV2 = 20,
DatabaseSettingsV2 = 21, DatabaseSettingsV2 = 21,
SchemaAnalyzer = 22,
} }
export enum TerminalKind { export enum TerminalKind {

View File

@ -17,6 +17,7 @@ import NotebookTabV2 from "./Tabs/NotebookV2Tab";
import NotebookViewerTab from "./Tabs/NotebookViewerTab"; import NotebookViewerTab from "./Tabs/NotebookViewerTab";
import QueryTab from "./Tabs/QueryTab"; import QueryTab from "./Tabs/QueryTab";
import QueryTablesTab from "./Tabs/QueryTablesTab"; import QueryTablesTab from "./Tabs/QueryTablesTab";
import SchemaAnalyzerTab from "./Tabs/SchemaAnalyzerTab";
import { DatabaseSettingsTabV2, SettingsTabV2 } from "./Tabs/SettingsTabV2"; import { DatabaseSettingsTabV2, SettingsTabV2 } from "./Tabs/SettingsTabV2";
import StoredProcedureTab from "./Tabs/StoredProcedureTab"; import StoredProcedureTab from "./Tabs/StoredProcedureTab";
import TerminalTab from "./Tabs/TerminalTab"; import TerminalTab from "./Tabs/TerminalTab";
@ -49,6 +50,7 @@ ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponent
GalleryTab, GalleryTab,
NotebookViewerTab, NotebookViewerTab,
DatabaseSettingsTabV2, DatabaseSettingsTabV2,
SchemaAnalyzerTab,
].forEach(({ component: { name, template } }) => ko.components.register(name, { template })); ].forEach(({ component: { name, template } }) => ko.components.register(name, { template }));
// Panes // Panes

View File

@ -200,10 +200,11 @@ export class NotebookClientV2 {
case actions.FETCH_KERNELSPECS_FULFILLED: { case actions.FETCH_KERNELSPECS_FULFILLED: {
const payload = ((action as unknown) as actions.FetchKernelspecsFulfilled).payload; const payload = ((action as unknown) as actions.FetchKernelspecsFulfilled).payload;
const defaultKernelName = payload.defaultKernelName; const defaultKernelName = payload.defaultKernelName;
this.kernelSpecsForDisplay = Object.keys(payload.kernelspecs) this.kernelSpecsForDisplay = Object.values(payload.kernelspecs)
.map((name) => ({ .filter((spec) => !spec.metadata?.hasOwnProperty("hidden"))
name, .map((spec) => ({
displayName: payload.kernelspecs[name].displayName, name: spec.name,
displayName: spec.displayName,
})) }))
.sort((a: KernelSpecsDisplay, b: KernelSpecsDisplay) => { .sort((a: KernelSpecsDisplay, b: KernelSpecsDisplay) => {
// Put default at the top, otherwise lexicographically compare // Put default at the top, otherwise lexicographically compare

View File

@ -0,0 +1,10 @@
.shemaAnalyzerComponent {
width: 100%;
height: 100%;
overflow-y: auto;
}
.schemaAnalyzerCard {
max-width: 4096px;
width: 100%;
}

View File

@ -0,0 +1,238 @@
import { ImmutableOutput } from "@nteract/commutable";
import { actions, AppState, ContentRef, KernelRef, selectors } from "@nteract/core";
import { KernelOutputError, Output, StreamText } from "@nteract/outputs";
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
import { Card } from "@uifabric/react-cards";
import Immutable from "immutable";
import { FontIcon, PrimaryButton, Spinner, SpinnerSize, Stack, Text, TextField } from "office-ui-fabric-react";
import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import loadTransform from "../NotebookComponent/loadTransform";
import "./SchemaAnalyzerComponent.less";
interface SchemaAnalyzerComponentPureProps {
contentRef: ContentRef;
kernelRef: KernelRef;
databaseId: string;
collectionId: string;
}
interface SchemaAnalyzerComponentDispatchProps {
runCell: (contentRef: ContentRef, cellId: string) => void;
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void;
updateCell: (text: string, id: string, contentRef: ContentRef) => void;
}
type OutputType = "rich" | "json";
interface SchemaAnalyzerComponentState {
outputType: OutputType;
filter?: string;
isFiltering: boolean;
}
type SchemaAnalyzerComponentProps = SchemaAnalyzerComponentPureProps &
StateProps &
SchemaAnalyzerComponentDispatchProps;
export class SchemaAnalyzerComponent extends React.Component<
SchemaAnalyzerComponentProps,
SchemaAnalyzerComponentState
> {
constructor(props: SchemaAnalyzerComponentProps) {
super(props);
this.state = {
outputType: "rich",
isFiltering: false,
};
}
componentDidMount(): void {
loadTransform(this.props);
}
private onFilterTextFieldChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
this.setState({
filter: newValue,
});
};
private onAnalyzeButtonClick = () => {
const query = {
command: "listSchema",
database: this.props.databaseId,
collection: this.props.collectionId,
outputType: this.state.outputType,
filter: this.state.filter,
};
if (this.state.filter) {
this.setState({
isFiltering: true,
});
}
this.props.updateCell(JSON.stringify(query), this.props.firstCellId, this.props.contentRef);
this.props.runCell(this.props.contentRef, this.props.firstCellId);
};
render(): JSX.Element {
const { firstCellId: id, contentRef, kernelStatus, outputs } = this.props;
if (!id) {
return <></>;
}
const isKernelBusy = kernelStatus === "busy";
const isKernelIdle = kernelStatus === "idle";
const showSchemaOutput = isKernelIdle && outputs.size > 0;
return (
<Stack className="schemaAnalyzerComponent" horizontalAlign="center" tokens={{ childrenGap: 20, padding: 20 }}>
<Stack.Item grow styles={{ root: { display: "contents" } }}>
<Stack horizontal tokens={{ childrenGap: 20 }} styles={{ root: { width: "100%" } }}>
<Stack.Item grow align="end">
<TextField
value={this.state.filter}
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 ? (
outputs.map((output, index) => (
<Card className="schemaAnalyzerCard" key={index}>
<Card.Item tokens={{ padding: 10 }}>
<Output output={output}>
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
<KernelOutputError />
<StreamText />
</Output>
</Card.Item>
</Card>
))
) : this.state.isFiltering ? (
<Stack.Item>
{isKernelBusy && <Spinner styles={{ root: { marginTop: 40 } }} size={SpinnerSize.large} />}
</Stack.Item>
) : (
<>
<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={this.onAnalyzeButtonClick}
disabled={kernelStatus !== "idle"}
/>
</Stack.Item>
<Stack.Item>{isKernelBusy && <Spinner size={SpinnerSize.large} />}</Stack.Item>
</>
)}
</Stack>
);
}
}
interface StateProps {
firstCellId: string;
kernelStatus: string;
outputs: Immutable.List<ImmutableOutput>;
}
interface InitialProps {
kernelRef: string;
contentRef: string;
}
// Redux
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
const { kernelRef, contentRef } = initialProps;
const mapStateToProps = (state: AppState) => {
let kernelStatus;
let firstCellId;
let outputs;
const kernel = selectors.kernel(state, { kernelRef });
if (kernel) {
kernelStatus = kernel.status;
}
const content = selectors.content(state, { contentRef });
if (content?.type === "notebook") {
const cellOrder = selectors.notebook.cellOrder(content.model);
if (cellOrder.size > 0) {
firstCellId = cellOrder.first() as string;
const model = selectors.model(state, { contentRef });
if (model && model.type === "notebook") {
const cell = selectors.notebook.cellById(model, { id: firstCellId });
if (cell) {
outputs = cell.get("outputs", Immutable.List());
}
}
}
}
return {
firstCellId,
kernelStatus,
outputs,
};
};
return mapStateToProps;
};
const makeMapDispatchToProps = () => {
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
return dispatch(
actions.addTransform({
mediaType: transform.MIMETYPE,
component: transform,
})
);
},
runCell: (contentRef: ContentRef, cellId: string) => {
return dispatch(
actions.executeCell({
contentRef,
id: cellId,
})
);
},
updateCell: (text: string, id: string, contentRef: ContentRef) => {
dispatch(actions.updateCellSource({ id, contentRef, value: text }));
},
};
};
return mapDispatchToProps;
};
export default connect(makeMapStateToProps, makeMapDispatchToProps)(SchemaAnalyzerComponent);

View File

@ -0,0 +1,88 @@
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,10 +1,10 @@
import * as ko from "knockout"; import * as ko from "knockout";
import { handleOpenAction } from "./OpenActions";
import * as ViewModels from "../Contracts/ViewModels";
import { ActionContracts } from "../Contracts/ExplorerContracts"; import { ActionContracts } from "../Contracts/ExplorerContracts";
import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "./Explorer"; import Explorer from "./Explorer";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; import { handleOpenAction } from "./OpenActions";
import AddCollectionPane from "./Panes/AddCollectionPane"; import AddCollectionPane from "./Panes/AddCollectionPane";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
describe("OpenActions", () => { describe("OpenActions", () => {
describe("handleOpenAction", () => { describe("handleOpenAction", () => {
@ -33,6 +33,7 @@ describe("OpenActions", () => {
collection.expandCollection = jest.fn(); collection.expandCollection = jest.fn();
collection.onDocumentDBDocumentsClick = jest.fn(); collection.onDocumentDBDocumentsClick = jest.fn();
collection.onMongoDBDocumentsClick = jest.fn(); collection.onMongoDBDocumentsClick = jest.fn();
collection.onSchemaAnalyzerClick = jest.fn();
collection.onTableEntitiesClick = jest.fn(); collection.onTableEntitiesClick = jest.fn();
collection.onGraphDocumentsClick = jest.fn(); collection.onGraphDocumentsClick = jest.fn();
collection.onNewQueryClick = jest.fn(); collection.onNewQueryClick = jest.fn();

View File

@ -79,6 +79,14 @@ function openCollectionTab(
break; break;
} }
if (
action.tabKind === ActionContracts.TabKind.SchemaAnalyzer ||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer]
) {
collection.onSchemaAnalyzerClick();
break;
}
if ( if (
action.tabKind === ActionContracts.TabKind.TableEntities || action.tabKind === ActionContracts.TabKind.TableEntities ||
(<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities] (<any>action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities]

View File

@ -0,0 +1,50 @@
import { Areas } from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../Explorer";
import { NotebookClientV2 } from "../Notebook/NotebookClientV2";
import TabsBase from "./TabsBase";
export interface NotebookTabBaseOptions extends ViewModels.TabOptions {
account: DataModels.DatabaseAccount;
masterKey: string;
container: Explorer;
}
/**
* Every notebook-based tab inherits from this class. It holds the static reference to a notebook client (singleton)
*/
export default class NotebookTabBase extends TabsBase {
protected static clientManager: NotebookClientV2;
protected container: Explorer;
constructor(options: NotebookTabBaseOptions) {
super(options);
this.container = options.container;
if (!NotebookTabBase.clientManager) {
NotebookTabBase.clientManager = new NotebookClientV2({
connectionInfo: this.container.notebookServerInfo(),
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
contentProvider: this.container.notebookManager?.notebookContentProvider,
});
}
}
/**
* Override base behavior
*/
public getContainer(): Explorer {
return this.container;
}
protected traceTelemetry(actionType: number): void {
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
dataExplorerArea: Areas.Notebook,
});
}
}

View File

@ -12,10 +12,9 @@ import RunAllIcon from "../../../images/notebook/Notebook-run-all.svg";
import RunIcon from "../../../images/notebook/Notebook-run.svg"; import RunIcon from "../../../images/notebook/Notebook-run.svg";
import { default as InterruptKernelIcon, default as KillKernelIcon } from "../../../images/notebook/Notebook-stop.svg"; import { default as InterruptKernelIcon, default as KillKernelIcon } from "../../../images/notebook/Notebook-stop.svg";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
import { Areas, ArmApiVersions } from "../../Common/Constants"; import { ArmApiVersions } from "../../Common/Constants";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { trackEvent } from "../../Shared/appInsights"; import { trackEvent } from "../../Shared/appInsights";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@ -23,25 +22,19 @@ import { userContext } from "../../UserContext";
import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils";
import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory"; import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBarComponentButtonFactory";
import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2"; import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter"; import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
import { NotebookContentItem } from "../Notebook/NotebookContentItem"; import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
import template from "./NotebookV2Tab.html"; import template from "./NotebookV2Tab.html";
import TabsBase from "./TabsBase";
export interface NotebookTabOptions extends ViewModels.TabOptions { export interface NotebookTabOptions extends NotebookTabBaseOptions {
account: DataModels.DatabaseAccount;
masterKey: string;
container: Explorer;
notebookContentItem: NotebookContentItem; notebookContentItem: NotebookContentItem;
} }
export default class NotebookTabV2 extends TabsBase { export default class NotebookTabV2 extends NotebookTabBase {
public static readonly component = { name: "notebookv2-tab", template }; public static readonly component = { name: "notebookv2-tab", template };
private static clientManager: NotebookClientV2;
private container: Explorer;
public notebookPath: ko.Observable<string>; public notebookPath: ko.Observable<string>;
private selectedSparkPool: ko.Observable<string>; private selectedSparkPool: ko.Observable<string>;
private notebookComponentAdapter: NotebookComponentAdapter; private notebookComponentAdapter: NotebookComponentAdapter;
@ -50,22 +43,12 @@ export default class NotebookTabV2 extends TabsBase {
super(options); super(options);
this.container = options.container; this.container = options.container;
if (!NotebookTabV2.clientManager) {
NotebookTabV2.clientManager = new NotebookClientV2({
connectionInfo: this.container.notebookServerInfo(),
databaseAccountName: this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience(),
contentProvider: this.container.notebookManager?.notebookContentProvider,
});
}
this.notebookPath = ko.observable(options.notebookContentItem.path); this.notebookPath = ko.observable(options.notebookContentItem.path);
this.container.notebookServerInfo.subscribe(() => logConsoleInfo("New notebook server info received.")); this.container.notebookServerInfo.subscribe(() => logConsoleInfo("New notebook server info received."));
this.notebookComponentAdapter = new NotebookComponentAdapter({ this.notebookComponentAdapter = new NotebookComponentAdapter({
contentItem: options.notebookContentItem, contentItem: options.notebookContentItem,
notebooksBasePath: this.container.getNotebookBasePath(), notebooksBasePath: this.container.getNotebookBasePath(),
notebookClient: NotebookTabV2.clientManager, notebookClient: NotebookTabBase.clientManager,
onUpdateKernelInfo: this.onKernelUpdate, onUpdateKernelInfo: this.onKernelUpdate,
}); });
@ -110,10 +93,6 @@ export default class NotebookTabV2 extends TabsBase {
return await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName()); return await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName());
} }
public getContainer(): Explorer {
return this.container;
}
protected getTabsButtons(): CommandButtonComponentProps[] { protected getTabsButtons(): CommandButtonComponentProps[] {
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs(); const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
@ -499,10 +478,4 @@ export default class NotebookTabV2 extends TabsBase {
this.container.copyNotebook(notebookContent.name, content); this.container.copyNotebook(notebookContent.name, content);
}; };
private traceTelemetry(actionType: number) {
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
dataExplorerArea: Areas.Notebook,
});
}
} }

View File

@ -0,0 +1 @@
<div data-bind="react:schemaAnalyzerComponentAdapter" style="height: 100%"></div>

View File

@ -0,0 +1,25 @@
import { SchemaAnalyzerComponentAdapter } from "../Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponentAdapter";
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
import template from "./SchemaAnalyzerTab.html";
export default class SchemaAnalyzerTab extends NotebookTabBase {
public static readonly component = { name: "schema-analyzer-tab", template };
private schemaAnalyzerComponentAdapter: SchemaAnalyzerComponentAdapter;
constructor(options: NotebookTabBaseOptions) {
super(options);
this.schemaAnalyzerComponentAdapter = new SchemaAnalyzerComponentAdapter(
{
contentRef: undefined,
notebookClient: NotebookTabBase.clientManager,
},
options.collection?.databaseId,
options.collection?.id()
);
}
protected buildCommandBarOptions(): void {
this.updateNavbarWithTabsButtons();
}
}

View File

@ -29,6 +29,7 @@ import MongoQueryTab from "../Tabs/MongoQueryTab";
import MongoShellTab from "../Tabs/MongoShellTab"; import MongoShellTab from "../Tabs/MongoShellTab";
import QueryTab from "../Tabs/QueryTab"; import QueryTab from "../Tabs/QueryTab";
import QueryTablesTab from "../Tabs/QueryTablesTab"; import QueryTablesTab from "../Tabs/QueryTablesTab";
import SchemaAnalyzerTab from "../Tabs/SchemaAnalyzerTab";
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2"; import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
import ConflictId from "./ConflictId"; import ConflictId from "./ConflictId";
import DocumentId from "./DocumentId"; import DocumentId from "./DocumentId";
@ -514,6 +515,50 @@ export default class Collection implements ViewModels.Collection {
} }
}; };
public onSchemaAnalyzerClick = () => {
this.container.selectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Mongo Schema node",
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
for (const tab of this.container.tabsManager.openedTabs()) {
if (
tab instanceof SchemaAnalyzerTab &&
tab.collection?.databaseId === this.databaseId &&
tab.collection?.id() === this.id()
) {
return this.container.tabsManager.activateTab(tab);
}
}
const startKey = TelemetryProcessor.traceStart(Action.Tab, {
databaseName: this.databaseId,
collectionName: this.id(),
dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Schema",
});
this.documentIds([]);
this.container.tabsManager.activateNewTab(
new SchemaAnalyzerTab({
account: userContext.databaseAccount,
masterKey: userContext.masterKey || "",
container: this.container,
tabKind: ViewModels.CollectionTabKind.SchemaAnalyzer,
title: "Schema",
tabPath: "",
collection: this,
node: this,
hashLocation: `${Constants.HashRoutePrefixes.collectionsWithIds(this.databaseId, this.id())}/schemaAnalyzer`,
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
})
);
};
public onSettingsClick = async (): Promise<void> => { public onSettingsClick = async (): Promise<void> => {
this.container.selectedNode(this); this.container.selectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings); this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings);

View File

@ -273,6 +273,17 @@ export class ResourceTreeAdapter implements ReactAdapter {
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection), contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection),
}); });
if (userContext.apiType === "Mongo" && userContext.features.enableSchemaAnalyzer) {
children.push({
label: "Schema (Preview)",
onClick: collection.onSchemaAnalyzerClick.bind(collection),
isSelected: () =>
this.isDataNodeSelected(collection.databaseId, collection.id(), [
ViewModels.CollectionTabKind.SchemaAnalyzer,
]),
});
}
if (userContext.apiType !== "Cassandra" || !this.container.isServerlessEnabled()) { if (userContext.apiType !== "Cassandra" || !this.container.isServerlessEnabled()) {
children.push({ children.push({
label: database.isDatabaseShared() || this.container.isServerlessEnabled() ? "Settings" : "Scale & Settings", label: database.isDatabaseShared() || this.container.isServerlessEnabled() ? "Settings" : "Scale & Settings",

View File

@ -8,6 +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;
readonly enableSDKoperations: boolean; readonly enableSDKoperations: boolean;
readonly enableSpark: boolean; readonly enableSpark: boolean;
readonly enableTtl: boolean; readonly enableTtl: boolean;
@ -49,6 +50,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
enableReactPane: "true" === get("enablereactpane"), enableReactPane: "true" === get("enablereactpane"),
enableRightPanelV2: "true" === get("enablerightpanelv2"), enableRightPanelV2: "true" === get("enablerightpanelv2"),
enableSchema: "true" === get("enableschema"), enableSchema: "true" === get("enableschema"),
enableSchemaAnalyzer: "true" === get("enableschemaanalyzer"),
enableSDKoperations: "true" === get("enablesdkoperations"), enableSDKoperations: "true" === get("enablesdkoperations"),
enableSpark: "true" === get("enablespark"), enableSpark: "true" === get("enablespark"),
enableTtl: "true" === get("enablettl"), enableTtl: "true" === get("enablettl"),

View File

@ -59,6 +59,13 @@ export class TabRouteHandler {
} }
); );
this._tabRouter.addRoute(
`${Constants.HashRoutePrefixes.collections}/schemaAnalyzer`,
(db_id: string, coll_id: string) => {
this._openSchemaAnalyzerTabForResource(db_id, coll_id);
}
);
this._tabRouter.addRoute( this._tabRouter.addRoute(
`${Constants.HashRoutePrefixes.collections}/mongoQuery`, `${Constants.HashRoutePrefixes.collections}/mongoQuery`,
(db_id: string, coll_id: string) => { (db_id: string, coll_id: string) => {
@ -175,6 +182,19 @@ export class TabRouteHandler {
}); });
} }
private _openSchemaAnalyzerTabForResource(databaseId: string, collectionId: string): void {
this._executeActionHelper(() => {
const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource(
databaseId,
collectionId
);
collection &&
collection.container &&
collection.container.isPreferredApiMongoDB() &&
collection.onSchemaAnalyzerClick();
});
}
private _openQueryTabForResource(databaseId: string, collectionId: string): void { private _openQueryTabForResource(databaseId: string, collectionId: string): void {
this._executeActionHelper(() => { this._executeActionHelper(() => {
const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource( const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource(