mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-25 23:16:56 +00:00
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:
parent
448566146f
commit
5ecc3d67b0
@ -4,6 +4,7 @@
|
|||||||
export enum TabKind {
|
export enum TabKind {
|
||||||
SQLDocuments,
|
SQLDocuments,
|
||||||
MongoDocuments,
|
MongoDocuments,
|
||||||
|
SchemaAnalyzer,
|
||||||
TableEntities,
|
TableEntities,
|
||||||
Graph,
|
Graph,
|
||||||
SQLQuery,
|
SQLQuery,
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
.shemaAnalyzerComponent {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schemaAnalyzerCard {
|
||||||
|
max-width: 4096px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
@ -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);
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
@ -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]
|
||||||
|
50
src/Explorer/Tabs/NotebookTabBase.ts
Normal file
50
src/Explorer/Tabs/NotebookTabBase.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
1
src/Explorer/Tabs/SchemaAnalyzerTab.html
Normal file
1
src/Explorer/Tabs/SchemaAnalyzerTab.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<div data-bind="react:schemaAnalyzerComponentAdapter" style="height: 100%"></div>
|
25
src/Explorer/Tabs/SchemaAnalyzerTab.ts
Normal file
25
src/Explorer/Tabs/SchemaAnalyzerTab.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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",
|
||||||
|
@ -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"),
|
||||||
|
@ -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(
|
||||||
|
Loading…
Reference in New Issue
Block a user