mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-20 09:20:16 +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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user