Compare commits

..

1 Commits

Author SHA1 Message Date
Laurent Nguyen
acdd05a79b New MongoQueryTab and component running nteract 2021-01-27 18:38:12 +05:30
38 changed files with 3784 additions and 2599 deletions

View File

@@ -156,7 +156,6 @@ jobs:
run: | run: |
npm ci npm ci
npm start & npm start &
node utils/cleanupDBs.js
npm run wait-for-server npm run wait-for-server
npm run test:e2e npm run test:e2e
shell: bash shell: bash

4249
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,10 @@
"@azure/identity": "1.2.1", "@azure/identity": "1.2.1",
"@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12", "@babel/plugin-proposal-decorators": "7.12.12",
"@jupyterlab/services": "6.0.2", "@jupyterlab/services": "6.0.0-rc.2",
"@jupyterlab/terminal": "3.0.3", "@jupyterlab/terminal": "3.0.0-rc.2",
"@microsoft/applicationinsights-web": "2.5.9", "@microsoft/applicationinsights-web": "2.5.9",
"@nteract/commutable": "7.4.2", "@nteract/commutable": "7.3.2",
"@nteract/connected-components": "6.8.2", "@nteract/connected-components": "6.8.2",
"@nteract/core": "15.1.0", "@nteract/core": "15.1.0",
"@nteract/data-explorer": "8.0.3", "@nteract/data-explorer": "8.0.3",
@@ -64,9 +64,6 @@
"eslint-plugin-react": "7.20.0", "eslint-plugin-react": "7.20.0",
"hasher": "1.2.0", "hasher": "1.2.0",
"html2canvas": "1.0.0-rc.5", "html2canvas": "1.0.0-rc.5",
"i18next": "19.8.4",
"i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "1.0.23",
"immutable": "4.0.0-rc.12", "immutable": "4.0.0-rc.12",
"is-ci": "2.0.0", "is-ci": "2.0.0",
"jquery": "3.5.1", "jquery": "3.5.1",
@@ -89,7 +86,6 @@
"react-dnd-html5-backend": "9.4.0", "react-dnd-html5-backend": "9.4.0",
"react-dom": "16.13.1", "react-dom": "16.13.1",
"react-hotkeys": "2.0.0", "react-hotkeys": "2.0.0",
"react-i18next": "11.8.5",
"react-notification-system": "0.2.17", "react-notification-system": "0.2.17",
"react-redux": "7.1.3", "react-redux": "7.1.3",
"redux": "4.0.4", "redux": "4.0.4",
@@ -128,7 +124,7 @@
"@types/prop-types": "15.5.8", "@types/prop-types": "15.5.8",
"@types/puppeteer": "3.0.1", "@types/puppeteer": "3.0.1",
"@types/q": "1.5.1", "@types/q": "1.5.1",
"@types/react": "17.0.0", "@types/react": "16.9.56",
"@types/react-dom": "17.0.0", "@types/react-dom": "17.0.0",
"@types/react-notification-system": "0.2.39", "@types/react-notification-system": "0.2.39",
"@types/react-redux": "7.1.7", "@types/react-redux": "7.1.7",

View File

@@ -5,6 +5,7 @@ import {
TriggerDefinition, TriggerDefinition,
UserDefinedFunctionDefinition, UserDefinedFunctionDefinition,
} from "@azure/cosmos"; } from "@azure/cosmos";
import Q from "q";
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
@@ -108,7 +109,7 @@ export interface CollectionBase extends TreeNode {
onDocumentDBDocumentsClick(): void; onDocumentDBDocumentsClick(): void;
onNewQueryClick(source: any, event: MouseEvent, queryText?: string): void; onNewQueryClick(source: any, event: MouseEvent, queryText?: string): void;
expandCollection(): void; expandCollection(): Q.Promise<any>;
collapseCollection(): void; collapseCollection(): void;
getDatabase(): Database; getDatabase(): Database;
} }
@@ -175,7 +176,7 @@ export interface Collection extends CollectionBase {
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void; onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
onDrop(source: Collection, event: { originalEvent: DragEvent }): void; onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
uploadFiles(fileList: FileList): Promise<UploadDetails>; uploadFiles(fileList: FileList): Q.Promise<UploadDetails>;
getLabel(): string; getLabel(): string;
} }
@@ -293,7 +294,7 @@ export interface DocumentsTabOptions extends TabOptions {
} }
export interface SettingsTabV2Options extends TabOptions { export interface SettingsTabV2Options extends TabOptions {
getPendingNotification: Promise<DataModels.Notification>; getPendingNotification: Q.Promise<DataModels.Notification>;
} }
export interface ConflictsTabOptions extends TabOptions { export interface ConflictsTabOptions extends TabOptions {

View File

@@ -26,7 +26,7 @@ ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponent
ko.components.register("tabs-manager", TabsManagerKOComponent()); ko.components.register("tabs-manager", TabsManagerKOComponent());
// Collection Tabs // Collection Tabs
ko.components.register("documents-tab", new TabComponents.DocumentsTab()); ko.components.register("documents-tab", new TabComponents.MongoDocumentsTabV2());
ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab()); ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab());
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab()); ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
ko.components.register("trigger-tab", new TabComponents.TriggerTab()); ko.components.register("trigger-tab", new TabComponents.TriggerTab());

View File

@@ -31,6 +31,7 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
})); }));
import { updateOffer } from "../../../Common/dataAccess/updateOffer"; import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types"; import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import Q from "q";
jest.mock("../../../Common/dataAccess/updateOffer", () => ({ jest.mock("../../../Common/dataAccess/updateOffer", () => ({
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer), updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer),
})); }));
@@ -46,7 +47,9 @@ describe("SettingsComponent", () => {
hashLocation: "settings", hashLocation: "settings",
isActive: ko.observable(false), isActive: ko.observable(false),
onUpdateTabsButtons: undefined, onUpdateTabsButtons: undefined,
getPendingNotification: Promise.resolve(undefined), getPendingNotification: Q.Promise<DataModels.Notification>(() => {
return;
}),
}), }),
}; };

View File

@@ -8,10 +8,10 @@ describe("SmartUiComponent", () => {
root: { root: {
id: "root", id: "root",
info: { info: {
messageTKey: "Start at $24/mo per database", message: "Start at $24/mo per database",
link: { link: {
href: "https://aka.ms/azure-cosmos-db-pricing", href: "https://aka.ms/azure-cosmos-db-pricing",
textTKey: "More Details", text: "More Details",
}, },
}, },
children: [ children: [
@@ -21,10 +21,10 @@ describe("SmartUiComponent", () => {
dataFieldName: "description", dataFieldName: "description",
type: "string", type: "string",
description: { description: {
textTKey: "this is an example description text.", text: "this is an example description text.",
link: { link: {
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
textTKey: "Click here for more information.", text: "Click here for more information.",
}, },
}, },
}, },
@@ -32,7 +32,7 @@ describe("SmartUiComponent", () => {
{ {
id: "throughput", id: "throughput",
input: { input: {
labelTKey: "Throughput (input)", label: "Throughput (input)",
dataFieldName: "throughput", dataFieldName: "throughput",
type: "number", type: "number",
min: 400, min: 400,
@@ -45,7 +45,7 @@ describe("SmartUiComponent", () => {
{ {
id: "throughput2", id: "throughput2",
input: { input: {
labelTKey: "Throughput (Slider)", label: "Throughput (Slider)",
dataFieldName: "throughput2", dataFieldName: "throughput2",
type: "number", type: "number",
min: 400, min: 400,
@@ -58,7 +58,7 @@ describe("SmartUiComponent", () => {
{ {
id: "throughput3", id: "throughput3",
input: { input: {
labelTKey: "Throughput (invalid)", label: "Throughput (invalid)",
dataFieldName: "throughput3", dataFieldName: "throughput3",
type: "boolean", type: "boolean",
min: 400, min: 400,
@@ -72,7 +72,7 @@ describe("SmartUiComponent", () => {
{ {
id: "containerId", id: "containerId",
input: { input: {
labelTKey: "Container id", label: "Container id",
dataFieldName: "containerId", dataFieldName: "containerId",
type: "string", type: "string",
}, },
@@ -80,9 +80,9 @@ describe("SmartUiComponent", () => {
{ {
id: "analyticalStore", id: "analyticalStore",
input: { input: {
labelTKey: "Analytical Store", label: "Analytical Store",
trueLabelTKey: "Enabled", trueLabel: "Enabled",
falseLabelTKey: "Disabled", falseLabel: "Disabled",
defaultValue: true, defaultValue: true,
dataFieldName: "analyticalStore", dataFieldName: "analyticalStore",
type: "boolean", type: "boolean",
@@ -91,7 +91,7 @@ describe("SmartUiComponent", () => {
{ {
id: "database", id: "database",
input: { input: {
labelTKey: "Database", label: "Database",
dataFieldName: "database", dataFieldName: "database",
type: "object", type: "object",
choices: [ choices: [
@@ -117,9 +117,6 @@ describe("SmartUiComponent", () => {
onError={() => { onError={() => {
return; return;
}} }}
getTranslation={(key: string) => {
return key;
}}
/> />
); );
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
@@ -148,9 +145,6 @@ describe("SmartUiComponent", () => {
onError={() => { onError={() => {
return; return;
}} }}
getTranslation={(key: string) => {
return key;
}}
/> />
); );
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));

View File

@@ -18,7 +18,6 @@ import {
NumberUiType, NumberUiType,
SmartUiInput, SmartUiInput,
} from "../../../SelfServe/SelfServeTypes"; } from "../../../SelfServe/SelfServeTypes";
import { TFunction } from "i18next";
/** /**
* Generic UX renderer * Generic UX renderer
@@ -35,8 +34,8 @@ interface BaseDisplay {
} }
interface BaseInput extends BaseDisplay { interface BaseInput extends BaseDisplay {
labelTKey: string; label: string;
placeholderTKey?: string; placeholder?: string;
errorMessage?: string; errorMessage?: string;
} }
@@ -52,8 +51,8 @@ interface NumberInput extends BaseInput {
} }
interface BooleanInput extends BaseInput { interface BooleanInput extends BaseInput {
trueLabelTKey: string; trueLabel: string;
falseLabelTKey: string; falseLabel: string;
defaultValue?: boolean; defaultValue?: boolean;
} }
@@ -90,7 +89,6 @@ export interface SmartUiComponentProps {
onInputChange: (input: AnyDisplay, newValue: InputType) => void; onInputChange: (input: AnyDisplay, newValue: InputType) => void;
onError: (hasError: boolean) => void; onError: (hasError: boolean) => void;
disabled: boolean; disabled: boolean;
getTranslation: TFunction;
} }
interface SmartUiComponentState { interface SmartUiComponentState {
@@ -124,10 +122,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
private renderInfo(info: Info): JSX.Element { private renderInfo(info: Info): JSX.Element {
return ( return (
<MessageBar styles={{ root: { width: 400 } }}> <MessageBar styles={{ root: { width: 400 } }}>
{this.props.getTranslation(info.messageTKey)} {info.message}
{info.link && ( {info.link && (
<Link href={info.link.href} target="_blank"> <Link href={info.link.href} target="_blank">
{this.props.getTranslation(info.link.textTKey)} {info.link.text}
</Link> </Link>
)} )}
</MessageBar> </MessageBar>
@@ -141,10 +139,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
<div className="stringInputContainer"> <div className="stringInputContainer">
<TextField <TextField
id={`${input.dataFieldName}-textField-input`} id={`${input.dataFieldName}-textField-input`}
label={this.props.getTranslation(input.labelTKey)} label={input.label}
type="text" type="text"
value={value || ""} value={value || ""}
placeholder={this.props.getTranslation(input.placeholderTKey)} placeholder={input.placeholder}
disabled={disabled} disabled={disabled}
onChange={(_, newValue) => this.props.onInputChange(input, newValue)} onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
styles={{ styles={{
@@ -167,10 +165,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
const description = input.description; const description = input.description;
return ( return (
<Text id={`${input.dataFieldName}-text-display`}> <Text id={`${input.dataFieldName}-text-display`}>
{this.props.getTranslation(input.description.textTKey)}{" "} {input.description.text}{" "}
{description.link && ( {description.link && (
<Link target="_blank" href={input.description.link.href}> <Link target="_blank" href={input.description.link.href}>
{this.props.getTranslation(input.description.link.textTKey)} {input.description.link.text}
</Link> </Link>
)} )}
</Text> </Text>
@@ -221,12 +219,12 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
}; };
private renderNumberInput(input: NumberInput): JSX.Element { private renderNumberInput(input: NumberInput): JSX.Element {
const { labelTKey, min, max, dataFieldName, step } = input; const { label, min, max, dataFieldName, step } = input;
const props = { const props = {
label: this.props.getTranslation(labelTKey), label: label,
min: min, min: min,
max: max, max: max,
ariaLabel: labelTKey, ariaLabel: label,
step: step, step: step,
}; };
@@ -286,10 +284,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return ( return (
<Toggle <Toggle
id={`${input.dataFieldName}-toggle-input`} id={`${input.dataFieldName}-toggle-input`}
label={this.props.getTranslation(input.labelTKey)} label={input.label}
checked={value || false} checked={value || false}
onText={this.props.getTranslation(input.trueLabelTKey)} onText={input.trueLabel}
offText={this.props.getTranslation(input.falseLabelTKey)} offText={input.falseLabel}
disabled={disabled} disabled={disabled}
onChange={(event, checked: boolean) => this.props.onInputChange(input, checked)} onChange={(event, checked: boolean) => this.props.onInputChange(input, checked)}
styles={{ root: { width: 400 } }} styles={{ root: { width: 400 } }}
@@ -298,7 +296,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
} }
private renderChoiceInput(input: ChoiceInput): JSX.Element { private renderChoiceInput(input: ChoiceInput): JSX.Element {
const { labelTKey, defaultKey, dataFieldName, choices, placeholderTKey } = input; const { label, defaultKey: defaultKey, dataFieldName, choices, placeholder } = input;
const value = this.props.currentValues.get(dataFieldName)?.value as string; const value = this.props.currentValues.get(dataFieldName)?.value as string;
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled; const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
let selectedKey = value ? value : defaultKey; let selectedKey = value ? value : defaultKey;
@@ -308,14 +306,14 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return ( return (
<Dropdown <Dropdown
id={`${input.dataFieldName}-dropdown-input`} id={`${input.dataFieldName}-dropdown-input`}
label={this.props.getTranslation(labelTKey)} label={label}
selectedKey={selectedKey} selectedKey={selectedKey}
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())} onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
placeholder={this.props.getTranslation(placeholderTKey)} placeholder={placeholder}
disabled={disabled} disabled={disabled}
options={choices.map((c) => ({ options={choices.map((c) => ({
key: c.key, key: c.key,
text: this.props.getTranslation(c.label), text: c.label,
}))} }))}
styles={{ styles={{
root: { width: 400 }, root: { width: 400 },

View File

@@ -2346,13 +2346,11 @@ export default class Explorer {
this.tabsManager.activateTab(notebookTab); this.tabsManager.activateTab(notebookTab);
} else { } else {
const options: NotebookTabOptions = { const options: NotebookTabOptions = {
account: userContext.databaseAccount,
tabKind: ViewModels.CollectionTabKind.NotebookV2, tabKind: ViewModels.CollectionTabKind.NotebookV2,
node: null, node: null,
title: notebookContentItem.name, title: notebookContentItem.name,
tabPath: notebookContentItem.path, tabPath: notebookContentItem.path,
collection: null, collection: null,
masterKey: userContext.masterKey || "",
hashLocation: "notebooks", hashLocation: "notebooks",
isActive: ko.observable(false), isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true), isTabsContentExpanded: ko.observable(true),

View File

@@ -36,36 +36,7 @@ export class CommandBarComponentButtonFactory {
} }
const newCollectionBtn = CommandBarComponentButtonFactory.createNewCollectionGroup(container); const newCollectionBtn = CommandBarComponentButtonFactory.createNewCollectionGroup(container);
const buttons: CommandButtonComponentProps[] = []; const buttons: CommandButtonComponentProps[] = [newCollectionBtn];
if (container.isFeatureEnabled && container.isFeatureEnabled("regionselectbutton")) {
const regions = [{ name: "West US" }, { name: "East US" }, { name: "North Europe" }];
buttons.push({
iconSrc: null,
onCommandClick: () => {},
commandButtonLabel: null,
hasPopup: false,
isDropdown: true,
dropdownPlaceholder: "West US",
dropdownSelectedKey: "West US",
dropdownWidth: 100,
children: regions.map(
(region) =>
({
iconSrc: null,
onCommandClick: () => {},
commandButtonLabel: region.name,
dropdownItemKey: region.name,
hasPopup: false,
disabled: false,
ariaLabel: "",
} as CommandButtonComponentProps)
),
ariaLabel: "",
});
}
buttons.push(newCollectionBtn);
const addSynapseLink = CommandBarComponentButtonFactory.createOpenSynapseLinkDialogButton(container); const addSynapseLink = CommandBarComponentButtonFactory.createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) { if (addSynapseLink) {

View File

@@ -0,0 +1,26 @@
.mongoQueryComponent {
margin-left: 10px;
height: 100%;
overflow-y: auto;
width: 100%;
input {
margin-top: 0;
}
label {
padding: 0;
margin-bottom: 0;
}
label:before {
top: 2px;
left: 2px;
height: 16px;
width: 16px;
}
.queryInput {
border: 1px solid black;
margin: 5px;
}
}

View File

@@ -0,0 +1,185 @@
import * as React from "react";
import { Dispatch } from "redux";
import MonacoEditor from "@nteract/monaco-editor";
import { PrimaryButton } from "office-ui-fabric-react";
import { ChoiceGroup, IChoiceGroupOption } from "office-ui-fabric-react/lib/ChoiceGroup";
import Outputs from "@nteract/stateful-components/lib/outputs";
import { KernelOutputError, StreamText } from "@nteract/outputs";
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
import { actions, selectors, AppState, ContentRef, KernelRef } from "@nteract/core";
import loadTransform from "../NotebookComponent/loadTransform";
import { connect } from "react-redux";
import "./MongoQueryComponent.less";
interface MongoQueryComponentPureProps {
contentRef: ContentRef;
kernelRef: KernelRef;
databaseId: string;
collectionId: string;
}
interface MongoQueryComponentDispatchProps {
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 MongoQueryComponentState {
query: string;
outputType: OutputType;
}
const options: IChoiceGroupOption[] = [
{ key: "rich", text: "Rich Output" },
{ key: "json", text: "Json Output" },
];
type MongoQueryComponentProps = MongoQueryComponentPureProps & StateProps & MongoQueryComponentDispatchProps;
export class MongoQueryComponent extends React.Component<MongoQueryComponentProps, MongoQueryComponentState> {
constructor(props: MongoQueryComponentProps) {
super(props);
this.state = {
query: this.props.inputValue,
outputType: "rich",
};
}
componentDidMount(): void {
loadTransform(this.props);
}
private onExecute = () => {
const query = JSON.parse(this.state.query);
query["database"] = this.props.databaseId;
query["collection"] = this.props.collectionId;
query["outputType"] = this.state.outputType;
this.props.updateCell(JSON.stringify(query), this.props.firstCellId, this.props.contentRef);
this.props.runCell(this.props.contentRef, this.props.firstCellId);
};
private onOutputTypeChange = (
e: React.FormEvent<HTMLElement | HTMLInputElement>,
option: IChoiceGroupOption
): void => {
const outputType = option.key as OutputType;
this.setState({ outputType }, () => this.onInputChange(this.props.inputValue));
};
private onInputChange = (text: string) => {
this.setState({ query: text });
};
render(): JSX.Element {
const { firstCellId: id, contentRef } = this.props;
if (!id) {
return <></>;
}
return (
<div className="mongoQueryComponent">
<div className="queryInput">
<MonacoEditor
id={this.props.firstCellId}
contentRef={this.props.contentRef}
theme={""}
language="json"
onChange={this.onInputChange}
value={this.state.query}
/>
</div>
<PrimaryButton text="Run" onClick={this.onExecute} disabled={!this.props.firstCellId} />
<ChoiceGroup
selectedKey={this.state.outputType}
options={options}
onChange={this.onOutputTypeChange}
label="Output Type"
styles={{ root: { marginTop: 0 } }}
/>
<hr />
<Outputs id={id} contentRef={contentRef}>
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
<KernelOutputError />
<StreamText />
</Outputs>
</div>
);
}
}
interface StateProps {
firstCellId: string;
inputValue: string;
}
interface InitialProps {
contentRef: string;
}
// Redux
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
const { contentRef } = initialProps;
const mapStateToProps = (state: AppState) => {
let firstCellId;
let inputValue = "";
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 cell = selectors.notebook.cellById(content.model, { id: firstCellId });
// Parse to extract filter and output type
const cellValue = cell.get("source", "");
if (cellValue) {
try {
const filterValue = JSON.parse(cellValue).filter;
if (filterValue) {
inputValue = filterValue;
}
} catch (e) {
console.error("Could not parse", e);
}
}
}
}
return {
firstCellId,
inputValue,
};
};
return mapStateToProps;
};
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: MongoQueryComponentProps) => {
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)(MongoQueryComponent);

View File

@@ -0,0 +1,89 @@
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import {
NotebookComponentBootstrapper,
NotebookComponentBootstrapperOptions,
} from "../NotebookComponent/NotebookComponentBootstrapper";
import MongoQueryComponent from "../MongoQueryComponent/MongoQueryComponent";
import { actions, createContentRef, createKernelRef, IContent, KernelRef } from "@nteract/core";
import { Provider } from "react-redux";
import { Notebook } from "@nteract/commutable";
export class MongoQueryComponentAdapter 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: "mongo-query-component-notebook.ipynb",
path: "mongo-query-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()}>
<MongoQueryComponent {...props} />;
</Provider>
);
}
}

View File

@@ -41,16 +41,16 @@ export class NotebookContainerClient {
} }
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> { private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
if (this.isResettingWorkspace) {
return undefined;
}
if (!this.notebookServerInfo() || !this.notebookServerInfo().notebookServerEndpoint) { if (!this.notebookServerInfo() || !this.notebookServerInfo().notebookServerEndpoint) {
const error = "No server endpoint detected"; const error = "No server endpoint detected";
Logger.logError(error, "NotebookContainerClient/getMemoryUsage"); Logger.logError(error, "NotebookContainerClient/getMemoryUsage");
return Promise.reject(error); return Promise.reject(error);
} }
if (this.isResettingWorkspace) {
return undefined;
}
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig(); const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
try { try {
const response = await fetch(`${notebookServerEndpoint}/api/metrics/memory`, { const response = await fetch(`${notebookServerEndpoint}/api/metrics/memory`, {

View File

@@ -818,7 +818,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
let indexingPolicy: DataModels.IndexingPolicy; let indexingPolicy: DataModels.IndexingPolicy;
let createMongoWildcardIndex: boolean; let createMongoWildcardIndex: boolean;
// todo - remove mongo indexing policy ticket # 616274 // todo - remove mongo indexing policy ticket # 616274
if (this.container.isPreferredApiMongoDB() && this.container.isEnableMongoCapabilityPresent()) { if (this.container.isPreferredApiMongoDB()) {
createMongoWildcardIndex = this.shouldCreateMongoWildcardIndex(); createMongoWildcardIndex = this.shouldCreateMongoWildcardIndex();
} else if (this.showIndexingOptionsForSharedThroughput()) { } else if (this.showIndexingOptionsForSharedThroughput()) {
if (this.useIndexingForSharedThroughput()) { if (this.useIndexingForSharedThroughput()) {

View File

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

View File

@@ -0,0 +1,49 @@
import * as Q from "q";
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
import { MongoQueryComponentAdapter } from "../Notebook/MongoQueryComponent/MongoQueryComponentAdapter";
export default class MongoDocumentsTabV2 extends NotebookTabBase {
private mongoQueryComponentAdapter: MongoQueryComponentAdapter;
constructor(options: NotebookTabBaseOptions) {
super(options);
this.mongoQueryComponentAdapter = new MongoQueryComponentAdapter(
{
contentRef: undefined,
notebookClient: NotebookTabBase.clientManager,
},
options.collection?.databaseId,
options.collection?.id()
);
}
public onCloseTabButtonClick(): Q.Promise<void> {
super.onCloseTabButtonClick();
// const cleanup = () => {
// this.notebookComponentAdapter.notebookShutdown();
// this.isActive(false);
// super.onCloseTabButtonClick();
// };
// if (this.notebookComponentAdapter.isContentDirty()) {
// this.container.showOkCancelModalDialog(
// "Close without saving?",
// `File has unsaved changes, close without saving?`,
// "Close",
// cleanup,
// "Cancel",
// undefined
// );
// return Q.resolve(null);
// } else {
// cleanup();
// return Q.resolve(null);
// }
return undefined;
}
protected buildCommandBarOptions(): void {
this.updateNavbarWithTabsButtons();
}
}

View File

@@ -0,0 +1,50 @@
import * as ViewModels from "../../Contracts/ViewModels";
import TabsBase from "./TabsBase";
import { NotebookClientV2 } from "../Notebook/NotebookClientV2";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Explorer from "../Explorer";
import { ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { Areas } from "../../Common/Constants";
export interface NotebookTabBaseOptions extends ViewModels.TabOptions {
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
*/
protected getContainer(): Explorer {
return this.container;
}
protected traceTelemetry(actionType: number): void {
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience && this.container.defaultExperience(),
dataExplorerArea: Areas.Notebook,
});
}
}

View File

@@ -2,9 +2,7 @@ import * as _ from "underscore";
import * as Q from "q"; import * as Q from "q";
import * as ko from "knockout"; import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import TabsBase from "./TabsBase";
import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg"; import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg";
import CutIcon from "../../../images/notebook/Notebook-cut.svg"; import CutIcon from "../../../images/notebook/Notebook-cut.svg";
@@ -17,33 +15,27 @@ import SaveIcon from "../../../images/save-cosmos.svg";
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg"; import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
import InterruptKernelIcon from "../../../images/notebook/Notebook-stop.svg"; import InterruptKernelIcon from "../../../images/notebook/Notebook-stop.svg";
import KillKernelIcon from "../../../images/notebook/Notebook-stop.svg"; import KillKernelIcon from "../../../images/notebook/Notebook-stop.svg";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { ArmApiVersions } from "../../Common/Constants";
import { Areas, ArmApiVersions } from "../../Common/Constants";
import { CommandBarComponentButtonFactory } from "../Menus/CommandBar/CommandBarComponentButtonFactory"; import { CommandBarComponentButtonFactory } from "../Menus/CommandBar/CommandBarComponentButtonFactory";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter"; import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
import { NotebookConfigurationUtils } from "../../Utils/NotebookConfigurationUtils"; import { NotebookConfigurationUtils } from "../../Utils/NotebookConfigurationUtils";
import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2"; import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import Explorer from "../Explorer";
import { NotebookContentItem } from "../Notebook/NotebookContentItem"; import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { toJS, stringifyNotebook } from "@nteract/commutable"; import { toJS, stringifyNotebook } from "@nteract/commutable";
import { appInsights } from "../../Shared/appInsights"; import { appInsights } from "../../Shared/appInsights";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
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 {
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;
@@ -52,16 +44,6 @@ 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((newValue: DataModels.NotebookWorkspaceConnectionInfo) => { this.container.notebookServerInfo.subscribe((newValue: DataModels.NotebookWorkspaceConnectionInfo) => {
@@ -71,7 +53,7 @@ export default class NotebookTabV2 extends TabsBase {
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,
}); });
@@ -117,10 +99,6 @@ export default class NotebookTabV2 extends TabsBase {
return await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName()); return await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName());
} }
protected getContainer(): Explorer {
return this.container;
}
protected getTabsButtons(): CommandButtonComponentProps[] { protected getTabsButtons(): CommandButtonComponentProps[] {
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs(); const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
@@ -504,12 +482,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, {
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
defaultExperience: this.container.defaultExperience && this.container.defaultExperience(),
dataExplorerArea: Areas.Notebook,
});
}
} }

View File

@@ -5,6 +5,7 @@ import SparkMasterTabTemplate from "./SparkMasterTab.html";
import NotebookV2TabTemplate from "./NotebookV2Tab.html"; import NotebookV2TabTemplate from "./NotebookV2Tab.html";
import TerminalTabTemplate from "./TerminalTab.html"; import TerminalTabTemplate from "./TerminalTab.html";
import MongoDocumentsTabTemplate from "./MongoDocumentsTab.html"; import MongoDocumentsTabTemplate from "./MongoDocumentsTab.html";
import MongoDocumentsTabV2Template from "./MongoDocumentsTabV2.html";
import MongoQueryTabTemplate from "./MongoQueryTab.html"; import MongoQueryTabTemplate from "./MongoQueryTab.html";
import MongoShellTabTemplate from "./MongoShellTab.html"; import MongoShellTabTemplate from "./MongoShellTab.html";
import QueryTabTemplate from "./QueryTab.html"; import QueryTabTemplate from "./QueryTab.html";
@@ -105,6 +106,15 @@ export class MongoQueryTab {
} }
} }
export class MongoDocumentsTabV2 {
constructor() {
return {
viewModel: TabComponent,
template: MongoDocumentsTabV2Template,
};
}
}
export class MongoShellTab { export class MongoShellTab {
constructor() { constructor() {
return { return {

View File

@@ -1,5 +1,6 @@
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos"; import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
import * as ko from "knockout"; import * as ko from "knockout";
import Q from "q";
import * as _ from "underscore"; import * as _ from "underscore";
import UploadWorker from "worker-loader!../../workers/upload"; import UploadWorker from "worker-loader!../../workers/upload";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
@@ -22,6 +23,7 @@ import ConflictsTab from "../Tabs/ConflictsTab";
import DocumentsTab from "../Tabs/DocumentsTab"; import DocumentsTab from "../Tabs/DocumentsTab";
import GraphTab from "../Tabs/GraphTab"; import GraphTab from "../Tabs/GraphTab";
import MongoDocumentsTab from "../Tabs/MongoDocumentsTab"; import MongoDocumentsTab from "../Tabs/MongoDocumentsTab";
import MongoDocumentsTabV2 from "../Tabs/MongoDocumentsTabV2";
import MongoQueryTab from "../Tabs/MongoQueryTab"; 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";
@@ -253,9 +255,9 @@ export default class Collection implements ViewModels.Collection {
}); });
} }
public expandCollection(): void { public expandCollection(): Q.Promise<any> {
if (this.isCollectionExpanded()) { if (this.isCollectionExpanded()) {
return; return Q();
} }
this.isCollectionExpanded(true); this.isCollectionExpanded(true);
@@ -267,6 +269,8 @@ export default class Collection implements ViewModels.Collection {
defaultExperience: this.container.defaultExperience(), defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
return Q.resolve();
} }
public onDocumentDBDocumentsClick() { public onDocumentDBDocumentsClick() {
@@ -493,11 +497,11 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
const mongoDocumentsTabs: MongoDocumentsTab[] = this.container.tabsManager.getTabs( const mongoDocumentsTabs: MongoDocumentsTabV2[] = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.Documents, ViewModels.CollectionTabKind.Documents,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id() (tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as MongoDocumentsTab[]; ) as MongoDocumentsTabV2[];
let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0]; let mongoDocumentsTab: MongoDocumentsTabV2 = mongoDocumentsTabs && mongoDocumentsTabs[0];
if (mongoDocumentsTab) { if (mongoDocumentsTab) {
this.container.tabsManager.activateTab(mongoDocumentsTab); this.container.tabsManager.activateTab(mongoDocumentsTab);
@@ -512,9 +516,8 @@ export default class Collection implements ViewModels.Collection {
}); });
this.documentIds([]); this.documentIds([]);
mongoDocumentsTab = new MongoDocumentsTab({ mongoDocumentsTab = new MongoDocumentsTabV2({
partitionKey: this.partitionKey, container: this.container,
documentIds: this.documentIds,
tabKind: ViewModels.CollectionTabKind.Documents, tabKind: ViewModels.CollectionTabKind.Documents,
title: "Documents", title: "Documents",
tabPath: "", tabPath: "",
@@ -544,7 +547,7 @@ export default class Collection implements ViewModels.Collection {
}); });
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings"; const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
const pendingNotificationsPromise: Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification(); const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.SettingsV2, (tab) => { const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.SettingsV2, (tab) => {
return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id(); return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id();
}); });
@@ -577,7 +580,7 @@ export default class Collection implements ViewModels.Collection {
settingsTabV2: SettingsTabV2, settingsTabV2: SettingsTabV2,
traceStartData: any, traceStartData: any,
settingsTabOptions: ViewModels.TabOptions, settingsTabOptions: ViewModels.TabOptions,
getPendingNotification: Promise<DataModels.Notification> getPendingNotification: Q.Promise<DataModels.Notification>
): void => { ): void => {
const settingsTabV2Options: ViewModels.SettingsTabV2Options = { const settingsTabV2Options: ViewModels.SettingsTabV2Options = {
...settingsTabOptions, ...settingsTabOptions,
@@ -977,19 +980,19 @@ export default class Collection implements ViewModels.Collection {
this.container.deleteCollectionConfirmationPane.open(); this.container.deleteCollectionConfirmationPane.open();
} }
public uploadFiles = (fileList: FileList): Promise<UploadDetails> => { public uploadFiles = (fileList: FileList): Q.Promise<UploadDetails> => {
// TODO: right now web worker is not working with AAD flow. Use main thread for upload for now until we have backend upload capability // TODO: right now web worker is not working with AAD flow. Use main thread for upload for now until we have backend upload capability
if (configContext.platform === Platform.Hosted && window.authType === AuthType.AAD) { if (configContext.platform === Platform.Hosted && window.authType === AuthType.AAD) {
return this._uploadFilesCors(fileList); return this._uploadFilesCors(fileList);
} }
const documentUploader: Worker = new UploadWorker(); const documentUploader: Worker = new UploadWorker();
const deferred: Q.Deferred<UploadDetails> = Q.defer<UploadDetails>();
let inProgressNotificationId: string = ""; let inProgressNotificationId: string = "";
if (!fileList || fileList.length === 0) { if (!fileList || fileList.length === 0) {
return Promise.reject("No files specified"); return Q.reject("No files specified");
} }
documentUploader.onmessage = (event: MessageEvent) => {
const onmessage = (resolve: (value: UploadDetails) => void, reject: (reason: any) => void, event: MessageEvent) => {
const numSuccessful: number = event.data.numUploadsSuccessful; const numSuccessful: number = event.data.numUploadsSuccessful;
const numFailed: number = event.data.numUploadsFailed; const numFailed: number = event.data.numUploadsFailed;
const runtimeError: string = event.data.runtimeError; const runtimeError: string = event.data.runtimeError;
@@ -998,26 +1001,31 @@ export default class Collection implements ViewModels.Collection {
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressNotificationId); NotificationConsoleUtils.clearInProgressMessageWithId(inProgressNotificationId);
documentUploader.terminate(); documentUploader.terminate();
if (!!runtimeError) { if (!!runtimeError) {
reject(runtimeError); deferred.reject(runtimeError);
} else if (numSuccessful === 0) { } else if (numSuccessful === 0) {
// all uploads failed // all uploads failed
NotificationConsoleUtils.logConsoleError(`Failed to upload all documents to container ${this.id()}`); NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to upload all documents to container ${this.id()}`
);
} else if (numFailed > 0) { } else if (numFailed > 0) {
NotificationConsoleUtils.logConsoleError( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to upload ${numFailed} of ${numSuccessful + numFailed} documents to container ${this.id()}` `Failed to upload ${numFailed} of ${numSuccessful + numFailed} documents to container ${this.id()}`
); );
} else { } else {
NotificationConsoleUtils.logConsoleInfo( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully uploaded all ${numSuccessful} documents to container ${this.id()}` `Successfully uploaded all ${numSuccessful} documents to container ${this.id()}`
); );
} }
this._logUploadDetailsInConsole(uploadDetails); this._logUploadDetailsInConsole(uploadDetails);
resolve(uploadDetails); deferred.resolve(uploadDetails);
}; };
function onerror(reject: (reason: any) => void, event: ErrorEvent) { documentUploader.onerror = (event: ErrorEvent): void => {
documentUploader.terminate(); documentUploader.terminate();
reject(event.error); deferred.reject(event.error);
} };
const uploaderMessage: StartUploadMessageParams = { const uploaderMessage: StartUploadMessageParams = {
files: fileList, files: fileList,
@@ -1032,33 +1040,42 @@ export default class Collection implements ViewModels.Collection {
}, },
}; };
return new Promise<UploadDetails>((resolve, reject) => { documentUploader.postMessage(uploaderMessage);
documentUploader.onmessage = onmessage.bind(null, resolve, reject); inProgressNotificationId = NotificationConsoleUtils.logConsoleMessage(
documentUploader.onerror = onerror.bind(null, reject); ConsoleDataType.InProgress,
`Uploading and creating documents in container ${this.id()}`
);
documentUploader.postMessage(uploaderMessage); return deferred.promise;
inProgressNotificationId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Uploading and creating documents in container ${this.id()}`
);
});
}; };
private async _uploadFilesCors(files: FileList): Promise<UploadDetails> { private _uploadFilesCors(files: FileList): Q.Promise<UploadDetails> {
const data = await Promise.all(Array.from(files).map((file) => this._uploadFile(file))); const deferred: Q.Deferred<UploadDetails> = Q.defer<UploadDetails>();
const promises: Array<Q.Promise<UploadDetailsRecord>> = [];
return { data }; for (let i = 0; i < files.length; i++) {
promises.push(this._uploadFile(files[i]));
}
Q.all(promises).then((uploadDetails: Array<UploadDetailsRecord>) => {
deferred.resolve({ data: uploadDetails });
});
return deferred.promise;
} }
private _uploadFile(file: File): Promise<UploadDetailsRecord> { private _uploadFile(file: File): Q.Promise<UploadDetailsRecord> {
const deferred: Q.Deferred<UploadDetailsRecord> = Q.defer();
const reader = new FileReader(); const reader = new FileReader();
const onload = (resolve: (value: UploadDetailsRecord) => void, evt: any): void => { reader.onload = (evt: any): void => {
const fileData: string = evt.target.result; const fileData: string = evt.target.result;
this._createDocumentsFromFile(file.name, fileData).then((record) => resolve(record)); this._createDocumentsFromFile(file.name, fileData).then((record) => {
deferred.resolve(record);
});
}; };
const onerror = (resolve: (value: UploadDetailsRecord) => void, evt: ProgressEvent): void => { reader.onerror = (evt: ProgressEvent): void => {
resolve({ deferred.resolve({
fileName: file.name, fileName: file.name,
numSucceeded: 0, numSucceeded: 0,
numFailed: 1, numFailed: 1,
@@ -1066,11 +1083,9 @@ export default class Collection implements ViewModels.Collection {
}); });
}; };
return new Promise<UploadDetailsRecord>((resolve) => { reader.readAsText(file);
reader.onload = onload.bind(this, resolve);
reader.onerror = onerror.bind(this, resolve); return deferred.promise;
reader.readAsText(file);
});
} }
private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> { private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> {
@@ -1104,35 +1119,46 @@ export default class Collection implements ViewModels.Collection {
} }
} }
private async _getPendingThroughputSplitNotification(): Promise<DataModels.Notification> { private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
if (!this.container) { if (!this.container) {
return undefined; return Q.resolve(undefined);
} }
const throughputUpdateRegExp = new RegExp("Throughput update (.*) in progress"); const deferred: Q.Deferred<DataModels.Notification> = Q.defer<DataModels.Notification>();
try { fetchPortalNotifications().then(
const notifications = await fetchPortalNotifications(); (notifications: DataModels.Notification[]) => {
if (!notifications) { if (!notifications || notifications.length === 0) {
return undefined; deferred.resolve(undefined);
return;
}
const pendingNotification = _.find(notifications, (notification: DataModels.Notification) => {
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress");
return (
notification.kind === "message" &&
notification.collectionName === this.id() &&
notification.description &&
throughputUpdateRegExp.test(notification.description)
);
});
deferred.resolve(pendingNotification);
},
(error: any) => {
Logger.logError(
JSON.stringify({
error: getErrorMessage(error),
accountName: this.container && this.container.databaseAccount(),
databaseName: this.databaseId,
collectionName: this.id(),
}),
"Settings tree node"
);
deferred.resolve(undefined);
} }
);
return notifications.find( return deferred.promise;
({ kind, collectionName, description = "" }) =>
kind === "message" && collectionName === this.id() && throughputUpdateRegExp.test(description)
);
} catch (error) {
Logger.logError(
JSON.stringify({
error: getErrorMessage(error),
accountName: this.container && this.container.databaseAccount(),
databaseName: this.databaseId,
collectionName: this.id(),
}),
"Settings tree node"
);
}
return undefined;
} }
private _logUploadDetailsInConsole(uploadDetails: UploadDetails): void { private _logUploadDetailsInConsole(uploadDetails: UploadDetails): void {

View File

@@ -41,9 +41,9 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
this.isCollectionExpanded = ko.observable<boolean>(true); this.isCollectionExpanded = ko.observable<boolean>(true);
} }
public expandCollection(): void { public expandCollection(): Q.Promise<void> {
if (this.isCollectionExpanded()) { if (this.isCollectionExpanded()) {
return; return Q();
} }
this.isCollectionExpanded(true); this.isCollectionExpanded(true);
@@ -55,6 +55,8 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
defaultExperience: this.container.defaultExperience(), defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
return Q.resolve();
} }
public collapseCollection() { public collapseCollection() {

View File

@@ -1,33 +0,0 @@
{
"translations": {
"Common": {
"Save": "Save",
"Discard": "Discard",
"Refresh": "Refesh"
},
"SelfServeExample": {
"North Central US": "North Central US",
"West US": "West US",
"East US 2": "East US 2",
"ClassInfo": "This is a self serve class",
"RegionDropdownInfo": "More regions can be added in the future.",
"ValidationError": "Regions and AccountName should not be empty.",
"DescriptionText": "This class sets collection and database throughput.",
"DecriptionLinkText": "Click here for more information",
"Regions": "Regions",
"RegionsPlaceholder": "Select a region",
"Enable Logging": "Enable Logging",
"Enable": "Enable",
"Disable": "Disable",
"Account Name": "Account Name",
"AccountNamePlaceHolder": "Enter the account name",
"Collection Throughput": "Collection Throughput",
"Enable DB level throughput": "Enable DB level throughput",
"Database Throughput": "Database Throughput",
"RefreshMessage": "Self Serve Example successfully refreshing",
"SubmissionMessage": "Submitted successfully"
},
"SqlX": {
}
}
}

View File

@@ -288,9 +288,11 @@ export class TabRouteHandler {
private _openSprocTabForResource(databaseId: string, collectionId: string, sprocId: string): void { private _openSprocTabForResource(databaseId: string, collectionId: string, sprocId: string): void {
this._executeActionHelper(() => { this._executeActionHelper(() => {
const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId); const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId);
collection && collection.expandCollection(); collection &&
const storedProcedure = collection && collection.findStoredProcedureWithId(sprocId); collection.expandCollection().then(() => {
storedProcedure && storedProcedure.open(); const storedProcedure = collection && collection.findStoredProcedureWithId(sprocId);
storedProcedure && storedProcedure.open();
});
}); });
} }
@@ -317,9 +319,11 @@ export class TabRouteHandler {
private _openTriggerTabForResource(databaseId: string, collectionId: string, triggerId: string): void { private _openTriggerTabForResource(databaseId: string, collectionId: string, triggerId: string): void {
this._executeActionHelper(() => { this._executeActionHelper(() => {
const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId); const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId);
collection && collection.expandCollection(); collection &&
const trigger = collection && collection.findTriggerWithId(triggerId); collection.expandCollection().then(() => {
trigger && trigger.open(); const trigger = collection && collection.findTriggerWithId(triggerId);
trigger && trigger.open();
});
}); });
} }
@@ -346,9 +350,11 @@ export class TabRouteHandler {
private _openUserDefinedFunctionTabForResource(databaseId: string, collectionId: string, udfId: string): void { private _openUserDefinedFunctionTabForResource(databaseId: string, collectionId: string, udfId: string): void {
this._executeActionHelper(() => { this._executeActionHelper(() => {
const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId); const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId);
collection && collection.expandCollection(); collection &&
const userDefinedFunction = collection && collection.findUserDefinedFunctionWithId(udfId); collection.expandCollection().then(() => {
userDefinedFunction && userDefinedFunction.open(); const userDefinedFunction = collection && collection.findUserDefinedFunctionWithId(udfId);
userDefinedFunction && userDefinedFunction.open();
});
}); });
} }

View File

@@ -8,7 +8,7 @@ interface Decorator {
} }
interface InputOptionsBase { interface InputOptionsBase {
labelTKey: string; label: string;
} }
export interface NumberInputOptions extends InputOptionsBase { export interface NumberInputOptions extends InputOptionsBase {
@@ -19,17 +19,17 @@ export interface NumberInputOptions extends InputOptionsBase {
} }
export interface StringInputOptions extends InputOptionsBase { export interface StringInputOptions extends InputOptionsBase {
placeholderTKey?: (() => Promise<string>) | string; placeholder?: (() => Promise<string>) | string;
} }
export interface BooleanInputOptions extends InputOptionsBase { export interface BooleanInputOptions extends InputOptionsBase {
trueLabelTKey: (() => Promise<string>) | string; trueLabel: (() => Promise<string>) | string;
falseLabelTKey: (() => Promise<string>) | string; falseLabel: (() => Promise<string>) | string;
} }
export interface ChoiceInputOptions extends InputOptionsBase { export interface ChoiceInputOptions extends InputOptionsBase {
choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[]; choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
placeholderTKey?: (() => Promise<string>) | string; placeholder?: (() => Promise<string>) | string;
} }
export interface DescriptionDisplayOptions { export interface DescriptionDisplayOptions {
@@ -48,7 +48,7 @@ const isNumberInputOptions = (inputOptions: InputOptions): inputOptions is Numbe
}; };
const isBooleanInputOptions = (inputOptions: InputOptions): inputOptions is BooleanInputOptions => { const isBooleanInputOptions = (inputOptions: InputOptions): inputOptions is BooleanInputOptions => {
return "trueLabelTKey" in inputOptions; return "trueLabel" in inputOptions;
}; };
const isChoiceInputOptions = (inputOptions: InputOptions): inputOptions is ChoiceInputOptions => { const isChoiceInputOptions = (inputOptions: InputOptions): inputOptions is ChoiceInputOptions => {
@@ -92,7 +92,7 @@ export const PropertyInfo = (info: (() => Promise<Info>) | Info): PropertyDecora
export const Values = (inputOptions: InputOptions): PropertyDecorator => { export const Values = (inputOptions: InputOptions): PropertyDecorator => {
if (isNumberInputOptions(inputOptions)) { if (isNumberInputOptions(inputOptions)) {
return addToMap( return addToMap(
{ name: "labelTKey", value: inputOptions.labelTKey }, { name: "label", value: inputOptions.label },
{ name: "min", value: inputOptions.min }, { name: "min", value: inputOptions.min },
{ name: "max", value: inputOptions.max }, { name: "max", value: inputOptions.max },
{ name: "step", value: inputOptions.step }, { name: "step", value: inputOptions.step },
@@ -100,22 +100,22 @@ export const Values = (inputOptions: InputOptions): PropertyDecorator => {
); );
} else if (isBooleanInputOptions(inputOptions)) { } else if (isBooleanInputOptions(inputOptions)) {
return addToMap( return addToMap(
{ name: "labelTKey", value: inputOptions.labelTKey }, { name: "label", value: inputOptions.label },
{ name: "trueLabelTKey", value: inputOptions.trueLabelTKey }, { name: "trueLabel", value: inputOptions.trueLabel },
{ name: "falseLabelTKey", value: inputOptions.falseLabelTKey } { name: "falseLabel", value: inputOptions.falseLabel }
); );
} else if (isChoiceInputOptions(inputOptions)) { } else if (isChoiceInputOptions(inputOptions)) {
return addToMap( return addToMap(
{ name: "labelTKey", value: inputOptions.labelTKey }, { name: "label", value: inputOptions.label },
{ name: "placeholderTKey", value: inputOptions.placeholderTKey }, { name: "placeholder", value: inputOptions.placeholder },
{ name: "choices", value: inputOptions.choices } { name: "choices", value: inputOptions.choices }
); );
} else if (isDescriptionDisplayOptions(inputOptions)) { } else if (isDescriptionDisplayOptions(inputOptions)) {
return addToMap({ name: "description", value: inputOptions.description }); return addToMap({ name: "description", value: inputOptions.description });
} else { } else {
return addToMap( return addToMap(
{ name: "labelTKey", value: inputOptions.labelTKey }, { name: "label", value: inputOptions.label },
{ name: "placeholderTKey", value: inputOptions.placeholderTKey } { name: "placeholder", value: inputOptions.placeholder }
); );
} }
}; };

View File

@@ -16,22 +16,10 @@ export interface InitializeResponse {
dbThroughput: number; dbThroughput: number;
} }
export const getMaxCollectionThroughput = async (): Promise<number> => { export const getMaxThroughput = async (): Promise<number> => {
return 10000; return 10000;
}; };
export const getMinCollectionThroughput = async (): Promise<number> => {
return 400;
};
export const getMaxDatabaseThroughput = async (): Promise<number> => {
return 10000;
};
export const getMinDatabaseThroughput = async (): Promise<number> => {
return 400;
};
export const update = async ( export const update = async (
regions: Regions, regions: Regions,
enableLogging: boolean, enableLogging: boolean,
@@ -71,6 +59,6 @@ export const onRefreshSelfServeExample = async (): Promise<RefreshResult> => {
const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded"; const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded";
return { return {
isUpdateInProgress: isUpdateInProgress, isUpdateInProgress: isUpdateInProgress,
notificationMessage: "RefreshMessage", notificationMessage: "Self Serve Example successfully refreshing",
}; };
}; };

View File

@@ -10,16 +10,7 @@ import {
SelfServeNotificationType, SelfServeNotificationType,
SmartUiInput, SmartUiInput,
} from "../SelfServeTypes"; } from "../SelfServeTypes";
import { import { onRefreshSelfServeExample, getMaxThroughput, Regions, update, initialize } from "./SelfServeExample.rp";
onRefreshSelfServeExample,
Regions,
update,
initialize,
getMinDatabaseThroughput,
getMaxDatabaseThroughput,
getMinCollectionThroughput,
getMaxCollectionThroughput,
} from "./SelfServeExample.rp";
const regionDropdownItems: ChoiceItem[] = [ const regionDropdownItems: ChoiceItem[] = [
{ label: "North Central US", key: Regions.NorthCentralUS }, { label: "North Central US", key: Regions.NorthCentralUS },
@@ -28,11 +19,11 @@ const regionDropdownItems: ChoiceItem[] = [
]; ];
const selfServeExampleInfo: Info = { const selfServeExampleInfo: Info = {
messageTKey: "ClassInfo", message: "This is a self serve class",
}; };
const regionDropdownInfo: Info = { const regionDropdownInfo: Info = {
messageTKey: "RegionDropdownInfo", message: "More regions can be added in the future.",
}; };
const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: InputType): Map<string, SmartUiInput> => { const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: InputType): Map<string, SmartUiInput> => {
@@ -59,7 +50,7 @@ const onEnableDbLevelThroughputChange = (
const validate = (currentvalues: Map<string, SmartUiInput>): void => { const validate = (currentvalues: Map<string, SmartUiInput>): void => {
if (!currentvalues.get("regions").value || !currentvalues.get("accountName").value) { if (!currentvalues.get("regions").value || !currentvalues.get("accountName").value) {
throw new Error("ValidationError"); throw new Error("Regions and AccountName should not be empty.");
} }
}; };
@@ -75,9 +66,6 @@ const validate = (currentvalues: Map<string, SmartUiInput>): void => {
You can test this self serve UI by using the featureflag '?feature.selfServeType=example' You can test this self serve UI by using the featureflag '?feature.selfServeType=example'
and plumb in similar feature flags for your own self serve class. and plumb in similar feature flags for your own self serve class.
All string to be used should be present in the "src/Localization" folder, in the language specific json files. The
corresponding key should be given as the value for the fields like "label", the error message etc.
*/ */
/* /*
@@ -129,7 +117,7 @@ export default class SelfServeExample extends SelfServeBaseClass {
let dbThroughput = currentValues.get("dbThroughput")?.value as number; let dbThroughput = currentValues.get("dbThroughput")?.value as number;
dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined; dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined;
await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput); await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput);
return { message: "SubmissionMessage", type: SelfServeNotificationType.info }; return { message: "submitted successfully", type: SelfServeNotificationType.info };
}; };
/* /*
@@ -173,10 +161,10 @@ export default class SelfServeExample extends SelfServeBaseClass {
*/ */
@Values({ @Values({
description: { description: {
textTKey: "DescriptionText", text: "This class sets collection and database throughput.",
link: { link: {
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
textTKey: "DecriptionLinkText", text: "Click here for more information",
}, },
}, },
}) })
@@ -205,26 +193,26 @@ export default class SelfServeExample extends SelfServeBaseClass {
any other value of "regions" any other value of "regions"
*/ */
@OnChange(onRegionsChange) @OnChange(onRegionsChange)
@Values({ labelTKey: "Regions", choices: regionDropdownItems, placeholderTKey: "RegionsPlaceholder" }) @Values({ label: "Regions", choices: regionDropdownItems, placeholder: "Select a region" })
regions: ChoiceItem; regions: ChoiceItem;
@Values({ @Values({
labelTKey: "Enable Logging", label: "Enable Logging",
trueLabelTKey: "Enable", trueLabel: "Enable",
falseLabelTKey: "Disable", falseLabel: "Disable",
}) })
enableLogging: boolean; enableLogging: boolean;
@Values({ @Values({
labelTKey: "Account Name", label: "Account Name",
placeholderTKey: "AccountNamePlaceHolder", placeholder: "Enter the account name",
}) })
accountName: string; accountName: string;
@Values({ @Values({
labelTKey: "Collection Throughput", label: "Collection Throughput",
min: getMinCollectionThroughput, min: 400,
max: getMaxCollectionThroughput, max: getMaxThroughput,
step: 100, step: 100,
uiType: NumberUiType.Spinner, uiType: NumberUiType.Spinner,
}) })
@@ -236,16 +224,16 @@ export default class SelfServeExample extends SelfServeBaseClass {
*/ */
@OnChange(onEnableDbLevelThroughputChange) @OnChange(onEnableDbLevelThroughputChange)
@Values({ @Values({
labelTKey: "Enable DB level throughput", label: "Enable DB level throughput",
trueLabelTKey: "Enable", trueLabel: "Enable",
falseLabelTKey: "Disable", falseLabel: "Disable",
}) })
enableDbLevelThroughput: boolean; enableDbLevelThroughput: boolean;
@Values({ @Values({
labelTKey: "Database Throughput", label: "Database Throughput",
min: getMinDatabaseThroughput, min: 400,
max: getMaxDatabaseThroughput, max: getMaxThroughput,
step: 100, step: 100,
uiType: NumberUiType.Slider, uiType: NumberUiType.Slider,
}) })

View File

@@ -34,17 +34,17 @@ describe("SelfServeComponent", () => {
root: { root: {
id: "root", id: "root",
info: { info: {
messageTKey: "Start at $24/mo per database", message: "Start at $24/mo per database",
link: { link: {
href: "https://aka.ms/azure-cosmos-db-pricing", href: "https://aka.ms/azure-cosmos-db-pricing",
textTKey: "More Details", text: "More Details",
}, },
}, },
children: [ children: [
{ {
id: "throughput", id: "throughput",
input: { input: {
labelTKey: "Throughput (input)", label: "Throughput (input)",
dataFieldName: "throughput", dataFieldName: "throughput",
type: "number", type: "number",
min: 400, min: 400,
@@ -57,7 +57,7 @@ describe("SelfServeComponent", () => {
{ {
id: "containerId", id: "containerId",
input: { input: {
labelTKey: "Container id", label: "Container id",
dataFieldName: "containerId", dataFieldName: "containerId",
type: "string", type: "string",
}, },
@@ -65,9 +65,9 @@ describe("SelfServeComponent", () => {
{ {
id: "analyticalStore", id: "analyticalStore",
input: { input: {
labelTKey: "Analytical Store", label: "Analytical Store",
trueLabelTKey: "Enabled", trueLabel: "Enabled",
falseLabelTKey: "Disabled", falseLabel: "Disabled",
defaultValue: true, defaultValue: true,
dataFieldName: "analyticalStore", dataFieldName: "analyticalStore",
type: "boolean", type: "boolean",
@@ -76,7 +76,7 @@ describe("SelfServeComponent", () => {
{ {
id: "database", id: "database",
input: { input: {
labelTKey: "Database", label: "Database",
dataFieldName: "database", dataFieldName: "database",
type: "object", type: "object",
choices: [ choices: [

View File

@@ -26,9 +26,6 @@ import {
} from "./SelfServeTypes"; } from "./SelfServeTypes";
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent"; import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
import { getMessageBarType } from "./SelfServeUtils"; import { getMessageBarType } from "./SelfServeUtils";
import { Translation } from "react-i18next";
import { TFunction } from "i18next";
import "../i18n";
export interface SelfServeComponentProps { export interface SelfServeComponentProps {
descriptor: SelfServeDescriptor; descriptor: SelfServeDescriptor;
@@ -46,8 +43,6 @@ export interface SelfServeComponentState {
} }
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> { export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
private smartUiGeneratorClassName: string;
componentDidMount(): void { componentDidMount(): void {
this.performRefresh(); this.performRefresh();
this.initializeSmartUiComponent(); this.initializeSmartUiComponent();
@@ -65,7 +60,6 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
notification: undefined, notification: undefined,
refreshResult: undefined, refreshResult: undefined,
}; };
this.smartUiGeneratorClassName = this.props.descriptor.root.id;
} }
private onError = (hasErrors: boolean): void => { private onError = (hasErrors: boolean): void => {
@@ -153,8 +147,8 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
currentValues: Map<string, SmartUiInput>, currentValues: Map<string, SmartUiInput>,
baselineValues: Map<string, SmartUiInput> baselineValues: Map<string, SmartUiInput>
): Promise<AnyDisplay> => { ): Promise<AnyDisplay> => {
input.labelTKey = await this.getResolvedValue(input.labelTKey); input.label = await this.getResolvedValue(input.label);
input.placeholderTKey = await this.getResolvedValue(input.placeholderTKey); input.placeholder = await this.getResolvedValue(input.placeholder);
switch (input.type) { switch (input.type) {
case "string": { case "string": {
@@ -183,8 +177,8 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
} }
case "boolean": { case "boolean": {
const booleanInput = input as BooleanInput; const booleanInput = input as BooleanInput;
booleanInput.trueLabelTKey = await this.getResolvedValue(booleanInput.trueLabelTKey); booleanInput.trueLabel = await this.getResolvedValue(booleanInput.trueLabel);
booleanInput.falseLabelTKey = await this.getResolvedValue(booleanInput.falseLabelTKey); booleanInput.falseLabel = await this.getResolvedValue(booleanInput.falseLabel);
return booleanInput; return booleanInput;
} }
default: { default: {
@@ -220,7 +214,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
onSavePromise.catch((error) => { onSavePromise.catch((error) => {
this.setState({ this.setState({
notification: { notification: {
message: `${error.message}`, message: `Error: ${error.message}`,
type: SelfServeNotificationType.error, type: SelfServeNotificationType.error,
}, },
}); });
@@ -279,15 +273,11 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
this.setState({ isInitializing: false }); this.setState({ isInitializing: false });
}; };
public getCommonTranslation = (translationFunction: TFunction, key: string): string => { private getCommandBarItems = (): ICommandBarItemProps[] => {
return translationFunction(`Common.${key}`);
};
private getCommandBarItems = (translate: TFunction): ICommandBarItemProps[] => {
return [ return [
{ {
key: "save", key: "save",
text: this.getCommonTranslation(translate, "Save"), text: "Save",
iconProps: { iconName: "Save" }, iconProps: { iconName: "Save" },
split: true, split: true,
disabled: this.isSaveButtonDisabled(), disabled: this.isSaveButtonDisabled(),
@@ -295,7 +285,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
}, },
{ {
key: "discard", key: "discard",
text: this.getCommonTranslation(translate, "Discard"), text: "Discard",
iconProps: { iconName: "Undo" }, iconProps: { iconName: "Undo" },
split: true, split: true,
disabled: this.isDiscardButtonDisabled(), disabled: this.isDiscardButtonDisabled(),
@@ -305,7 +295,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
}, },
{ {
key: "refresh", key: "refresh",
text: this.getCommonTranslation(translate, "Refresh"), text: "Refresh",
disabled: this.state.isInitializing, disabled: this.state.isInitializing,
iconProps: { iconName: "Refresh" }, iconProps: { iconName: "Refresh" },
split: true, split: true,
@@ -316,66 +306,47 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
]; ];
}; };
private getNotificationMessageTranslation = (translationFunction: TFunction, messageKey: string): string => {
const translation = translationFunction(messageKey);
if (translation === `${this.smartUiGeneratorClassName}.${messageKey}`) {
return messageKey;
}
return translation;
};
public render(): JSX.Element { public render(): JSX.Element {
const containerStackTokens: IStackTokens = { childrenGap: 5 }; const containerStackTokens: IStackTokens = { childrenGap: 5 };
if (this.state.compileErrorMessage) { if (this.state.compileErrorMessage) {
return <MessageBar messageBarType={MessageBarType.error}>{this.state.compileErrorMessage}</MessageBar>; return <MessageBar messageBarType={MessageBarType.error}>{this.state.compileErrorMessage}</MessageBar>;
} }
return ( return (
<Translation> <div style={{ overflowX: "auto" }}>
{(translate) => { <Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}>
const getTranslation = (key: string): string => { <CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems()} />
return translate(`${this.smartUiGeneratorClassName}.${key}`); {this.state.isInitializing ? (
}; <Spinner
size={SpinnerSize.large}
return ( styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }}
<div style={{ overflowX: "auto" }}> />
<Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}> ) : (
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems(translate)} /> <>
{this.state.isInitializing ? ( {this.state.refreshResult?.isUpdateInProgress && (
<Spinner <MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}>
size={SpinnerSize.large} {this.state.refreshResult.notificationMessage}
styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }} </MessageBar>
/> )}
) : ( {this.state.notification && (
<> <MessageBar
{this.state.refreshResult?.isUpdateInProgress && ( messageBarType={getMessageBarType(this.state.notification.type)}
<MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}> styles={{ root: { width: 400 } }}
{getTranslation(this.state.refreshResult.notificationMessage)} onDismiss={() => this.setState({ notification: undefined })}
</MessageBar> >
)} {this.state.notification.message}
{this.state.notification && ( </MessageBar>
<MessageBar )}
messageBarType={getMessageBarType(this.state.notification.type)} <SmartUiComponent
styles={{ root: { width: 400 } }} disabled={this.state.refreshResult?.isUpdateInProgress}
onDismiss={() => this.setState({ notification: undefined })} descriptor={this.state.root as SmartUiDescriptor}
> currentValues={this.state.currentValues}
{this.getNotificationMessageTranslation(getTranslation, this.state.notification.message)} onInputChange={this.onInputChange}
</MessageBar> onError={this.onError}
)} />
<SmartUiComponent </>
disabled={this.state.refreshResult?.isUpdateInProgress} )}
descriptor={this.state.root as SmartUiDescriptor} </Stack>
currentValues={this.state.currentValues} </div>
onInputChange={this.onInputChange}
onError={this.onError}
getTranslation={getTranslation}
/>
</>
)}
</Stack>
</div>
);
}}
</Translation>
); );
} }
} }

View File

@@ -2,9 +2,9 @@ interface BaseInput {
dataFieldName: string; dataFieldName: string;
errorMessage?: string; errorMessage?: string;
type: InputTypeValue; type: InputTypeValue;
labelTKey?: (() => Promise<string>) | string; label?: (() => Promise<string>) | string;
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>; onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>;
placeholderTKey?: (() => Promise<string>) | string; placeholder?: (() => Promise<string>) | string;
} }
export interface NumberInput extends BaseInput { export interface NumberInput extends BaseInput {
@@ -16,8 +16,8 @@ export interface NumberInput extends BaseInput {
} }
export interface BooleanInput extends BaseInput { export interface BooleanInput extends BaseInput {
trueLabelTKey: (() => Promise<string>) | string; trueLabel: (() => Promise<string>) | string;
falseLabelTKey: (() => Promise<string>) | string; falseLabel: (() => Promise<string>) | string;
defaultValue?: boolean; defaultValue?: boolean;
} }
@@ -92,18 +92,18 @@ export type ChoiceItem = { label: string; key: string };
export type InputType = number | string | boolean | ChoiceItem; export type InputType = number | string | boolean | ChoiceItem;
export interface Info { export interface Info {
messageTKey: string; message: string;
link?: { link?: {
href: string; href: string;
textTKey: string; text: string;
}; };
} }
export interface Description { export interface Description {
textTKey: string; text: string;
link?: { link?: {
href: string; href: string;
textTKey: string; text: string;
}; };
} }

View File

@@ -58,7 +58,7 @@ describe("SelfServeUtils", () => {
id: "dbThroughput", id: "dbThroughput",
dataFieldName: "dbThroughput", dataFieldName: "dbThroughput",
type: "number", type: "number",
labelTKey: "Database Throughput", label: "Database Throughput",
min: 1, min: 1,
max: 5, max: 5,
step: 1, step: 1,
@@ -71,7 +71,7 @@ describe("SelfServeUtils", () => {
id: "collThroughput", id: "collThroughput",
dataFieldName: "collThroughput", dataFieldName: "collThroughput",
type: "number", type: "number",
labelTKey: "Coll Throughput", label: "Coll Throughput",
min: 1, min: 1,
max: 5, max: 5,
step: 1, step: 1,
@@ -84,7 +84,7 @@ describe("SelfServeUtils", () => {
id: "invalidThroughput", id: "invalidThroughput",
dataFieldName: "invalidThroughput", dataFieldName: "invalidThroughput",
type: "boolean", type: "boolean",
labelTKey: "Invalid Coll Throughput", label: "Invalid Coll Throughput",
min: 1, min: 1,
max: 5, max: 5,
step: 1, step: 1,
@@ -98,8 +98,8 @@ describe("SelfServeUtils", () => {
id: "collName", id: "collName",
dataFieldName: "collName", dataFieldName: "collName",
type: "string", type: "string",
labelTKey: "Coll Name", label: "Coll Name",
placeholderTKey: "placeholder text", placeholder: "placeholder text",
}, },
], ],
[ [
@@ -108,9 +108,9 @@ describe("SelfServeUtils", () => {
id: "enableLogging", id: "enableLogging",
dataFieldName: "enableLogging", dataFieldName: "enableLogging",
type: "boolean", type: "boolean",
labelTKey: "Enable Logging", label: "Enable Logging",
trueLabelTKey: "Enable", trueLabel: "Enable",
falseLabelTKey: "Disable", falseLabel: "Disable",
}, },
], ],
[ [
@@ -119,8 +119,8 @@ describe("SelfServeUtils", () => {
id: "invalidEnableLogging", id: "invalidEnableLogging",
dataFieldName: "invalidEnableLogging", dataFieldName: "invalidEnableLogging",
type: "boolean", type: "boolean",
labelTKey: "Invalid Enable Logging", label: "Invalid Enable Logging",
placeholderTKey: "placeholder text", placeholder: "placeholder text",
}, },
], ],
[ [
@@ -129,7 +129,7 @@ describe("SelfServeUtils", () => {
id: "regions", id: "regions",
dataFieldName: "regions", dataFieldName: "regions",
type: "object", type: "object",
labelTKey: "Regions", label: "Regions",
choices: [ choices: [
{ label: "South West US", key: "SWUS" }, { label: "South West US", key: "SWUS" },
{ label: "North Central US", key: "NCUS" }, { label: "North Central US", key: "NCUS" },
@@ -143,14 +143,14 @@ describe("SelfServeUtils", () => {
id: "invalidRegions", id: "invalidRegions",
dataFieldName: "invalidRegions", dataFieldName: "invalidRegions",
type: "object", type: "object",
labelTKey: "Invalid Regions", label: "Invalid Regions",
placeholderTKey: "placeholder text", placeholder: "placeholder text",
}, },
], ],
]); ]);
const expectedDescriptor = { const expectedDescriptor = {
root: { root: {
id: "TestClass", id: "root",
children: [ children: [
{ {
id: "dbThroughput", id: "dbThroughput",
@@ -158,7 +158,7 @@ describe("SelfServeUtils", () => {
id: "dbThroughput", id: "dbThroughput",
dataFieldName: "dbThroughput", dataFieldName: "dbThroughput",
type: "number", type: "number",
labelTKey: "Database Throughput", label: "Database Throughput",
min: 1, min: 1,
max: 5, max: 5,
step: 1, step: 1,
@@ -172,7 +172,7 @@ describe("SelfServeUtils", () => {
id: "collThroughput", id: "collThroughput",
dataFieldName: "collThroughput", dataFieldName: "collThroughput",
type: "number", type: "number",
labelTKey: "Coll Throughput", label: "Coll Throughput",
min: 1, min: 1,
max: 5, max: 5,
step: 1, step: 1,
@@ -186,7 +186,7 @@ describe("SelfServeUtils", () => {
id: "invalidThroughput", id: "invalidThroughput",
dataFieldName: "invalidThroughput", dataFieldName: "invalidThroughput",
type: "boolean", type: "boolean",
labelTKey: "Invalid Coll Throughput", label: "Invalid Coll Throughput",
min: 1, min: 1,
max: 5, max: 5,
step: 1, step: 1,
@@ -201,8 +201,8 @@ describe("SelfServeUtils", () => {
id: "collName", id: "collName",
dataFieldName: "collName", dataFieldName: "collName",
type: "string", type: "string",
labelTKey: "Coll Name", label: "Coll Name",
placeholderTKey: "placeholder text", placeholder: "placeholder text",
}, },
children: [] as Node[], children: [] as Node[],
}, },
@@ -212,9 +212,9 @@ describe("SelfServeUtils", () => {
id: "enableLogging", id: "enableLogging",
dataFieldName: "enableLogging", dataFieldName: "enableLogging",
type: "boolean", type: "boolean",
labelTKey: "Enable Logging", label: "Enable Logging",
trueLabelTKey: "Enable", trueLabel: "Enable",
falseLabelTKey: "Disable", falseLabel: "Disable",
}, },
children: [] as Node[], children: [] as Node[],
}, },
@@ -224,8 +224,8 @@ describe("SelfServeUtils", () => {
id: "invalidEnableLogging", id: "invalidEnableLogging",
dataFieldName: "invalidEnableLogging", dataFieldName: "invalidEnableLogging",
type: "boolean", type: "boolean",
labelTKey: "Invalid Enable Logging", label: "Invalid Enable Logging",
placeholderTKey: "placeholder text", placeholder: "placeholder text",
errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidEnableLogging'.", errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidEnableLogging'.",
}, },
children: [] as Node[], children: [] as Node[],
@@ -236,7 +236,7 @@ describe("SelfServeUtils", () => {
id: "regions", id: "regions",
dataFieldName: "regions", dataFieldName: "regions",
type: "object", type: "object",
labelTKey: "Regions", label: "Regions",
choices: [ choices: [
{ label: "South West US", key: "SWUS" }, { label: "South West US", key: "SWUS" },
{ label: "North Central US", key: "NCUS" }, { label: "North Central US", key: "NCUS" },
@@ -251,8 +251,8 @@ describe("SelfServeUtils", () => {
id: "invalidRegions", id: "invalidRegions",
dataFieldName: "invalidRegions", dataFieldName: "invalidRegions",
type: "object", type: "object",
labelTKey: "Invalid Regions", label: "Invalid Regions",
placeholderTKey: "placeholder text", placeholder: "placeholder text",
errorMessage: "label and choices are required for Choice input 'invalidRegions'.", errorMessage: "label and choices are required for Choice input 'invalidRegions'.",
}, },
children: [] as Node[], children: [] as Node[],
@@ -270,7 +270,7 @@ describe("SelfServeUtils", () => {
"invalidRegions", "invalidRegions",
], ],
}; };
const descriptor = mapToSmartUiDescriptor("TestClass", context); const descriptor = mapToSmartUiDescriptor(context);
expect(descriptor).toEqual(expectedDescriptor); expect(descriptor).toEqual(expectedDescriptor);
}); });
}); });

View File

@@ -32,14 +32,14 @@ export interface DecoratorProperties {
id: string; id: string;
info?: (() => Promise<Info>) | Info; info?: (() => Promise<Info>) | Info;
type?: InputTypeValue; type?: InputTypeValue;
labelTKey?: (() => Promise<string>) | string; label?: (() => Promise<string>) | string;
placeholderTKey?: (() => Promise<string>) | string; placeholder?: (() => Promise<string>) | string;
dataFieldName?: string; dataFieldName?: string;
min?: (() => Promise<number>) | number; min?: (() => Promise<number>) | number;
max?: (() => Promise<number>) | number; max?: (() => Promise<number>) | number;
step?: (() => Promise<number>) | number; step?: (() => Promise<number>) | number;
trueLabelTKey?: (() => Promise<string>) | string; trueLabel?: (() => Promise<string>) | string;
falseLabelTKey?: (() => Promise<string>) | string; falseLabel?: (() => Promise<string>) | string;
choices?: (() => Promise<ChoiceItem[]>) | ChoiceItem[]; choices?: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
uiType?: string; uiType?: string;
errorMessage?: string; errorMessage?: string;
@@ -100,21 +100,18 @@ export const updateContextWithDecorator = <T extends keyof DecoratorProperties,
export const buildSmartUiDescriptor = (className: string, target: unknown): void => { export const buildSmartUiDescriptor = (className: string, target: unknown): void => {
const context = Reflect.getMetadata(className, target) as Map<string, DecoratorProperties>; const context = Reflect.getMetadata(className, target) as Map<string, DecoratorProperties>;
const smartUiDescriptor = mapToSmartUiDescriptor(className, context); const smartUiDescriptor = mapToSmartUiDescriptor(context);
Reflect.defineMetadata(className, smartUiDescriptor, target); Reflect.defineMetadata(className, smartUiDescriptor, target);
}; };
export const mapToSmartUiDescriptor = ( export const mapToSmartUiDescriptor = (context: Map<string, DecoratorProperties>): SelfServeDescriptor => {
className: string,
context: Map<string, DecoratorProperties>
): SelfServeDescriptor => {
const root = context.get("root"); const root = context.get("root");
context.delete("root"); context.delete("root");
const inputNames: string[] = []; const inputNames: string[] = [];
const smartUiDescriptor: SelfServeDescriptor = { const smartUiDescriptor: SelfServeDescriptor = {
root: { root: {
id: className, id: "root",
info: root?.info, info: root?.info,
children: [], children: [],
}, },
@@ -150,7 +147,7 @@ const addToDescriptor = (
const getInput = (value: DecoratorProperties): AnyDisplay => { const getInput = (value: DecoratorProperties): AnyDisplay => {
switch (value.type) { switch (value.type) {
case "number": case "number":
if (!value.labelTKey || !value.step || !value.uiType || !value.min || !value.max) { if (!value.label || !value.step || !value.uiType || !value.min || !value.max) {
value.errorMessage = `label, step, min, max and uiType are required for number input '${value.id}'.`; value.errorMessage = `label, step, min, max and uiType are required for number input '${value.id}'.`;
} }
return value as NumberInput; return value as NumberInput;
@@ -158,17 +155,17 @@ const getInput = (value: DecoratorProperties): AnyDisplay => {
if (value.description) { if (value.description) {
return value as DescriptionDisplay; return value as DescriptionDisplay;
} }
if (!value.labelTKey) { if (!value.label) {
value.errorMessage = `label is required for string input '${value.id}'.`; value.errorMessage = `label is required for string input '${value.id}'.`;
} }
return value as StringInput; return value as StringInput;
case "boolean": case "boolean":
if (!value.labelTKey || !value.trueLabelTKey || !value.falseLabelTKey) { if (!value.label || !value.trueLabel || !value.falseLabel) {
value.errorMessage = `label, truelabel and falselabel are required for boolean input '${value.id}'.`; value.errorMessage = `label, truelabel and falselabel are required for boolean input '${value.id}'.`;
} }
return value as BooleanInput; return value as BooleanInput;
default: default:
if (!value.labelTKey || !value.choices) { if (!value.label || !value.choices) {
value.errorMessage = `label and choices are required for Choice input '${value.id}'.`; value.errorMessage = `label and choices are required for Choice input '${value.id}'.`;
} }
return value as ChoiceInput; return value as ChoiceInput;

View File

@@ -62,10 +62,10 @@ export default class SqlX extends SelfServeBaseClass {
@Values({ @Values({
description: { description: {
textTKey: "Provisioning dedicated gateways for SqlX accounts.", text: "Provisioning dedicated gateways for SqlX accounts.",
link: { link: {
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
textTKey: "Learn more about dedicated gateway.", text: "Learn more about dedicated gateway.",
}, },
}, },
}) })
@@ -73,21 +73,21 @@ export default class SqlX extends SelfServeBaseClass {
@OnChange(onEnableDedicatedGatewayChange) @OnChange(onEnableDedicatedGatewayChange)
@Values({ @Values({
labelTKey: "Dedicated Gateway", label: "Dedicated Gateway",
trueLabelTKey: "Enable", trueLabel: "Enable",
falseLabelTKey: "Disable", falseLabel: "Disable",
}) })
enableDedicatedGateway: boolean; enableDedicatedGateway: boolean;
@Values({ @Values({
labelTKey: "SKUs", label: "SKUs",
choices: getSkus, choices: getSkus,
placeholderTKey: "Select SKUs", placeholder: "Select SKUs",
}) })
sku: ChoiceItem; sku: ChoiceItem;
@Values({ @Values({
labelTKey: "Number of instances", label: "Number of instances",
min: getInstancesMin, min: getInstancesMin,
max: getInstancesMax, max: getInstancesMax,
step: 1, step: 1,

View File

@@ -1,21 +1,686 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelfServeComponent message bar and spinner snapshots 1`] = ` exports[`SelfServeComponent message bar and spinner snapshots 1`] = `
<Translation> <div
<Component /> style={
</Translation> Object {
"overflowX": "auto",
}
}
>
<Stack
styles={
Object {
"root": Object {
"padding": 10,
},
}
}
tokens={
Object {
"childrenGap": 5,
}
}
>
<StyledCommandBarBase
items={
Array [
Object {
"disabled": true,
"iconProps": Object {
"iconName": "Save",
},
"key": "save",
"onClick": [Function],
"split": true,
"text": "Save",
},
Object {
"disabled": true,
"iconProps": Object {
"iconName": "Undo",
},
"key": "discard",
"onClick": [Function],
"split": true,
"text": "Discard",
},
Object {
"disabled": false,
"iconProps": Object {
"iconName": "Refresh",
},
"key": "refresh",
"onClick": [Function],
"split": true,
"text": "Refresh",
},
]
}
styles={
Object {
"root": Object {
"paddingLeft": 0,
},
}
}
/>
<StyledMessageBarBase
messageBarType={0}
styles={
Object {
"root": Object {
"width": 400,
},
}
}
>
refresh performed successfully
</StyledMessageBarBase>
<StyledMessageBarBase
messageBarType={0}
onDismiss={[Function]}
styles={
Object {
"root": Object {
"width": 400,
},
}
}
>
submitted successfully
</StyledMessageBarBase>
<SmartUiComponent
currentValues={
Map {
"throughput" => Object {
"value": 450,
},
"analyticalStore" => Object {
"value": false,
},
"database" => Object {
"value": "db2",
},
}
}
descriptor={
Object {
"initialize": [MockFunction] {
"calls": Array [
Array [],
Array [],
Array [],
Array [],
Array [],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
],
},
"inputNames": Array [
"throughput",
"analyticalStore",
"database",
],
"onRefresh": [MockFunction] {
"calls": Array [
Array [],
Array [],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
],
},
"onSave": [MockFunction] {
"calls": Array [
Array [
Map {
"throughput" => Object {
"value": 450,
},
"analyticalStore" => Object {
"value": false,
},
"database" => Object {
"value": "db2",
},
},
],
Array [
Map {
"throughput" => Object {
"value": 450,
},
"analyticalStore" => Object {
"value": false,
},
"database" => Object {
"value": "db2",
},
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
],
},
"root": Object {
"children": Array [
Object {
"id": "throughput",
"info": undefined,
"input": Object {
"dataFieldName": "throughput",
"defaultValue": 400,
"label": "Throughput (input)",
"max": 500,
"min": 400,
"placeholder": undefined,
"step": 10,
"type": "number",
"uiType": "Spinner",
},
},
Object {
"id": "containerId",
"info": undefined,
"input": Object {
"dataFieldName": "containerId",
"label": "Container id",
"placeholder": undefined,
"type": "string",
},
},
Object {
"id": "analyticalStore",
"info": undefined,
"input": Object {
"dataFieldName": "analyticalStore",
"defaultValue": true,
"falseLabel": "Disabled",
"label": "Analytical Store",
"placeholder": undefined,
"trueLabel": "Enabled",
"type": "boolean",
},
},
Object {
"id": "database",
"info": undefined,
"input": Object {
"choices": Array [
Object {
"key": "db1",
"label": "Database 1",
},
Object {
"key": "db2",
"label": "Database 2",
},
Object {
"key": "db3",
"label": "Database 3",
},
],
"dataFieldName": "database",
"defaultKey": "db2",
"label": "Database",
"placeholder": undefined,
"type": "object",
},
},
],
"id": "root",
"info": Object {
"link": Object {
"href": "https://aka.ms/azure-cosmos-db-pricing",
"text": "More Details",
},
"message": "Start at $24/mo per database",
},
},
}
}
disabled={true}
onError={[Function]}
onInputChange={[Function]}
/>
</Stack>
</div>
`; `;
exports[`SelfServeComponent message bar and spinner snapshots 2`] = ` exports[`SelfServeComponent message bar and spinner snapshots 2`] = `
<Translation> <div
<Component /> style={
</Translation> Object {
"overflowX": "auto",
}
}
>
<Stack
styles={
Object {
"root": Object {
"padding": 10,
},
}
}
tokens={
Object {
"childrenGap": 5,
}
}
>
<StyledCommandBarBase
items={
Array [
Object {
"disabled": true,
"iconProps": Object {
"iconName": "Save",
},
"key": "save",
"onClick": [Function],
"split": true,
"text": "Save",
},
Object {
"disabled": true,
"iconProps": Object {
"iconName": "Undo",
},
"key": "discard",
"onClick": [Function],
"split": true,
"text": "Discard",
},
Object {
"disabled": false,
"iconProps": Object {
"iconName": "Refresh",
},
"key": "refresh",
"onClick": [Function],
"split": true,
"text": "Refresh",
},
]
}
styles={
Object {
"root": Object {
"paddingLeft": 0,
},
}
}
/>
<StyledMessageBarBase
messageBarType={0}
onDismiss={[Function]}
styles={
Object {
"root": Object {
"width": 400,
},
}
}
>
submitted successfully
</StyledMessageBarBase>
<SmartUiComponent
currentValues={
Map {
"throughput" => Object {
"value": 450,
},
"analyticalStore" => Object {
"value": false,
},
"database" => Object {
"value": "db2",
},
}
}
descriptor={
Object {
"initialize": [MockFunction] {
"calls": Array [
Array [],
Array [],
Array [],
Array [],
Array [],
Array [],
Array [],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
],
},
"inputNames": Array [
"throughput",
"analyticalStore",
"database",
],
"onRefresh": [MockFunction] {
"calls": Array [
Array [],
Array [],
Array [],
Array [],
Array [],
Array [],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
],
},
"onSave": [MockFunction] {
"calls": Array [
Array [
Map {
"throughput" => Object {
"value": 450,
},
"analyticalStore" => Object {
"value": false,
},
"database" => Object {
"value": "db2",
},
},
],
Array [
Map {
"throughput" => Object {
"value": 450,
},
"analyticalStore" => Object {
"value": false,
},
"database" => Object {
"value": "db2",
},
},
],
Array [
Map {
"throughput" => Object {
"value": 450,
},
"analyticalStore" => Object {
"value": false,
},
"database" => Object {
"value": "db2",
},
},
],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
Object {
"type": "return",
"value": Promise {},
},
],
},
"root": Object {
"children": Array [
Object {
"id": "throughput",
"info": undefined,
"input": Object {
"dataFieldName": "throughput",
"defaultValue": 400,
"label": "Throughput (input)",
"max": 500,
"min": 400,
"placeholder": undefined,
"step": 10,
"type": "number",
"uiType": "Spinner",
},
},
Object {
"id": "containerId",
"info": undefined,
"input": Object {
"dataFieldName": "containerId",
"label": "Container id",
"placeholder": undefined,
"type": "string",
},
},
Object {
"id": "analyticalStore",
"info": undefined,
"input": Object {
"dataFieldName": "analyticalStore",
"defaultValue": true,
"falseLabel": "Disabled",
"label": "Analytical Store",
"placeholder": undefined,
"trueLabel": "Enabled",
"type": "boolean",
},
},
Object {
"id": "database",
"info": undefined,
"input": Object {
"choices": Array [
Object {
"key": "db1",
"label": "Database 1",
},
Object {
"key": "db2",
"label": "Database 2",
},
Object {
"key": "db3",
"label": "Database 3",
},
],
"dataFieldName": "database",
"defaultKey": "db2",
"label": "Database",
"placeholder": undefined,
"type": "object",
},
},
],
"id": "root",
"info": Object {
"link": Object {
"href": "https://aka.ms/azure-cosmos-db-pricing",
"text": "More Details",
},
"message": "Start at $24/mo per database",
},
},
}
}
disabled={false}
onError={[Function]}
onInputChange={[Function]}
/>
</Stack>
</div>
`; `;
exports[`SelfServeComponent message bar and spinner snapshots 3`] = ` exports[`SelfServeComponent message bar and spinner snapshots 3`] = `
<Translation> <div
<Component /> style={
</Translation> Object {
"overflowX": "auto",
}
}
>
<Stack
styles={
Object {
"root": Object {
"padding": 10,
},
}
}
tokens={
Object {
"childrenGap": 5,
}
}
>
<StyledCommandBarBase
items={
Array [
Object {
"disabled": true,
"iconProps": Object {
"iconName": "Save",
},
"key": "save",
"onClick": [Function],
"split": true,
"text": "Save",
},
Object {
"disabled": true,
"iconProps": Object {
"iconName": "Undo",
},
"key": "discard",
"onClick": [Function],
"split": true,
"text": "Discard",
},
Object {
"disabled": true,
"iconProps": Object {
"iconName": "Refresh",
},
"key": "refresh",
"onClick": [Function],
"split": true,
"text": "Refresh",
},
]
}
styles={
Object {
"root": Object {
"paddingLeft": 0,
},
}
}
/>
<StyledSpinnerBase
size={3}
styles={
Object {
"root": Object {
"height": "100%",
"justifyContent": "center",
"textAlign": "center",
"width": "100%",
},
}
}
/>
</Stack>
</div>
`; `;
exports[`SelfServeComponent message bar and spinner snapshots 4`] = ` exports[`SelfServeComponent message bar and spinner snapshots 4`] = `
@@ -27,7 +692,195 @@ exports[`SelfServeComponent message bar and spinner snapshots 4`] = `
`; `;
exports[`SelfServeComponent should render and honor save, discard, refresh actions 1`] = ` exports[`SelfServeComponent should render and honor save, discard, refresh actions 1`] = `
<Translation> <div
<Component /> style={
</Translation> Object {
"overflowX": "auto",
}
}
>
<Stack
styles={
Object {
"root": Object {
"padding": 10,
},
}
}
tokens={
Object {
"childrenGap": 5,
}
}
>
<StyledCommandBarBase
items={
Array [
Object {
"disabled": true,
"iconProps": Object {
"iconName": "Save",
},
"key": "save",
"onClick": [Function],
"split": true,
"text": "Save",
},
Object {
"disabled": true,
"iconProps": Object {
"iconName": "Undo",
},
"key": "discard",
"onClick": [Function],
"split": true,
"text": "Discard",
},
Object {
"disabled": false,
"iconProps": Object {
"iconName": "Refresh",
},
"key": "refresh",
"onClick": [Function],
"split": true,
"text": "Refresh",
},
]
}
styles={
Object {
"root": Object {
"paddingLeft": 0,
},
}
}
/>
<SmartUiComponent
currentValues={
Map {
"throughput" => Object {
"value": 450,
},
"analyticalStore" => Object {
"value": false,
},
"database" => Object {
"value": "db2",
},
}
}
descriptor={
Object {
"initialize": [MockFunction] {
"calls": Array [
Array [],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
},
"inputNames": Array [
"throughput",
"analyticalStore",
"database",
],
"onRefresh": [MockFunction] {
"calls": Array [
Array [],
],
"results": Array [
Object {
"type": "return",
"value": Promise {},
},
],
},
"onSave": [MockFunction],
"root": Object {
"children": Array [
Object {
"id": "throughput",
"info": undefined,
"input": Object {
"dataFieldName": "throughput",
"defaultValue": 400,
"label": "Throughput (input)",
"max": 500,
"min": 400,
"placeholder": undefined,
"step": 10,
"type": "number",
"uiType": "Spinner",
},
},
Object {
"id": "containerId",
"info": undefined,
"input": Object {
"dataFieldName": "containerId",
"label": "Container id",
"placeholder": undefined,
"type": "string",
},
},
Object {
"id": "analyticalStore",
"info": undefined,
"input": Object {
"dataFieldName": "analyticalStore",
"defaultValue": true,
"falseLabel": "Disabled",
"label": "Analytical Store",
"placeholder": undefined,
"trueLabel": "Enabled",
"type": "boolean",
},
},
Object {
"id": "database",
"info": undefined,
"input": Object {
"choices": Array [
Object {
"key": "db1",
"label": "Database 1",
},
Object {
"key": "db2",
"label": "Database 2",
},
Object {
"key": "db3",
"label": "Database 3",
},
],
"dataFieldName": "database",
"defaultKey": "db2",
"label": "Database",
"placeholder": undefined,
"type": "object",
},
},
],
"id": "root",
"info": Object {
"link": Object {
"href": "https://aka.ms/azure-cosmos-db-pricing",
"text": "More Details",
},
"message": "Start at $24/mo per database",
},
},
}
}
disabled={false}
onError={[Function]}
onInputChange={[Function]}
/>
</Stack>
</div>
`; `;

View File

@@ -41,13 +41,12 @@ export function useKnockoutExplorer(config: ConfigContext, explorerParams: Explo
if (config) { if (config) {
if (config.platform === Platform.Hosted) { if (config.platform === Platform.Hosted) {
await configureHosted(config); await configureHosted(config);
applyExplorerBindings(explorer);
} else if (config.platform === Platform.Emulator) { } else if (config.platform === Platform.Emulator) {
configureEmulator(); configureEmulator();
applyExplorerBindings(explorer);
} else if (config.platform === Platform.Portal) { } else if (config.platform === Platform.Portal) {
configurePortal(); configurePortal();
} }
applyExplorerBindings(explorer);
} }
}; };
effect(); effect();
@@ -238,14 +237,13 @@ function configurePortal() {
); );
console.dir(message); console.dir(message);
explorer.configure(message); explorer.configure(message);
applyExplorerBindings(explorer);
} }
} }
// In the Portal, configuration of Explorer happens via iframe message // In the Portal, configuration of Explorer happens via iframe message
window.addEventListener( window.addEventListener(
"message", "message",
(event) => { (event) => {
console.dir(event);
if (isInvalidParentFrameOrigin(event)) { if (isInvalidParentFrameOrigin(event)) {
return; return;
} }
@@ -267,7 +265,6 @@ function configurePortal() {
} }
explorer.configure(inputs); explorer.configure(inputs);
applyExplorerBindings(explorer);
} }
}, },
false false

View File

@@ -1,31 +0,0 @@
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
import XHR from "i18next-http-backend";
import EnglishTranslations from "./Localization/en/translations.json";
i18n
.use(XHR)
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
en: EnglishTranslations,
},
fallbackLng: "en",
detection: { order: ["navigator", "cookie", "localStorage", "sessionStorage", "querystring", "htmlTag"] },
debug: process.env.NODE_ENV === "development",
ns: ["translations"],
defaultNS: "translations",
keySeparator: ".",
interpolation: {
formatSeparator: ",",
},
react: {
wait: true,
bindI18n: "languageChanged loaded",
bindI18nStore: "added removed",
nsMode: "default",
useSuspense: false,
},
});

View File

@@ -37,7 +37,6 @@ export const uploadNotebookIfNotExist = async (frame: Frame, notebookName: strin
export const getNotebookNode = async (frame: Frame, uploadNotebookName: string): Promise<ElementHandle<Element>> => { export const getNotebookNode = async (frame: Frame, uploadNotebookName: string): Promise<ElementHandle<Element>> => {
const notebookResourceTree = await frame.waitForSelector(".notebookResourceTree"); const notebookResourceTree = await frame.waitForSelector(".notebookResourceTree");
await frame.waitFor(RENDER_DELAY);
let currentNotebookNode: ElementHandle<Element>; let currentNotebookNode: ElementHandle<Element>;
const treeNodeHeaders = await notebookResourceTree.$$(".treeNodeHeader"); const treeNodeHeaders = await notebookResourceTree.$$(".treeNodeHeader");

View File

@@ -1,51 +0,0 @@
const { CosmosClient } = require("@azure/cosmos");
// TODO: Add support for other API connection strings
const mongoRegex = RegExp("mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com");
const connectionString = process.env.PORTAL_RUNNER_CONNECTION_STRING;
async function cleanup() {
if (!connectionString) {
throw new Error("Connection string not provided");
}
let client;
switch (true) {
case connectionString.includes("mongodb://"): {
const [, key, accountName] = connectionString.match(mongoRegex);
client = new CosmosClient({
key,
endpoint: `https://${accountName}.documents.azure.com:443/`,
});
break;
}
// TODO: Add support for other API connection strings
default:
client = new CosmosClient(connectionString);
break;
}
const response = await client.databases.readAll().fetchAll();
return Promise.all(
response.resources.map(async (db) => {
const dbTimestamp = new Date(db._ts * 1000);
const twentyMinutesAgo = new Date(Date.now() - 1000 * 60 * 20);
if (dbTimestamp < twentyMinutesAgo) {
await client.database(db.id).delete();
console.log(`DELETED: ${db.id} | Timestamp: ${dbTimestamp}`);
} else {
console.log(`SKIPPED: ${db.id} | Timestamp: ${dbTimestamp}`);
}
})
);
}
cleanup()
.then(() => {
process.exit(0);
})
.catch((error) => {
console.error(error);
process.exit(1);
});