Merge branch 'master' into v-yiqcao/nullCheck

This commit is contained in:
Chris-MS-896 2021-01-28 17:59:16 -06:00 committed by GitHub
commit b000115afe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 2590 additions and 3389 deletions

View File

@ -156,6 +156,7 @@ 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

4213
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.0-rc.2", "@jupyterlab/services": "6.0.2",
"@jupyterlab/terminal": "3.0.0-rc.2", "@jupyterlab/terminal": "3.0.3",
"@microsoft/applicationinsights-web": "2.5.9", "@microsoft/applicationinsights-web": "2.5.9",
"@nteract/commutable": "7.3.2", "@nteract/commutable": "7.4.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,6 +64,9 @@
"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",
@ -86,6 +89,7 @@
"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",
@ -124,7 +128,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": "16.9.56", "@types/react": "17.0.0",
"@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,7 +5,6 @@ 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";
@ -109,7 +108,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(): Q.Promise<any>; expandCollection(): void;
collapseCollection(): void; collapseCollection(): void;
getDatabase(): Database; getDatabase(): Database;
} }
@ -176,7 +175,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): Q.Promise<UploadDetails>; uploadFiles(fileList: FileList): Promise<UploadDetails>;
getLabel(): string; getLabel(): string;
} }
@ -294,7 +293,7 @@ export interface DocumentsTabOptions extends TabOptions {
} }
export interface SettingsTabV2Options extends TabOptions { export interface SettingsTabV2Options extends TabOptions {
getPendingNotification: Q.Promise<DataModels.Notification>; getPendingNotification: Promise<DataModels.Notification>;
} }
export interface ConflictsTabOptions extends TabOptions { export interface ConflictsTabOptions extends TabOptions {

View File

@ -31,7 +31,6 @@ 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),
})); }));
@ -47,9 +46,7 @@ describe("SettingsComponent", () => {
hashLocation: "settings", hashLocation: "settings",
isActive: ko.observable(false), isActive: ko.observable(false),
onUpdateTabsButtons: undefined, onUpdateTabsButtons: undefined,
getPendingNotification: Q.Promise<DataModels.Notification>(() => { getPendingNotification: Promise.resolve(undefined),
return;
}),
}), }),
}; };

View File

@ -8,10 +8,10 @@ describe("SmartUiComponent", () => {
root: { root: {
id: "root", id: "root",
info: { info: {
message: "Start at $24/mo per database", messageTKey: "Start at $24/mo per database",
link: { link: {
href: "https://aka.ms/azure-cosmos-db-pricing", href: "https://aka.ms/azure-cosmos-db-pricing",
text: "More Details", textTKey: "More Details",
}, },
}, },
children: [ children: [
@ -21,10 +21,10 @@ describe("SmartUiComponent", () => {
dataFieldName: "description", dataFieldName: "description",
type: "string", type: "string",
description: { description: {
text: "this is an example description text.", textTKey: "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",
text: "Click here for more information.", textTKey: "Click here for more information.",
}, },
}, },
}, },
@ -32,7 +32,7 @@ describe("SmartUiComponent", () => {
{ {
id: "throughput", id: "throughput",
input: { input: {
label: "Throughput (input)", labelTKey: "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: {
label: "Throughput (Slider)", labelTKey: "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: {
label: "Throughput (invalid)", labelTKey: "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: {
label: "Container id", labelTKey: "Container id",
dataFieldName: "containerId", dataFieldName: "containerId",
type: "string", type: "string",
}, },
@ -80,9 +80,9 @@ describe("SmartUiComponent", () => {
{ {
id: "analyticalStore", id: "analyticalStore",
input: { input: {
label: "Analytical Store", labelTKey: "Analytical Store",
trueLabel: "Enabled", trueLabelTKey: "Enabled",
falseLabel: "Disabled", falseLabelTKey: "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: {
label: "Database", labelTKey: "Database",
dataFieldName: "database", dataFieldName: "database",
type: "object", type: "object",
choices: [ choices: [
@ -117,6 +117,9 @@ 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));
@ -145,6 +148,9 @@ 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,6 +18,7 @@ import {
NumberUiType, NumberUiType,
SmartUiInput, SmartUiInput,
} from "../../../SelfServe/SelfServeTypes"; } from "../../../SelfServe/SelfServeTypes";
import { TFunction } from "i18next";
/** /**
* Generic UX renderer * Generic UX renderer
@ -34,8 +35,8 @@ interface BaseDisplay {
} }
interface BaseInput extends BaseDisplay { interface BaseInput extends BaseDisplay {
label: string; labelTKey: string;
placeholder?: string; placeholderTKey?: string;
errorMessage?: string; errorMessage?: string;
} }
@ -51,8 +52,8 @@ interface NumberInput extends BaseInput {
} }
interface BooleanInput extends BaseInput { interface BooleanInput extends BaseInput {
trueLabel: string; trueLabelTKey: string;
falseLabel: string; falseLabelTKey: string;
defaultValue?: boolean; defaultValue?: boolean;
} }
@ -89,6 +90,7 @@ 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 {
@ -122,10 +124,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 } }}>
{info.message} {this.props.getTranslation(info.messageTKey)}
{info.link && ( {info.link && (
<Link href={info.link.href} target="_blank"> <Link href={info.link.href} target="_blank">
{info.link.text} {this.props.getTranslation(info.link.textTKey)}
</Link> </Link>
)} )}
</MessageBar> </MessageBar>
@ -139,10 +141,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={input.label} label={this.props.getTranslation(input.labelTKey)}
type="text" type="text"
value={value || ""} value={value || ""}
placeholder={input.placeholder} placeholder={this.props.getTranslation(input.placeholderTKey)}
disabled={disabled} disabled={disabled}
onChange={(_, newValue) => this.props.onInputChange(input, newValue)} onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
styles={{ styles={{
@ -165,10 +167,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`}>
{input.description.text}{" "} {this.props.getTranslation(input.description.textTKey)}{" "}
{description.link && ( {description.link && (
<Link target="_blank" href={input.description.link.href}> <Link target="_blank" href={input.description.link.href}>
{input.description.link.text} {this.props.getTranslation(input.description.link.textTKey)}
</Link> </Link>
)} )}
</Text> </Text>
@ -219,12 +221,12 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
}; };
private renderNumberInput(input: NumberInput): JSX.Element { private renderNumberInput(input: NumberInput): JSX.Element {
const { label, min, max, dataFieldName, step } = input; const { labelTKey, min, max, dataFieldName, step } = input;
const props = { const props = {
label: label, label: this.props.getTranslation(labelTKey),
min: min, min: min,
max: max, max: max,
ariaLabel: label, ariaLabel: labelTKey,
step: step, step: step,
}; };
@ -284,10 +286,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return ( return (
<Toggle <Toggle
id={`${input.dataFieldName}-toggle-input`} id={`${input.dataFieldName}-toggle-input`}
label={input.label} label={this.props.getTranslation(input.labelTKey)}
checked={value || false} checked={value || false}
onText={input.trueLabel} onText={this.props.getTranslation(input.trueLabelTKey)}
offText={input.falseLabel} offText={this.props.getTranslation(input.falseLabelTKey)}
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 } }}
@ -296,7 +298,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
} }
private renderChoiceInput(input: ChoiceInput): JSX.Element { private renderChoiceInput(input: ChoiceInput): JSX.Element {
const { label, defaultKey: defaultKey, dataFieldName, choices, placeholder } = input; const { labelTKey, defaultKey, dataFieldName, choices, placeholderTKey } = 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;
@ -306,14 +308,14 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return ( return (
<Dropdown <Dropdown
id={`${input.dataFieldName}-dropdown-input`} id={`${input.dataFieldName}-dropdown-input`}
label={label} label={this.props.getTranslation(labelTKey)}
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={placeholder} placeholder={this.props.getTranslation(placeholderTKey)}
disabled={disabled} disabled={disabled}
options={choices.map((c) => ({ options={choices.map((c) => ({
key: c.key, key: c.key,
text: c.label, text: this.props.getTranslation(c.label),
}))} }))}
styles={{ styles={{
root: { width: 400 }, root: { width: 400 },

View File

@ -36,7 +36,36 @@ export class CommandBarComponentButtonFactory {
} }
const newCollectionBtn = CommandBarComponentButtonFactory.createNewCollectionGroup(container); const newCollectionBtn = CommandBarComponentButtonFactory.createNewCollectionGroup(container);
const buttons: CommandButtonComponentProps[] = [newCollectionBtn]; const buttons: CommandButtonComponentProps[] = [];
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

@ -45,8 +45,9 @@ type NotebookRendererProps = NotebookRendererBaseProps & NotebookRendererDispatc
const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, children: React.ReactNode) => { const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, children: React.ReactNode) => {
const Cell = () => ( const Cell = () => (
<DraggableCell id={id} contentRef={contentRef}> // TODO Draggable and HijackScroll not working anymore. Fix or remove when reworking MarkdownCell.
<HijackScroll id={id} contentRef={contentRef}> // <DraggableCell id={id} contentRef={contentRef}>
// <HijackScroll id={id} contentRef={contentRef}>
<CellCreator id={id} contentRef={contentRef}> <CellCreator id={id} contentRef={contentRef}>
<CellLabeler id={id} contentRef={contentRef}> <CellLabeler id={id} contentRef={contentRef}>
<HoverableCell id={id} contentRef={contentRef}> <HoverableCell id={id} contentRef={contentRef}>
@ -54,8 +55,8 @@ const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, child
</HoverableCell> </HoverableCell>
</CellLabeler> </CellLabeler>
</CellCreator> </CellCreator>
</HijackScroll> // </HijackScroll>
</DraggableCell> // </DraggableCell>
); );
Cell.defaultProps = { cell_type }; Cell.defaultProps = { cell_type };

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()) { if (this.container.isPreferredApiMongoDB() && this.container.isEnableMongoCapabilityPresent()) {
createMongoWildcardIndex = this.shouldCreateMongoWildcardIndex(); createMongoWildcardIndex = this.shouldCreateMongoWildcardIndex();
} else if (this.showIndexingOptionsForSharedThroughput()) { } else if (this.showIndexingOptionsForSharedThroughput()) {
if (this.useIndexingForSharedThroughput()) { if (this.useIndexingForSharedThroughput()) {

View File

@ -1,6 +1,5 @@
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";
@ -254,9 +253,9 @@ export default class Collection implements ViewModels.Collection {
}); });
} }
public expandCollection(): Q.Promise<any> { public expandCollection(): void {
if (this.isCollectionExpanded()) { if (this.isCollectionExpanded()) {
return Q(); return;
} }
this.isCollectionExpanded(true); this.isCollectionExpanded(true);
@ -268,8 +267,6 @@ 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() {
@ -547,7 +544,7 @@ export default class Collection implements ViewModels.Collection {
}); });
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings"; const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification(); const pendingNotificationsPromise: 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();
}); });
@ -580,7 +577,7 @@ export default class Collection implements ViewModels.Collection {
settingsTabV2: SettingsTabV2, settingsTabV2: SettingsTabV2,
traceStartData: any, traceStartData: any,
settingsTabOptions: ViewModels.TabOptions, settingsTabOptions: ViewModels.TabOptions,
getPendingNotification: Q.Promise<DataModels.Notification> getPendingNotification: Promise<DataModels.Notification>
): void => { ): void => {
const settingsTabV2Options: ViewModels.SettingsTabV2Options = { const settingsTabV2Options: ViewModels.SettingsTabV2Options = {
...settingsTabOptions, ...settingsTabOptions,
@ -980,19 +977,19 @@ export default class Collection implements ViewModels.Collection {
this.container.deleteCollectionConfirmationPane.open(); this.container.deleteCollectionConfirmationPane.open();
} }
public uploadFiles = (fileList: FileList): Q.Promise<UploadDetails> => { public uploadFiles = (fileList: FileList): 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 Q.reject("No files specified"); return Promise.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;
@ -1001,31 +998,26 @@ export default class Collection implements ViewModels.Collection {
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressNotificationId); NotificationConsoleUtils.clearInProgressMessageWithId(inProgressNotificationId);
documentUploader.terminate(); documentUploader.terminate();
if (!!runtimeError) { if (!!runtimeError) {
deferred.reject(runtimeError); reject(runtimeError);
} else if (numSuccessful === 0) { } else if (numSuccessful === 0) {
// all uploads failed // all uploads failed
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleError(`Failed to upload all documents to container ${this.id()}`);
ConsoleDataType.Error,
`Failed to upload all documents to container ${this.id()}`
);
} else if (numFailed > 0) { } else if (numFailed > 0) {
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleError(
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.logConsoleMessage( NotificationConsoleUtils.logConsoleInfo(
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);
deferred.resolve(uploadDetails); resolve(uploadDetails);
}; };
documentUploader.onerror = (event: ErrorEvent): void => { function onerror(reject: (reason: any) => void, event: ErrorEvent) {
documentUploader.terminate(); documentUploader.terminate();
deferred.reject(event.error); reject(event.error);
}; }
const uploaderMessage: StartUploadMessageParams = { const uploaderMessage: StartUploadMessageParams = {
files: fileList, files: fileList,
@ -1040,42 +1032,33 @@ export default class Collection implements ViewModels.Collection {
}, },
}; };
return new Promise<UploadDetails>((resolve, reject) => {
documentUploader.onmessage = onmessage.bind(null, resolve, reject);
documentUploader.onerror = onerror.bind(null, reject);
documentUploader.postMessage(uploaderMessage); documentUploader.postMessage(uploaderMessage);
inProgressNotificationId = NotificationConsoleUtils.logConsoleMessage( inProgressNotificationId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
`Uploading and creating documents in container ${this.id()}` `Uploading and creating documents in container ${this.id()}`
); );
});
return deferred.promise;
}; };
private _uploadFilesCors(files: FileList): Q.Promise<UploadDetails> { private async _uploadFilesCors(files: FileList): Promise<UploadDetails> {
const deferred: Q.Deferred<UploadDetails> = Q.defer<UploadDetails>(); const data = await Promise.all(Array.from(files).map((file) => this._uploadFile(file)));
const promises: Array<Q.Promise<UploadDetailsRecord>> = [];
for (let i = 0; i < files.length; i++) { return { data };
promises.push(this._uploadFile(files[i]));
}
Q.all(promises).then((uploadDetails: Array<UploadDetailsRecord>) => {
deferred.resolve({ data: uploadDetails });
});
return deferred.promise;
} }
private _uploadFile(file: File): Q.Promise<UploadDetailsRecord> { private _uploadFile(file: File): Promise<UploadDetailsRecord> {
const deferred: Q.Deferred<UploadDetailsRecord> = Q.defer();
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (evt: any): void => { const onload = (resolve: (value: UploadDetailsRecord) => void, evt: any): void => {
const fileData: string = evt.target.result; const fileData: string = evt.target.result;
this._createDocumentsFromFile(file.name, fileData).then((record) => { this._createDocumentsFromFile(file.name, fileData).then((record) => resolve(record));
deferred.resolve(record);
});
}; };
reader.onerror = (evt: ProgressEvent): void => { const onerror = (resolve: (value: UploadDetailsRecord) => void, evt: ProgressEvent): void => {
deferred.resolve({ resolve({
fileName: file.name, fileName: file.name,
numSucceeded: 0, numSucceeded: 0,
numFailed: 1, numFailed: 1,
@ -1083,9 +1066,11 @@ export default class Collection implements ViewModels.Collection {
}); });
}; };
return new Promise<UploadDetailsRecord>((resolve) => {
reader.onload = onload.bind(this, resolve);
reader.onerror = onerror.bind(this, resolve);
reader.readAsText(file); reader.readAsText(file);
});
return deferred.promise;
} }
private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> { private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> {
@ -1119,32 +1104,23 @@ export default class Collection implements ViewModels.Collection {
} }
} }
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> { private async _getPendingThroughputSplitNotification(): Promise<DataModels.Notification> {
if (!this.container) { if (!this.container) {
return Q.resolve(undefined); return undefined;
} }
const deferred: Q.Deferred<DataModels.Notification> = Q.defer<DataModels.Notification>(); const throughputUpdateRegExp = new RegExp("Throughput update (.*) in progress");
fetchPortalNotifications().then( try {
(notifications: DataModels.Notification[]) => { const notifications = await fetchPortalNotifications();
if (!notifications || notifications.length === 0) { if (!notifications) {
deferred.resolve(undefined); return undefined;
return;
} }
const pendingNotification = _.find(notifications, (notification: DataModels.Notification) => { return notifications.find(
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress"); ({ kind, collectionName, description = "" }) =>
return ( kind === "message" && collectionName === this.id() && throughputUpdateRegExp.test(description)
notification.kind === "message" &&
notification.collectionName === this.id() &&
notification.description &&
throughputUpdateRegExp.test(notification.description)
); );
}); } catch (error) {
deferred.resolve(pendingNotification);
},
(error: any) => {
Logger.logError( Logger.logError(
JSON.stringify({ JSON.stringify({
error: getErrorMessage(error), error: getErrorMessage(error),
@ -1154,11 +1130,9 @@ export default class Collection implements ViewModels.Collection {
}), }),
"Settings tree node" "Settings tree node"
); );
deferred.resolve(undefined);
} }
);
return deferred.promise; 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(): Q.Promise<void> { public expandCollection(): void {
if (this.isCollectionExpanded()) { if (this.isCollectionExpanded()) {
return Q(); return;
} }
this.isCollectionExpanded(true); this.isCollectionExpanded(true);
@ -55,8 +55,6 @@ 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

@ -248,7 +248,7 @@ describe("Gallery", () => {
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith( expect(window.fetch).toBeCalledWith(
`${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleDatabaseAccount.name}/gallery/${id}/favorite`, `${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleSubscriptionId}/${sampleDatabaseAccount.name}/gallery/${id}/favorite`,
{ {
method: "PATCH", method: "PATCH",
headers: { headers: {

View File

@ -80,7 +80,7 @@ export class JunoClient {
} }
public async getPinnedRepos(scope: string): Promise<IJunoResponse<IPinnedRepo[]>> { public async getPinnedRepos(scope: string): Promise<IJunoResponse<IPinnedRepo[]>> {
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/pinnedrepos`, { const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/github/pinnedrepos`, {
headers: JunoClient.getHeaders(), headers: JunoClient.getHeaders(),
}); });
@ -103,7 +103,7 @@ export class JunoClient {
} }
public async updatePinnedRepos(repos: IPinnedRepo[]): Promise<IJunoResponse<undefined>> { public async updatePinnedRepos(repos: IPinnedRepo[]): Promise<IJunoResponse<undefined>> {
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/pinnedrepos`, { const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/github/pinnedrepos`, {
method: "PUT", method: "PUT",
body: JSON.stringify(repos), body: JSON.stringify(repos),
headers: JunoClient.getHeaders(), headers: JunoClient.getHeaders(),
@ -120,7 +120,7 @@ export class JunoClient {
} }
public async deleteGitHubInfo(): Promise<IJunoResponse<undefined>> { public async deleteGitHubInfo(): Promise<IJunoResponse<undefined>> {
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github`, { const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/github`, {
method: "DELETE", method: "DELETE",
headers: JunoClient.getHeaders(), headers: JunoClient.getHeaders(),
}); });
@ -135,9 +135,12 @@ export class JunoClient {
const githubParams = JunoClient.getGitHubClientParams(); const githubParams = JunoClient.getGitHubClientParams();
githubParams.append("code", code); githubParams.append("code", code);
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/token?${githubParams.toString()}`, { const response = await window.fetch(
`${this.getNotebooksSubscriptionIdAccountUrl()}/github/token?${githubParams.toString()}`,
{
headers: JunoClient.getHeaders(), headers: JunoClient.getHeaders(),
}); }
);
let data: IGitHubOAuthToken; let data: IGitHubOAuthToken;
const body = await response.text(); const body = await response.text();
@ -159,10 +162,13 @@ export class JunoClient {
const githubParams = JunoClient.getGitHubClientParams(); const githubParams = JunoClient.getGitHubClientParams();
githubParams.append("access_token", token); githubParams.append("access_token", token);
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/token?${githubParams.toString()}`, { const response = await window.fetch(
`${this.getNotebooksSubscriptionIdAccountUrl()}/github/token?${githubParams.toString()}`,
{
method: "DELETE", method: "DELETE",
headers: JunoClient.getHeaders(), headers: JunoClient.getHeaders(),
}); }
);
return { return {
status: response.status, status: response.status,
@ -179,7 +185,7 @@ export class JunoClient {
} }
public async getPublicGalleryData(): Promise<IJunoResponse<IPublicGalleryData>> { public async getPublicGalleryData(): Promise<IJunoResponse<IPublicGalleryData>> {
const url = `${this.getNotebooksAccountUrl()}/gallery/public`; const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/public`;
const response = await window.fetch(url, { const response = await window.fetch(url, {
method: "PATCH", method: "PATCH",
headers: JunoClient.getHeaders(), headers: JunoClient.getHeaders(),
@ -197,7 +203,7 @@ export class JunoClient {
} }
public async acceptCodeOfConduct(): Promise<IJunoResponse<boolean>> { public async acceptCodeOfConduct(): Promise<IJunoResponse<boolean>> {
const url = `${this.getNotebooksAccountUrl()}/gallery/acceptCodeOfConduct`; const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/acceptCodeOfConduct`;
const response = await window.fetch(url, { const response = await window.fetch(url, {
method: "PATCH", method: "PATCH",
headers: JunoClient.getHeaders(), headers: JunoClient.getHeaders(),
@ -215,7 +221,7 @@ export class JunoClient {
} }
public async isCodeOfConductAccepted(): Promise<IJunoResponse<boolean>> { public async isCodeOfConductAccepted(): Promise<IJunoResponse<boolean>> {
const url = `${this.getNotebooksAccountUrl()}/gallery/isCodeOfConductAccepted`; const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/isCodeOfConductAccepted`;
const response = await window.fetch(url, { const response = await window.fetch(url, {
method: "PATCH", method: "PATCH",
headers: JunoClient.getHeaders(), headers: JunoClient.getHeaders(),
@ -294,7 +300,7 @@ export class JunoClient {
} }
public async favoriteNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> { public async favoriteNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery/${id}/favorite`, { const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/${id}/favorite`, {
method: "PATCH", method: "PATCH",
headers: JunoClient.getHeaders(), headers: JunoClient.getHeaders(),
}); });
@ -365,9 +371,7 @@ export class JunoClient {
content: string, content: string,
isLinkInjectionEnabled: boolean isLinkInjectionEnabled: boolean
): Promise<IJunoResponse<IGalleryItem>> { ): Promise<IJunoResponse<IGalleryItem>> {
const response = await window.fetch( const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery`, {
`${this.getNotebooksUrl()}/${this.getSubscriptionId()}/${this.getAccount()}/gallery`,
{
method: "PUT", method: "PUT",
headers: JunoClient.getHeaders(), headers: JunoClient.getHeaders(),
body: JSON.stringify({ body: JSON.stringify({
@ -379,8 +383,7 @@ export class JunoClient {
content: JSON.parse(content), content: JSON.parse(content),
addLinkToNotebookViewer: isLinkInjectionEnabled, addLinkToNotebookViewer: isLinkInjectionEnabled,
} as IPublishNotebookRequest), } as IPublishNotebookRequest),
} });
);
let data: IGalleryItem; let data: IGalleryItem;
if (response.status === HttpStatusCodes.OK) { if (response.status === HttpStatusCodes.OK) {
@ -502,6 +505,10 @@ export class JunoClient {
return `${this.getNotebooksUrl()}/${this.getAccount()}`; return `${this.getNotebooksUrl()}/${this.getAccount()}`;
} }
private getNotebooksSubscriptionIdAccountUrl(): string {
return `${this.getNotebooksUrl()}/${this.getSubscriptionId()}/${this.getAccount()}`;
}
private getAnalyticsUrl(): string { private getAnalyticsUrl(): string {
return `${configContext.JUNO_ENDPOINT}/api/analytics`; return `${configContext.JUNO_ENDPOINT}/api/analytics`;
} }

View File

@ -0,0 +1,33 @@
{
"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,12 +288,10 @@ 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 && collection.expandCollection();
collection.expandCollection().then(() => {
const storedProcedure = collection && collection.findStoredProcedureWithId(sprocId); const storedProcedure = collection && collection.findStoredProcedureWithId(sprocId);
storedProcedure && storedProcedure.open(); storedProcedure && storedProcedure.open();
}); });
});
} }
private _openNewTriggerTabForResource(databaseId: string, collectionId: string): void { private _openNewTriggerTabForResource(databaseId: string, collectionId: string): void {
@ -319,12 +317,10 @@ 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 && collection.expandCollection();
collection.expandCollection().then(() => {
const trigger = collection && collection.findTriggerWithId(triggerId); const trigger = collection && collection.findTriggerWithId(triggerId);
trigger && trigger.open(); trigger && trigger.open();
}); });
});
} }
private _openNewUserDefinedFunctionTabForResource(databaseId: string, collectionId: string): void { private _openNewUserDefinedFunctionTabForResource(databaseId: string, collectionId: string): void {
@ -350,12 +346,10 @@ 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 && collection.expandCollection();
collection.expandCollection().then(() => {
const userDefinedFunction = collection && collection.findUserDefinedFunctionWithId(udfId); const userDefinedFunction = collection && collection.findUserDefinedFunctionWithId(udfId);
userDefinedFunction && userDefinedFunction.open(); userDefinedFunction && userDefinedFunction.open();
}); });
});
} }
private _openConflictsTabForResource(databaseId: string, collectionId: string): void { private _openConflictsTabForResource(databaseId: string, collectionId: string): void {

View File

@ -8,7 +8,7 @@ interface Decorator {
} }
interface InputOptionsBase { interface InputOptionsBase {
label: string; labelTKey: 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 {
placeholder?: (() => Promise<string>) | string; placeholderTKey?: (() => Promise<string>) | string;
} }
export interface BooleanInputOptions extends InputOptionsBase { export interface BooleanInputOptions extends InputOptionsBase {
trueLabel: (() => Promise<string>) | string; trueLabelTKey: (() => Promise<string>) | string;
falseLabel: (() => Promise<string>) | string; falseLabelTKey: (() => Promise<string>) | string;
} }
export interface ChoiceInputOptions extends InputOptionsBase { export interface ChoiceInputOptions extends InputOptionsBase {
choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[]; choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
placeholder?: (() => Promise<string>) | string; placeholderTKey?: (() => 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 "trueLabel" in inputOptions; return "trueLabelTKey" 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: "label", value: inputOptions.label }, { name: "labelTKey", value: inputOptions.labelTKey },
{ 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: "label", value: inputOptions.label }, { name: "labelTKey", value: inputOptions.labelTKey },
{ name: "trueLabel", value: inputOptions.trueLabel }, { name: "trueLabelTKey", value: inputOptions.trueLabelTKey },
{ name: "falseLabel", value: inputOptions.falseLabel } { name: "falseLabelTKey", value: inputOptions.falseLabelTKey }
); );
} else if (isChoiceInputOptions(inputOptions)) { } else if (isChoiceInputOptions(inputOptions)) {
return addToMap( return addToMap(
{ name: "label", value: inputOptions.label }, { name: "labelTKey", value: inputOptions.labelTKey },
{ name: "placeholder", value: inputOptions.placeholder }, { name: "placeholderTKey", value: inputOptions.placeholderTKey },
{ 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: "label", value: inputOptions.label }, { name: "labelTKey", value: inputOptions.labelTKey },
{ name: "placeholder", value: inputOptions.placeholder } { name: "placeholderTKey", value: inputOptions.placeholderTKey }
); );
} }
}; };

View File

@ -16,10 +16,22 @@ export interface InitializeResponse {
dbThroughput: number; dbThroughput: number;
} }
export const getMaxThroughput = async (): Promise<number> => { export const getMaxCollectionThroughput = 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,
@ -59,6 +71,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: "Self Serve Example successfully refreshing", notificationMessage: "RefreshMessage",
}; };
}; };

View File

@ -10,7 +10,16 @@ import {
SelfServeNotificationType, SelfServeNotificationType,
SmartUiInput, SmartUiInput,
} from "../SelfServeTypes"; } from "../SelfServeTypes";
import { onRefreshSelfServeExample, getMaxThroughput, Regions, update, initialize } from "./SelfServeExample.rp"; import {
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 },
@ -19,11 +28,11 @@ const regionDropdownItems: ChoiceItem[] = [
]; ];
const selfServeExampleInfo: Info = { const selfServeExampleInfo: Info = {
message: "This is a self serve class", messageTKey: "ClassInfo",
}; };
const regionDropdownInfo: Info = { const regionDropdownInfo: Info = {
message: "More regions can be added in the future.", messageTKey: "RegionDropdownInfo",
}; };
const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: InputType): Map<string, SmartUiInput> => { const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: InputType): Map<string, SmartUiInput> => {
@ -50,7 +59,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("Regions and AccountName should not be empty."); throw new Error("ValidationError");
} }
}; };
@ -66,6 +75,9 @@ 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.
*/ */
/* /*
@ -117,7 +129,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: "submitted successfully", type: SelfServeNotificationType.info }; return { message: "SubmissionMessage", type: SelfServeNotificationType.info };
}; };
/* /*
@ -161,10 +173,10 @@ export default class SelfServeExample extends SelfServeBaseClass {
*/ */
@Values({ @Values({
description: { description: {
text: "This class sets collection and database throughput.", textTKey: "DescriptionText",
link: { link: {
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
text: "Click here for more information", textTKey: "DecriptionLinkText",
}, },
}, },
}) })
@ -193,26 +205,26 @@ export default class SelfServeExample extends SelfServeBaseClass {
any other value of "regions" any other value of "regions"
*/ */
@OnChange(onRegionsChange) @OnChange(onRegionsChange)
@Values({ label: "Regions", choices: regionDropdownItems, placeholder: "Select a region" }) @Values({ labelTKey: "Regions", choices: regionDropdownItems, placeholderTKey: "RegionsPlaceholder" })
regions: ChoiceItem; regions: ChoiceItem;
@Values({ @Values({
label: "Enable Logging", labelTKey: "Enable Logging",
trueLabel: "Enable", trueLabelTKey: "Enable",
falseLabel: "Disable", falseLabelTKey: "Disable",
}) })
enableLogging: boolean; enableLogging: boolean;
@Values({ @Values({
label: "Account Name", labelTKey: "Account Name",
placeholder: "Enter the account name", placeholderTKey: "AccountNamePlaceHolder",
}) })
accountName: string; accountName: string;
@Values({ @Values({
label: "Collection Throughput", labelTKey: "Collection Throughput",
min: 400, min: getMinCollectionThroughput,
max: getMaxThroughput, max: getMaxCollectionThroughput,
step: 100, step: 100,
uiType: NumberUiType.Spinner, uiType: NumberUiType.Spinner,
}) })
@ -224,16 +236,16 @@ export default class SelfServeExample extends SelfServeBaseClass {
*/ */
@OnChange(onEnableDbLevelThroughputChange) @OnChange(onEnableDbLevelThroughputChange)
@Values({ @Values({
label: "Enable DB level throughput", labelTKey: "Enable DB level throughput",
trueLabel: "Enable", trueLabelTKey: "Enable",
falseLabel: "Disable", falseLabelTKey: "Disable",
}) })
enableDbLevelThroughput: boolean; enableDbLevelThroughput: boolean;
@Values({ @Values({
label: "Database Throughput", labelTKey: "Database Throughput",
min: 400, min: getMinDatabaseThroughput,
max: getMaxThroughput, max: getMaxDatabaseThroughput,
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: {
message: "Start at $24/mo per database", messageTKey: "Start at $24/mo per database",
link: { link: {
href: "https://aka.ms/azure-cosmos-db-pricing", href: "https://aka.ms/azure-cosmos-db-pricing",
text: "More Details", textTKey: "More Details",
}, },
}, },
children: [ children: [
{ {
id: "throughput", id: "throughput",
input: { input: {
label: "Throughput (input)", labelTKey: "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: {
label: "Container id", labelTKey: "Container id",
dataFieldName: "containerId", dataFieldName: "containerId",
type: "string", type: "string",
}, },
@ -65,9 +65,9 @@ describe("SelfServeComponent", () => {
{ {
id: "analyticalStore", id: "analyticalStore",
input: { input: {
label: "Analytical Store", labelTKey: "Analytical Store",
trueLabel: "Enabled", trueLabelTKey: "Enabled",
falseLabel: "Disabled", falseLabelTKey: "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: {
label: "Database", labelTKey: "Database",
dataFieldName: "database", dataFieldName: "database",
type: "object", type: "object",
choices: [ choices: [

View File

@ -26,6 +26,9 @@ 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;
@ -43,6 +46,8 @@ 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();
@ -60,6 +65,7 @@ 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 => {
@ -147,8 +153,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.label = await this.getResolvedValue(input.label); input.labelTKey = await this.getResolvedValue(input.labelTKey);
input.placeholder = await this.getResolvedValue(input.placeholder); input.placeholderTKey = await this.getResolvedValue(input.placeholderTKey);
switch (input.type) { switch (input.type) {
case "string": { case "string": {
@ -177,8 +183,8 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
} }
case "boolean": { case "boolean": {
const booleanInput = input as BooleanInput; const booleanInput = input as BooleanInput;
booleanInput.trueLabel = await this.getResolvedValue(booleanInput.trueLabel); booleanInput.trueLabelTKey = await this.getResolvedValue(booleanInput.trueLabelTKey);
booleanInput.falseLabel = await this.getResolvedValue(booleanInput.falseLabel); booleanInput.falseLabelTKey = await this.getResolvedValue(booleanInput.falseLabelTKey);
return booleanInput; return booleanInput;
} }
default: { default: {
@ -214,7 +220,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
onSavePromise.catch((error) => { onSavePromise.catch((error) => {
this.setState({ this.setState({
notification: { notification: {
message: `Error: ${error.message}`, message: `${error.message}`,
type: SelfServeNotificationType.error, type: SelfServeNotificationType.error,
}, },
}); });
@ -273,11 +279,15 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
this.setState({ isInitializing: false }); this.setState({ isInitializing: false });
}; };
private getCommandBarItems = (): ICommandBarItemProps[] => { public getCommonTranslation = (translationFunction: TFunction, key: string): string => {
return translationFunction(`Common.${key}`);
};
private getCommandBarItems = (translate: TFunction): ICommandBarItemProps[] => {
return [ return [
{ {
key: "save", key: "save",
text: "Save", text: this.getCommonTranslation(translate, "Save"),
iconProps: { iconName: "Save" }, iconProps: { iconName: "Save" },
split: true, split: true,
disabled: this.isSaveButtonDisabled(), disabled: this.isSaveButtonDisabled(),
@ -285,7 +295,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
}, },
{ {
key: "discard", key: "discard",
text: "Discard", text: this.getCommonTranslation(translate, "Discard"),
iconProps: { iconName: "Undo" }, iconProps: { iconName: "Undo" },
split: true, split: true,
disabled: this.isDiscardButtonDisabled(), disabled: this.isDiscardButtonDisabled(),
@ -295,7 +305,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
}, },
{ {
key: "refresh", key: "refresh",
text: "Refresh", text: this.getCommonTranslation(translate, "Refresh"),
disabled: this.state.isInitializing, disabled: this.state.isInitializing,
iconProps: { iconName: "Refresh" }, iconProps: { iconName: "Refresh" },
split: true, split: true,
@ -306,15 +316,30 @@ 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 (
<Translation>
{(translate) => {
const getTranslation = (key: string): string => {
return translate(`${this.smartUiGeneratorClassName}.${key}`);
};
return ( return (
<div style={{ overflowX: "auto" }}> <div style={{ overflowX: "auto" }}>
<Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}> <Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}>
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems()} /> <CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems(translate)} />
{this.state.isInitializing ? ( {this.state.isInitializing ? (
<Spinner <Spinner
size={SpinnerSize.large} size={SpinnerSize.large}
@ -324,7 +349,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
<> <>
{this.state.refreshResult?.isUpdateInProgress && ( {this.state.refreshResult?.isUpdateInProgress && (
<MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}> <MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}>
{this.state.refreshResult.notificationMessage} {getTranslation(this.state.refreshResult.notificationMessage)}
</MessageBar> </MessageBar>
)} )}
{this.state.notification && ( {this.state.notification && (
@ -333,7 +358,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
styles={{ root: { width: 400 } }} styles={{ root: { width: 400 } }}
onDismiss={() => this.setState({ notification: undefined })} onDismiss={() => this.setState({ notification: undefined })}
> >
{this.state.notification.message} {this.getNotificationMessageTranslation(getTranslation, this.state.notification.message)}
</MessageBar> </MessageBar>
)} )}
<SmartUiComponent <SmartUiComponent
@ -342,11 +367,15 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
currentValues={this.state.currentValues} currentValues={this.state.currentValues}
onInputChange={this.onInputChange} onInputChange={this.onInputChange}
onError={this.onError} onError={this.onError}
getTranslation={getTranslation}
/> />
</> </>
)} )}
</Stack> </Stack>
</div> </div>
); );
}}
</Translation>
);
} }
} }

View File

@ -2,9 +2,9 @@ interface BaseInput {
dataFieldName: string; dataFieldName: string;
errorMessage?: string; errorMessage?: string;
type: InputTypeValue; type: InputTypeValue;
label?: (() => Promise<string>) | string; labelTKey?: (() => Promise<string>) | string;
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>; onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>;
placeholder?: (() => Promise<string>) | string; placeholderTKey?: (() => 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 {
trueLabel: (() => Promise<string>) | string; trueLabelTKey: (() => Promise<string>) | string;
falseLabel: (() => Promise<string>) | string; falseLabelTKey: (() => 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 {
message: string; messageTKey: string;
link?: { link?: {
href: string; href: string;
text: string; textTKey: string;
}; };
} }
export interface Description { export interface Description {
text: string; textTKey: string;
link?: { link?: {
href: string; href: string;
text: string; textTKey: string;
}; };
} }

View File

@ -58,7 +58,7 @@ describe("SelfServeUtils", () => {
id: "dbThroughput", id: "dbThroughput",
dataFieldName: "dbThroughput", dataFieldName: "dbThroughput",
type: "number", type: "number",
label: "Database Throughput", labelTKey: "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",
label: "Coll Throughput", labelTKey: "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",
label: "Invalid Coll Throughput", labelTKey: "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",
label: "Coll Name", labelTKey: "Coll Name",
placeholder: "placeholder text", placeholderTKey: "placeholder text",
}, },
], ],
[ [
@ -108,9 +108,9 @@ describe("SelfServeUtils", () => {
id: "enableLogging", id: "enableLogging",
dataFieldName: "enableLogging", dataFieldName: "enableLogging",
type: "boolean", type: "boolean",
label: "Enable Logging", labelTKey: "Enable Logging",
trueLabel: "Enable", trueLabelTKey: "Enable",
falseLabel: "Disable", falseLabelTKey: "Disable",
}, },
], ],
[ [
@ -119,8 +119,8 @@ describe("SelfServeUtils", () => {
id: "invalidEnableLogging", id: "invalidEnableLogging",
dataFieldName: "invalidEnableLogging", dataFieldName: "invalidEnableLogging",
type: "boolean", type: "boolean",
label: "Invalid Enable Logging", labelTKey: "Invalid Enable Logging",
placeholder: "placeholder text", placeholderTKey: "placeholder text",
}, },
], ],
[ [
@ -129,7 +129,7 @@ describe("SelfServeUtils", () => {
id: "regions", id: "regions",
dataFieldName: "regions", dataFieldName: "regions",
type: "object", type: "object",
label: "Regions", labelTKey: "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",
label: "Invalid Regions", labelTKey: "Invalid Regions",
placeholder: "placeholder text", placeholderTKey: "placeholder text",
}, },
], ],
]); ]);
const expectedDescriptor = { const expectedDescriptor = {
root: { root: {
id: "root", id: "TestClass",
children: [ children: [
{ {
id: "dbThroughput", id: "dbThroughput",
@ -158,7 +158,7 @@ describe("SelfServeUtils", () => {
id: "dbThroughput", id: "dbThroughput",
dataFieldName: "dbThroughput", dataFieldName: "dbThroughput",
type: "number", type: "number",
label: "Database Throughput", labelTKey: "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",
label: "Coll Throughput", labelTKey: "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",
label: "Invalid Coll Throughput", labelTKey: "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",
label: "Coll Name", labelTKey: "Coll Name",
placeholder: "placeholder text", placeholderTKey: "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",
label: "Enable Logging", labelTKey: "Enable Logging",
trueLabel: "Enable", trueLabelTKey: "Enable",
falseLabel: "Disable", falseLabelTKey: "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",
label: "Invalid Enable Logging", labelTKey: "Invalid Enable Logging",
placeholder: "placeholder text", placeholderTKey: "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",
label: "Regions", labelTKey: "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",
label: "Invalid Regions", labelTKey: "Invalid Regions",
placeholder: "placeholder text", placeholderTKey: "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(context); const descriptor = mapToSmartUiDescriptor("TestClass", 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;
label?: (() => Promise<string>) | string; labelTKey?: (() => Promise<string>) | string;
placeholder?: (() => Promise<string>) | string; placeholderTKey?: (() => 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;
trueLabel?: (() => Promise<string>) | string; trueLabelTKey?: (() => Promise<string>) | string;
falseLabel?: (() => Promise<string>) | string; falseLabelTKey?: (() => Promise<string>) | string;
choices?: (() => Promise<ChoiceItem[]>) | ChoiceItem[]; choices?: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
uiType?: string; uiType?: string;
errorMessage?: string; errorMessage?: string;
@ -100,18 +100,21 @@ 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(context); const smartUiDescriptor = mapToSmartUiDescriptor(className, context);
Reflect.defineMetadata(className, smartUiDescriptor, target); Reflect.defineMetadata(className, smartUiDescriptor, target);
}; };
export const mapToSmartUiDescriptor = (context: Map<string, DecoratorProperties>): SelfServeDescriptor => { export const mapToSmartUiDescriptor = (
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: "root", id: className,
info: root?.info, info: root?.info,
children: [], children: [],
}, },
@ -147,7 +150,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.label || !value.step || !value.uiType || !value.min || !value.max) { if (!value.labelTKey || !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;
@ -155,17 +158,17 @@ const getInput = (value: DecoratorProperties): AnyDisplay => {
if (value.description) { if (value.description) {
return value as DescriptionDisplay; return value as DescriptionDisplay;
} }
if (!value.label) { if (!value.labelTKey) {
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.label || !value.trueLabel || !value.falseLabel) { if (!value.labelTKey || !value.trueLabelTKey || !value.falseLabelTKey) {
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.label || !value.choices) { if (!value.labelTKey || !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: {
text: "Provisioning dedicated gateways for SqlX accounts.", textTKey: "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",
text: "Learn more about dedicated gateway.", textTKey: "Learn more about dedicated gateway.",
}, },
}, },
}) })
@ -73,21 +73,21 @@ export default class SqlX extends SelfServeBaseClass {
@OnChange(onEnableDedicatedGatewayChange) @OnChange(onEnableDedicatedGatewayChange)
@Values({ @Values({
label: "Dedicated Gateway", labelTKey: "Dedicated Gateway",
trueLabel: "Enable", trueLabelTKey: "Enable",
falseLabel: "Disable", falseLabelTKey: "Disable",
}) })
enableDedicatedGateway: boolean; enableDedicatedGateway: boolean;
@Values({ @Values({
label: "SKUs", labelTKey: "SKUs",
choices: getSkus, choices: getSkus,
placeholder: "Select SKUs", placeholderTKey: "Select SKUs",
}) })
sku: ChoiceItem; sku: ChoiceItem;
@Values({ @Values({
label: "Number of instances", labelTKey: "Number of instances",
min: getInstancesMin, min: getInstancesMin,
max: getInstancesMax, max: getInstancesMax,
step: 1, step: 1,

View File

@ -1,686 +1,21 @@
// 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`] = `
<div <Translation>
style={ <Component />
Object { </Translation>
"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`] = `
<div <Translation>
style={ <Component />
Object { </Translation>
"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`] = `
<div <Translation>
style={ <Component />
Object { </Translation>
"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`] = `
@ -692,195 +27,7 @@ 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`] = `
<div <Translation>
style={ <Component />
Object { </Translation>
"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,12 +41,13 @@ 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();
@ -237,13 +238,14 @@ 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;
} }
@ -265,6 +267,7 @@ function configurePortal() {
} }
explorer.configure(inputs); explorer.configure(inputs);
applyExplorerBindings(explorer);
} }
}, },
false false

31
src/i18n.ts Normal file
View File

@ -0,0 +1,31 @@
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,6 +37,7 @@ 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");

51
utils/cleanupDBs.js Normal file
View File

@ -0,0 +1,51 @@
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);
});