diff --git a/src/Contracts/ActionContracts.ts b/src/Contracts/ActionContracts.ts index dac45dbe0..f8fc956e6 100644 --- a/src/Contracts/ActionContracts.ts +++ b/src/Contracts/ActionContracts.ts @@ -4,6 +4,7 @@ export enum TabKind { SQLDocuments, MongoDocuments, + SchemaAnalyzer, TableEntities, Graph, SQLQuery, diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index dc14bb86c..656b87eb8 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -141,6 +141,7 @@ export interface Collection extends CollectionBase { onTableEntitiesClick(): void; onGraphDocumentsClick(): void; onMongoDBDocumentsClick(): void; + onSchemaAnalyzerClick(): void; openTab(): void; onSettingsClick: () => Promise; @@ -366,6 +367,7 @@ export enum CollectionTabKind { Schema = 19, CollectionSettingsV2 = 20, DatabaseSettingsV2 = 21, + SchemaAnalyzer = 22, } export enum TerminalKind { diff --git a/src/Explorer/ComponentRegisterer.ts b/src/Explorer/ComponentRegisterer.ts index e3eecc0a6..1f2984612 100644 --- a/src/Explorer/ComponentRegisterer.ts +++ b/src/Explorer/ComponentRegisterer.ts @@ -17,6 +17,7 @@ import NotebookTabV2 from "./Tabs/NotebookV2Tab"; import NotebookViewerTab from "./Tabs/NotebookViewerTab"; import QueryTab from "./Tabs/QueryTab"; import QueryTablesTab from "./Tabs/QueryTablesTab"; +import SchemaAnalyzerTab from "./Tabs/SchemaAnalyzerTab"; import { DatabaseSettingsTabV2, SettingsTabV2 } from "./Tabs/SettingsTabV2"; import StoredProcedureTab from "./Tabs/StoredProcedureTab"; import TerminalTab from "./Tabs/TerminalTab"; @@ -49,6 +50,7 @@ ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponent GalleryTab, NotebookViewerTab, DatabaseSettingsTabV2, + SchemaAnalyzerTab, ].forEach(({ component: { name, template } }) => ko.components.register(name, { template })); // Panes diff --git a/src/Explorer/Notebook/NotebookClientV2.ts b/src/Explorer/Notebook/NotebookClientV2.ts index 7c5f4179a..e7c1fa289 100644 --- a/src/Explorer/Notebook/NotebookClientV2.ts +++ b/src/Explorer/Notebook/NotebookClientV2.ts @@ -200,10 +200,11 @@ export class NotebookClientV2 { case actions.FETCH_KERNELSPECS_FULFILLED: { const payload = ((action as unknown) as actions.FetchKernelspecsFulfilled).payload; const defaultKernelName = payload.defaultKernelName; - this.kernelSpecsForDisplay = Object.keys(payload.kernelspecs) - .map((name) => ({ - name, - displayName: payload.kernelspecs[name].displayName, + this.kernelSpecsForDisplay = Object.values(payload.kernelspecs) + .filter((spec) => !spec.metadata?.hasOwnProperty("hidden")) + .map((spec) => ({ + name: spec.name, + displayName: spec.displayName, })) .sort((a: KernelSpecsDisplay, b: KernelSpecsDisplay) => { // Put default at the top, otherwise lexicographically compare diff --git a/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.less b/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.less new file mode 100644 index 000000000..f98d9729a --- /dev/null +++ b/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.less @@ -0,0 +1,10 @@ +.shemaAnalyzerComponent { + width: 100%; + height: 100%; + overflow-y: auto; +} + +.schemaAnalyzerCard { + max-width: 4096px; + width: 100%; +} \ No newline at end of file diff --git a/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.tsx b/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.tsx new file mode 100644 index 000000000..cf53bf2db --- /dev/null +++ b/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponent.tsx @@ -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, + 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 ( + + + + + + + + + + + + + {showSchemaOutput ? ( + outputs.map((output, index) => ( + + + + + + + + + + + )) + ) : 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 && } + + )} + + ); + } +} + +interface StateProps { + firstCellId: string; + kernelStatus: string; + outputs: Immutable.List; +} + +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); diff --git a/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponentAdapter.tsx b/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponentAdapter.tsx new file mode 100644 index 000000000..75b221b88 --- /dev/null +++ b/src/Explorer/Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponentAdapter.tsx @@ -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 ( + + ; + + ); + } +} diff --git a/src/Explorer/OpenActions.test.ts b/src/Explorer/OpenActions.test.ts index ab9e3314a..47a3003af 100644 --- a/src/Explorer/OpenActions.test.ts +++ b/src/Explorer/OpenActions.test.ts @@ -1,10 +1,10 @@ import * as ko from "knockout"; -import { handleOpenAction } from "./OpenActions"; -import * as ViewModels from "../Contracts/ViewModels"; import { ActionContracts } from "../Contracts/ExplorerContracts"; +import * as ViewModels from "../Contracts/ViewModels"; import Explorer from "./Explorer"; -import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; +import { handleOpenAction } from "./OpenActions"; import AddCollectionPane from "./Panes/AddCollectionPane"; +import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane"; describe("OpenActions", () => { describe("handleOpenAction", () => { @@ -33,6 +33,7 @@ describe("OpenActions", () => { collection.expandCollection = jest.fn(); collection.onDocumentDBDocumentsClick = jest.fn(); collection.onMongoDBDocumentsClick = jest.fn(); + collection.onSchemaAnalyzerClick = jest.fn(); collection.onTableEntitiesClick = jest.fn(); collection.onGraphDocumentsClick = jest.fn(); collection.onNewQueryClick = jest.fn(); diff --git a/src/Explorer/OpenActions.ts b/src/Explorer/OpenActions.ts index 0937317c5..4742978cc 100644 --- a/src/Explorer/OpenActions.ts +++ b/src/Explorer/OpenActions.ts @@ -79,6 +79,14 @@ function openCollectionTab( break; } + if ( + action.tabKind === ActionContracts.TabKind.SchemaAnalyzer || + (action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.SchemaAnalyzer] + ) { + collection.onSchemaAnalyzerClick(); + break; + } + if ( action.tabKind === ActionContracts.TabKind.TableEntities || (action).tabKind === ActionContracts.TabKind[ActionContracts.TabKind.TableEntities] diff --git a/src/Explorer/Tabs/NotebookTabBase.ts b/src/Explorer/Tabs/NotebookTabBase.ts new file mode 100644 index 000000000..e74f12b86 --- /dev/null +++ b/src/Explorer/Tabs/NotebookTabBase.ts @@ -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, + }); + } +} diff --git a/src/Explorer/Tabs/NotebookV2Tab.ts b/src/Explorer/Tabs/NotebookV2Tab.ts index 168166ef7..f0440f61e 100644 --- a/src/Explorer/Tabs/NotebookV2Tab.ts +++ b/src/Explorer/Tabs/NotebookV2Tab.ts @@ -12,10 +12,9 @@ import RunAllIcon from "../../../images/notebook/Notebook-run-all.svg"; import RunIcon from "../../../images/notebook/Notebook-run.svg"; import { default as InterruptKernelIcon, default as KillKernelIcon } from "../../../images/notebook/Notebook-stop.svg"; import SaveIcon from "../../../images/save-cosmos.svg"; -import { Areas, ArmApiVersions } from "../../Common/Constants"; +import { ArmApiVersions } from "../../Common/Constants"; import { configContext } from "../../ConfigContext"; import * as DataModels from "../../Contracts/DataModels"; -import * as ViewModels from "../../Contracts/ViewModels"; import { trackEvent } from "../../Shared/appInsights"; import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; @@ -23,25 +22,19 @@ import { userContext } from "../../UserContext"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils"; import { logConsoleInfo } from "../../Utils/NotificationConsoleUtils"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; -import Explorer from "../Explorer"; 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 { NotebookContentItem } from "../Notebook/NotebookContentItem"; +import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase"; import template from "./NotebookV2Tab.html"; -import TabsBase from "./TabsBase"; -export interface NotebookTabOptions extends ViewModels.TabOptions { - account: DataModels.DatabaseAccount; - masterKey: string; - container: Explorer; +export interface NotebookTabOptions extends NotebookTabBaseOptions { notebookContentItem: NotebookContentItem; } -export default class NotebookTabV2 extends TabsBase { +export default class NotebookTabV2 extends NotebookTabBase { public static readonly component = { name: "notebookv2-tab", template }; - private static clientManager: NotebookClientV2; - private container: Explorer; public notebookPath: ko.Observable; private selectedSparkPool: ko.Observable; private notebookComponentAdapter: NotebookComponentAdapter; @@ -50,22 +43,12 @@ export default class NotebookTabV2 extends TabsBase { super(options); 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.container.notebookServerInfo.subscribe(() => logConsoleInfo("New notebook server info received.")); this.notebookComponentAdapter = new NotebookComponentAdapter({ contentItem: options.notebookContentItem, notebooksBasePath: this.container.getNotebookBasePath(), - notebookClient: NotebookTabV2.clientManager, + notebookClient: NotebookTabBase.clientManager, onUpdateKernelInfo: this.onKernelUpdate, }); @@ -110,10 +93,6 @@ export default class NotebookTabV2 extends TabsBase { return await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName()); } - public getContainer(): Explorer { - return this.container; - } - protected getTabsButtons(): CommandButtonComponentProps[] { const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs(); @@ -499,10 +478,4 @@ export default class NotebookTabV2 extends TabsBase { this.container.copyNotebook(notebookContent.name, content); }; - - private traceTelemetry(actionType: number) { - TelemetryProcessor.trace(actionType, ActionModifiers.Mark, { - dataExplorerArea: Areas.Notebook, - }); - } } diff --git a/src/Explorer/Tabs/SchemaAnalyzerTab.html b/src/Explorer/Tabs/SchemaAnalyzerTab.html new file mode 100644 index 000000000..120d9213b --- /dev/null +++ b/src/Explorer/Tabs/SchemaAnalyzerTab.html @@ -0,0 +1 @@ +
diff --git a/src/Explorer/Tabs/SchemaAnalyzerTab.ts b/src/Explorer/Tabs/SchemaAnalyzerTab.ts new file mode 100644 index 000000000..1f998d657 --- /dev/null +++ b/src/Explorer/Tabs/SchemaAnalyzerTab.ts @@ -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(); + } +} diff --git a/src/Explorer/Tree/Collection.ts b/src/Explorer/Tree/Collection.ts index ed525adfe..97da4ccfb 100644 --- a/src/Explorer/Tree/Collection.ts +++ b/src/Explorer/Tree/Collection.ts @@ -29,6 +29,7 @@ import MongoQueryTab from "../Tabs/MongoQueryTab"; import MongoShellTab from "../Tabs/MongoShellTab"; import QueryTab from "../Tabs/QueryTab"; import QueryTablesTab from "../Tabs/QueryTablesTab"; +import SchemaAnalyzerTab from "../Tabs/SchemaAnalyzerTab"; import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2"; import ConflictId from "./ConflictId"; 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 => { this.container.selectedNode(this); this.selectedSubnodeKind(ViewModels.CollectionTabKind.Settings); diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index b3f928497..3a05c06bb 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -273,6 +273,17 @@ export class ResourceTreeAdapter implements ReactAdapter { 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()) { children.push({ label: database.isDatabaseShared() || this.container.isServerlessEnabled() ? "Settings" : "Scale & Settings", diff --git a/src/Platform/Hosted/extractFeatures.ts b/src/Platform/Hosted/extractFeatures.ts index 6921d6bdd..b7f62e87e 100644 --- a/src/Platform/Hosted/extractFeatures.ts +++ b/src/Platform/Hosted/extractFeatures.ts @@ -8,6 +8,7 @@ export type Features = { readonly enableReactPane: boolean; readonly enableRightPanelV2: boolean; readonly enableSchema: boolean; + readonly enableSchemaAnalyzer: boolean; readonly enableSDKoperations: boolean; readonly enableSpark: boolean; readonly enableTtl: boolean; @@ -49,6 +50,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear enableReactPane: "true" === get("enablereactpane"), enableRightPanelV2: "true" === get("enablerightpanelv2"), enableSchema: "true" === get("enableschema"), + enableSchemaAnalyzer: "true" === get("enableschemaanalyzer"), enableSDKoperations: "true" === get("enablesdkoperations"), enableSpark: "true" === get("enablespark"), enableTtl: "true" === get("enablettl"), diff --git a/src/RouteHandlers/TabRouteHandler.ts b/src/RouteHandlers/TabRouteHandler.ts index f20606cae..06a8d03ef 100644 --- a/src/RouteHandlers/TabRouteHandler.ts +++ b/src/RouteHandlers/TabRouteHandler.ts @@ -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( `${Constants.HashRoutePrefixes.collections}/mongoQuery`, (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 { this._executeActionHelper(() => { const collection: ViewModels.Collection = this._findAndExpandMatchingCollectionForResource(