mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-07-15 16:33:49 +01:00
Merge branch 'master' into v-yiqcao/nullCheck
This commit is contained in:
commit
b000115afe
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@ -156,6 +156,7 @@ jobs:
|
||||
run: |
|
||||
npm ci
|
||||
npm start &
|
||||
node utils/cleanupDBs.js
|
||||
npm run wait-for-server
|
||||
npm run test:e2e
|
||||
shell: bash
|
||||
|
4213
package-lock.json
generated
4213
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@ -10,10 +10,10 @@
|
||||
"@azure/identity": "1.2.1",
|
||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||
"@jupyterlab/services": "6.0.0-rc.2",
|
||||
"@jupyterlab/terminal": "3.0.0-rc.2",
|
||||
"@jupyterlab/services": "6.0.2",
|
||||
"@jupyterlab/terminal": "3.0.3",
|
||||
"@microsoft/applicationinsights-web": "2.5.9",
|
||||
"@nteract/commutable": "7.3.2",
|
||||
"@nteract/commutable": "7.4.2",
|
||||
"@nteract/connected-components": "6.8.2",
|
||||
"@nteract/core": "15.1.0",
|
||||
"@nteract/data-explorer": "8.0.3",
|
||||
@ -64,6 +64,9 @@
|
||||
"eslint-plugin-react": "7.20.0",
|
||||
"hasher": "1.2.0",
|
||||
"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",
|
||||
"is-ci": "2.0.0",
|
||||
"jquery": "3.5.1",
|
||||
@ -86,6 +89,7 @@
|
||||
"react-dnd-html5-backend": "9.4.0",
|
||||
"react-dom": "16.13.1",
|
||||
"react-hotkeys": "2.0.0",
|
||||
"react-i18next": "11.8.5",
|
||||
"react-notification-system": "0.2.17",
|
||||
"react-redux": "7.1.3",
|
||||
"redux": "4.0.4",
|
||||
@ -124,7 +128,7 @@
|
||||
"@types/prop-types": "15.5.8",
|
||||
"@types/puppeteer": "3.0.1",
|
||||
"@types/q": "1.5.1",
|
||||
"@types/react": "16.9.56",
|
||||
"@types/react": "17.0.0",
|
||||
"@types/react-dom": "17.0.0",
|
||||
"@types/react-notification-system": "0.2.39",
|
||||
"@types/react-redux": "7.1.7",
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
TriggerDefinition,
|
||||
UserDefinedFunctionDefinition,
|
||||
} from "@azure/cosmos";
|
||||
import Q from "q";
|
||||
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
@ -109,7 +108,7 @@ export interface CollectionBase extends TreeNode {
|
||||
|
||||
onDocumentDBDocumentsClick(): void;
|
||||
onNewQueryClick(source: any, event: MouseEvent, queryText?: string): void;
|
||||
expandCollection(): Q.Promise<any>;
|
||||
expandCollection(): void;
|
||||
collapseCollection(): void;
|
||||
getDatabase(): Database;
|
||||
}
|
||||
@ -176,7 +175,7 @@ export interface Collection extends CollectionBase {
|
||||
|
||||
onDragOver(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;
|
||||
}
|
||||
@ -294,7 +293,7 @@ export interface DocumentsTabOptions extends TabOptions {
|
||||
}
|
||||
|
||||
export interface SettingsTabV2Options extends TabOptions {
|
||||
getPendingNotification: Q.Promise<DataModels.Notification>;
|
||||
getPendingNotification: Promise<DataModels.Notification>;
|
||||
}
|
||||
|
||||
export interface ConflictsTabOptions extends TabOptions {
|
||||
|
@ -31,7 +31,6 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
|
||||
}));
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import Q from "q";
|
||||
jest.mock("../../../Common/dataAccess/updateOffer", () => ({
|
||||
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer),
|
||||
}));
|
||||
@ -47,9 +46,7 @@ describe("SettingsComponent", () => {
|
||||
hashLocation: "settings",
|
||||
isActive: ko.observable(false),
|
||||
onUpdateTabsButtons: undefined,
|
||||
getPendingNotification: Q.Promise<DataModels.Notification>(() => {
|
||||
return;
|
||||
}),
|
||||
getPendingNotification: Promise.resolve(undefined),
|
||||
}),
|
||||
};
|
||||
|
||||
|
@ -8,10 +8,10 @@ describe("SmartUiComponent", () => {
|
||||
root: {
|
||||
id: "root",
|
||||
info: {
|
||||
message: "Start at $24/mo per database",
|
||||
messageTKey: "Start at $24/mo per database",
|
||||
link: {
|
||||
href: "https://aka.ms/azure-cosmos-db-pricing",
|
||||
text: "More Details",
|
||||
textTKey: "More Details",
|
||||
},
|
||||
},
|
||||
children: [
|
||||
@ -21,10 +21,10 @@ describe("SmartUiComponent", () => {
|
||||
dataFieldName: "description",
|
||||
type: "string",
|
||||
description: {
|
||||
text: "this is an example description text.",
|
||||
textTKey: "this is an example description text.",
|
||||
link: {
|
||||
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",
|
||||
input: {
|
||||
label: "Throughput (input)",
|
||||
labelTKey: "Throughput (input)",
|
||||
dataFieldName: "throughput",
|
||||
type: "number",
|
||||
min: 400,
|
||||
@ -45,7 +45,7 @@ describe("SmartUiComponent", () => {
|
||||
{
|
||||
id: "throughput2",
|
||||
input: {
|
||||
label: "Throughput (Slider)",
|
||||
labelTKey: "Throughput (Slider)",
|
||||
dataFieldName: "throughput2",
|
||||
type: "number",
|
||||
min: 400,
|
||||
@ -58,7 +58,7 @@ describe("SmartUiComponent", () => {
|
||||
{
|
||||
id: "throughput3",
|
||||
input: {
|
||||
label: "Throughput (invalid)",
|
||||
labelTKey: "Throughput (invalid)",
|
||||
dataFieldName: "throughput3",
|
||||
type: "boolean",
|
||||
min: 400,
|
||||
@ -72,7 +72,7 @@ describe("SmartUiComponent", () => {
|
||||
{
|
||||
id: "containerId",
|
||||
input: {
|
||||
label: "Container id",
|
||||
labelTKey: "Container id",
|
||||
dataFieldName: "containerId",
|
||||
type: "string",
|
||||
},
|
||||
@ -80,9 +80,9 @@ describe("SmartUiComponent", () => {
|
||||
{
|
||||
id: "analyticalStore",
|
||||
input: {
|
||||
label: "Analytical Store",
|
||||
trueLabel: "Enabled",
|
||||
falseLabel: "Disabled",
|
||||
labelTKey: "Analytical Store",
|
||||
trueLabelTKey: "Enabled",
|
||||
falseLabelTKey: "Disabled",
|
||||
defaultValue: true,
|
||||
dataFieldName: "analyticalStore",
|
||||
type: "boolean",
|
||||
@ -91,7 +91,7 @@ describe("SmartUiComponent", () => {
|
||||
{
|
||||
id: "database",
|
||||
input: {
|
||||
label: "Database",
|
||||
labelTKey: "Database",
|
||||
dataFieldName: "database",
|
||||
type: "object",
|
||||
choices: [
|
||||
@ -117,6 +117,9 @@ describe("SmartUiComponent", () => {
|
||||
onError={() => {
|
||||
return;
|
||||
}}
|
||||
getTranslation={(key: string) => {
|
||||
return key;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
@ -145,6 +148,9 @@ describe("SmartUiComponent", () => {
|
||||
onError={() => {
|
||||
return;
|
||||
}}
|
||||
getTranslation={(key: string) => {
|
||||
return key;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
@ -18,6 +18,7 @@ import {
|
||||
NumberUiType,
|
||||
SmartUiInput,
|
||||
} from "../../../SelfServe/SelfServeTypes";
|
||||
import { TFunction } from "i18next";
|
||||
|
||||
/**
|
||||
* Generic UX renderer
|
||||
@ -34,8 +35,8 @@ interface BaseDisplay {
|
||||
}
|
||||
|
||||
interface BaseInput extends BaseDisplay {
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
labelTKey: string;
|
||||
placeholderTKey?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
@ -51,8 +52,8 @@ interface NumberInput extends BaseInput {
|
||||
}
|
||||
|
||||
interface BooleanInput extends BaseInput {
|
||||
trueLabel: string;
|
||||
falseLabel: string;
|
||||
trueLabelTKey: string;
|
||||
falseLabelTKey: string;
|
||||
defaultValue?: boolean;
|
||||
}
|
||||
|
||||
@ -89,6 +90,7 @@ export interface SmartUiComponentProps {
|
||||
onInputChange: (input: AnyDisplay, newValue: InputType) => void;
|
||||
onError: (hasError: boolean) => void;
|
||||
disabled: boolean;
|
||||
getTranslation: TFunction;
|
||||
}
|
||||
|
||||
interface SmartUiComponentState {
|
||||
@ -122,10 +124,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
private renderInfo(info: Info): JSX.Element {
|
||||
return (
|
||||
<MessageBar styles={{ root: { width: 400 } }}>
|
||||
{info.message}
|
||||
{this.props.getTranslation(info.messageTKey)}
|
||||
{info.link && (
|
||||
<Link href={info.link.href} target="_blank">
|
||||
{info.link.text}
|
||||
{this.props.getTranslation(info.link.textTKey)}
|
||||
</Link>
|
||||
)}
|
||||
</MessageBar>
|
||||
@ -139,10 +141,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
<div className="stringInputContainer">
|
||||
<TextField
|
||||
id={`${input.dataFieldName}-textField-input`}
|
||||
label={input.label}
|
||||
label={this.props.getTranslation(input.labelTKey)}
|
||||
type="text"
|
||||
value={value || ""}
|
||||
placeholder={input.placeholder}
|
||||
placeholder={this.props.getTranslation(input.placeholderTKey)}
|
||||
disabled={disabled}
|
||||
onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
|
||||
styles={{
|
||||
@ -165,10 +167,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
const description = input.description;
|
||||
return (
|
||||
<Text id={`${input.dataFieldName}-text-display`}>
|
||||
{input.description.text}{" "}
|
||||
{this.props.getTranslation(input.description.textTKey)}{" "}
|
||||
{description.link && (
|
||||
<Link target="_blank" href={input.description.link.href}>
|
||||
{input.description.link.text}
|
||||
{this.props.getTranslation(input.description.link.textTKey)}
|
||||
</Link>
|
||||
)}
|
||||
</Text>
|
||||
@ -219,12 +221,12 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
};
|
||||
|
||||
private renderNumberInput(input: NumberInput): JSX.Element {
|
||||
const { label, min, max, dataFieldName, step } = input;
|
||||
const { labelTKey, min, max, dataFieldName, step } = input;
|
||||
const props = {
|
||||
label: label,
|
||||
label: this.props.getTranslation(labelTKey),
|
||||
min: min,
|
||||
max: max,
|
||||
ariaLabel: label,
|
||||
ariaLabel: labelTKey,
|
||||
step: step,
|
||||
};
|
||||
|
||||
@ -284,10 +286,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
return (
|
||||
<Toggle
|
||||
id={`${input.dataFieldName}-toggle-input`}
|
||||
label={input.label}
|
||||
label={this.props.getTranslation(input.labelTKey)}
|
||||
checked={value || false}
|
||||
onText={input.trueLabel}
|
||||
offText={input.falseLabel}
|
||||
onText={this.props.getTranslation(input.trueLabelTKey)}
|
||||
offText={this.props.getTranslation(input.falseLabelTKey)}
|
||||
disabled={disabled}
|
||||
onChange={(event, checked: boolean) => this.props.onInputChange(input, checked)}
|
||||
styles={{ root: { width: 400 } }}
|
||||
@ -296,7 +298,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
}
|
||||
|
||||
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 disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
|
||||
let selectedKey = value ? value : defaultKey;
|
||||
@ -306,14 +308,14 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
return (
|
||||
<Dropdown
|
||||
id={`${input.dataFieldName}-dropdown-input`}
|
||||
label={label}
|
||||
label={this.props.getTranslation(labelTKey)}
|
||||
selectedKey={selectedKey}
|
||||
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
|
||||
placeholder={placeholder}
|
||||
placeholder={this.props.getTranslation(placeholderTKey)}
|
||||
disabled={disabled}
|
||||
options={choices.map((c) => ({
|
||||
key: c.key,
|
||||
text: c.label,
|
||||
text: this.props.getTranslation(c.label),
|
||||
}))}
|
||||
styles={{
|
||||
root: { width: 400 },
|
||||
|
@ -36,7 +36,36 @@ export class CommandBarComponentButtonFactory {
|
||||
}
|
||||
|
||||
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);
|
||||
if (addSynapseLink) {
|
||||
|
@ -45,8 +45,9 @@ type NotebookRendererProps = NotebookRendererBaseProps & NotebookRendererDispatc
|
||||
|
||||
const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, children: React.ReactNode) => {
|
||||
const Cell = () => (
|
||||
<DraggableCell id={id} contentRef={contentRef}>
|
||||
<HijackScroll id={id} contentRef={contentRef}>
|
||||
// TODO Draggable and HijackScroll not working anymore. Fix or remove when reworking MarkdownCell.
|
||||
// <DraggableCell id={id} contentRef={contentRef}>
|
||||
// <HijackScroll id={id} contentRef={contentRef}>
|
||||
<CellCreator id={id} contentRef={contentRef}>
|
||||
<CellLabeler id={id} contentRef={contentRef}>
|
||||
<HoverableCell id={id} contentRef={contentRef}>
|
||||
@ -54,8 +55,8 @@ const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, child
|
||||
</HoverableCell>
|
||||
</CellLabeler>
|
||||
</CellCreator>
|
||||
</HijackScroll>
|
||||
</DraggableCell>
|
||||
// </HijackScroll>
|
||||
// </DraggableCell>
|
||||
);
|
||||
|
||||
Cell.defaultProps = { cell_type };
|
||||
|
@ -818,7 +818,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
let indexingPolicy: DataModels.IndexingPolicy;
|
||||
let createMongoWildcardIndex: boolean;
|
||||
// todo - remove mongo indexing policy ticket # 616274
|
||||
if (this.container.isPreferredApiMongoDB()) {
|
||||
if (this.container.isPreferredApiMongoDB() && this.container.isEnableMongoCapabilityPresent()) {
|
||||
createMongoWildcardIndex = this.shouldCreateMongoWildcardIndex();
|
||||
} else if (this.showIndexingOptionsForSharedThroughput()) {
|
||||
if (this.useIndexingForSharedThroughput()) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||
import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import * as _ from "underscore";
|
||||
import UploadWorker from "worker-loader!../../workers/upload";
|
||||
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()) {
|
||||
return Q();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isCollectionExpanded(true);
|
||||
@ -268,8 +267,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
});
|
||||
|
||||
return Q.resolve();
|
||||
}
|
||||
|
||||
public onDocumentDBDocumentsClick() {
|
||||
@ -547,7 +544,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
});
|
||||
|
||||
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) => {
|
||||
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,
|
||||
traceStartData: any,
|
||||
settingsTabOptions: ViewModels.TabOptions,
|
||||
getPendingNotification: Q.Promise<DataModels.Notification>
|
||||
getPendingNotification: Promise<DataModels.Notification>
|
||||
): void => {
|
||||
const settingsTabV2Options: ViewModels.SettingsTabV2Options = {
|
||||
...settingsTabOptions,
|
||||
@ -980,19 +977,19 @@ export default class Collection implements ViewModels.Collection {
|
||||
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
|
||||
if (configContext.platform === Platform.Hosted && window.authType === AuthType.AAD) {
|
||||
return this._uploadFilesCors(fileList);
|
||||
}
|
||||
const documentUploader: Worker = new UploadWorker();
|
||||
const deferred: Q.Deferred<UploadDetails> = Q.defer<UploadDetails>();
|
||||
let inProgressNotificationId: string = "";
|
||||
|
||||
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 numFailed: number = event.data.numUploadsFailed;
|
||||
const runtimeError: string = event.data.runtimeError;
|
||||
@ -1001,31 +998,26 @@ export default class Collection implements ViewModels.Collection {
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressNotificationId);
|
||||
documentUploader.terminate();
|
||||
if (!!runtimeError) {
|
||||
deferred.reject(runtimeError);
|
||||
reject(runtimeError);
|
||||
} else if (numSuccessful === 0) {
|
||||
// all uploads failed
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Failed to upload all documents to container ${this.id()}`
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleError(`Failed to upload all documents to container ${this.id()}`);
|
||||
} else if (numFailed > 0) {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
NotificationConsoleUtils.logConsoleError(
|
||||
`Failed to upload ${numFailed} of ${numSuccessful + numFailed} documents to container ${this.id()}`
|
||||
);
|
||||
} else {
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
NotificationConsoleUtils.logConsoleInfo(
|
||||
`Successfully uploaded all ${numSuccessful} documents to container ${this.id()}`
|
||||
);
|
||||
}
|
||||
this._logUploadDetailsInConsole(uploadDetails);
|
||||
deferred.resolve(uploadDetails);
|
||||
resolve(uploadDetails);
|
||||
};
|
||||
documentUploader.onerror = (event: ErrorEvent): void => {
|
||||
function onerror(reject: (reason: any) => void, event: ErrorEvent) {
|
||||
documentUploader.terminate();
|
||||
deferred.reject(event.error);
|
||||
};
|
||||
reject(event.error);
|
||||
}
|
||||
|
||||
const uploaderMessage: StartUploadMessageParams = {
|
||||
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);
|
||||
inProgressNotificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Uploading and creating documents in container ${this.id()}`
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
});
|
||||
};
|
||||
|
||||
private _uploadFilesCors(files: FileList): Q.Promise<UploadDetails> {
|
||||
const deferred: Q.Deferred<UploadDetails> = Q.defer<UploadDetails>();
|
||||
const promises: Array<Q.Promise<UploadDetailsRecord>> = [];
|
||||
private async _uploadFilesCors(files: FileList): Promise<UploadDetails> {
|
||||
const data = await Promise.all(Array.from(files).map((file) => this._uploadFile(file)));
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
promises.push(this._uploadFile(files[i]));
|
||||
}
|
||||
Q.all(promises).then((uploadDetails: Array<UploadDetailsRecord>) => {
|
||||
deferred.resolve({ data: uploadDetails });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
return { data };
|
||||
}
|
||||
|
||||
private _uploadFile(file: File): Q.Promise<UploadDetailsRecord> {
|
||||
const deferred: Q.Deferred<UploadDetailsRecord> = Q.defer();
|
||||
|
||||
private _uploadFile(file: File): Promise<UploadDetailsRecord> {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (evt: any): void => {
|
||||
const onload = (resolve: (value: UploadDetailsRecord) => void, evt: any): void => {
|
||||
const fileData: string = evt.target.result;
|
||||
this._createDocumentsFromFile(file.name, fileData).then((record) => {
|
||||
deferred.resolve(record);
|
||||
});
|
||||
this._createDocumentsFromFile(file.name, fileData).then((record) => resolve(record));
|
||||
};
|
||||
|
||||
reader.onerror = (evt: ProgressEvent): void => {
|
||||
deferred.resolve({
|
||||
const onerror = (resolve: (value: UploadDetailsRecord) => void, evt: ProgressEvent): void => {
|
||||
resolve({
|
||||
fileName: file.name,
|
||||
numSucceeded: 0,
|
||||
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);
|
||||
|
||||
return deferred.promise;
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
return Q.resolve(undefined);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const deferred: Q.Deferred<DataModels.Notification> = Q.defer<DataModels.Notification>();
|
||||
fetchPortalNotifications().then(
|
||||
(notifications: DataModels.Notification[]) => {
|
||||
if (!notifications || notifications.length === 0) {
|
||||
deferred.resolve(undefined);
|
||||
return;
|
||||
const throughputUpdateRegExp = new RegExp("Throughput update (.*) in progress");
|
||||
try {
|
||||
const notifications = await fetchPortalNotifications();
|
||||
if (!notifications) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const pendingNotification = _.find(notifications, (notification: DataModels.Notification) => {
|
||||
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress");
|
||||
return (
|
||||
notification.kind === "message" &&
|
||||
notification.collectionName === this.id() &&
|
||||
notification.description &&
|
||||
throughputUpdateRegExp.test(notification.description)
|
||||
return notifications.find(
|
||||
({ kind, collectionName, description = "" }) =>
|
||||
kind === "message" && collectionName === this.id() && throughputUpdateRegExp.test(description)
|
||||
);
|
||||
});
|
||||
|
||||
deferred.resolve(pendingNotification);
|
||||
},
|
||||
(error: any) => {
|
||||
} catch (error) {
|
||||
Logger.logError(
|
||||
JSON.stringify({
|
||||
error: getErrorMessage(error),
|
||||
@ -1154,11 +1130,9 @@ export default class Collection implements ViewModels.Collection {
|
||||
}),
|
||||
"Settings tree node"
|
||||
);
|
||||
deferred.resolve(undefined);
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _logUploadDetailsInConsole(uploadDetails: UploadDetails): void {
|
||||
|
@ -41,9 +41,9 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
||||
this.isCollectionExpanded = ko.observable<boolean>(true);
|
||||
}
|
||||
|
||||
public expandCollection(): Q.Promise<void> {
|
||||
public expandCollection(): void {
|
||||
if (this.isCollectionExpanded()) {
|
||||
return Q();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isCollectionExpanded(true);
|
||||
@ -55,8 +55,6 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
});
|
||||
|
||||
return Q.resolve();
|
||||
}
|
||||
|
||||
public collapseCollection() {
|
||||
|
@ -248,7 +248,7 @@ describe("Gallery", () => {
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||
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",
|
||||
headers: {
|
||||
|
@ -80,7 +80,7 @@ export class JunoClient {
|
||||
}
|
||||
|
||||
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(),
|
||||
});
|
||||
|
||||
@ -103,7 +103,7 @@ export class JunoClient {
|
||||
}
|
||||
|
||||
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",
|
||||
body: JSON.stringify(repos),
|
||||
headers: JunoClient.getHeaders(),
|
||||
@ -120,7 +120,7 @@ export class JunoClient {
|
||||
}
|
||||
|
||||
public async deleteGitHubInfo(): Promise<IJunoResponse<undefined>> {
|
||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github`, {
|
||||
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/github`, {
|
||||
method: "DELETE",
|
||||
headers: JunoClient.getHeaders(),
|
||||
});
|
||||
@ -135,9 +135,12 @@ export class JunoClient {
|
||||
const githubParams = JunoClient.getGitHubClientParams();
|
||||
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(),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
let data: IGitHubOAuthToken;
|
||||
const body = await response.text();
|
||||
@ -159,10 +162,13 @@ export class JunoClient {
|
||||
const githubParams = JunoClient.getGitHubClientParams();
|
||||
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",
|
||||
headers: JunoClient.getHeaders(),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
@ -179,7 +185,7 @@ export class JunoClient {
|
||||
}
|
||||
|
||||
public async getPublicGalleryData(): Promise<IJunoResponse<IPublicGalleryData>> {
|
||||
const url = `${this.getNotebooksAccountUrl()}/gallery/public`;
|
||||
const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/public`;
|
||||
const response = await window.fetch(url, {
|
||||
method: "PATCH",
|
||||
headers: JunoClient.getHeaders(),
|
||||
@ -197,7 +203,7 @@ export class JunoClient {
|
||||
}
|
||||
|
||||
public async acceptCodeOfConduct(): Promise<IJunoResponse<boolean>> {
|
||||
const url = `${this.getNotebooksAccountUrl()}/gallery/acceptCodeOfConduct`;
|
||||
const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/acceptCodeOfConduct`;
|
||||
const response = await window.fetch(url, {
|
||||
method: "PATCH",
|
||||
headers: JunoClient.getHeaders(),
|
||||
@ -215,7 +221,7 @@ export class JunoClient {
|
||||
}
|
||||
|
||||
public async isCodeOfConductAccepted(): Promise<IJunoResponse<boolean>> {
|
||||
const url = `${this.getNotebooksAccountUrl()}/gallery/isCodeOfConductAccepted`;
|
||||
const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/isCodeOfConductAccepted`;
|
||||
const response = await window.fetch(url, {
|
||||
method: "PATCH",
|
||||
headers: JunoClient.getHeaders(),
|
||||
@ -294,7 +300,7 @@ export class JunoClient {
|
||||
}
|
||||
|
||||
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",
|
||||
headers: JunoClient.getHeaders(),
|
||||
});
|
||||
@ -365,9 +371,7 @@ export class JunoClient {
|
||||
content: string,
|
||||
isLinkInjectionEnabled: boolean
|
||||
): Promise<IJunoResponse<IGalleryItem>> {
|
||||
const response = await window.fetch(
|
||||
`${this.getNotebooksUrl()}/${this.getSubscriptionId()}/${this.getAccount()}/gallery`,
|
||||
{
|
||||
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery`, {
|
||||
method: "PUT",
|
||||
headers: JunoClient.getHeaders(),
|
||||
body: JSON.stringify({
|
||||
@ -379,8 +383,7 @@ export class JunoClient {
|
||||
content: JSON.parse(content),
|
||||
addLinkToNotebookViewer: isLinkInjectionEnabled,
|
||||
} as IPublishNotebookRequest),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
let data: IGalleryItem;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
@ -502,6 +505,10 @@ export class JunoClient {
|
||||
return `${this.getNotebooksUrl()}/${this.getAccount()}`;
|
||||
}
|
||||
|
||||
private getNotebooksSubscriptionIdAccountUrl(): string {
|
||||
return `${this.getNotebooksUrl()}/${this.getSubscriptionId()}/${this.getAccount()}`;
|
||||
}
|
||||
|
||||
private getAnalyticsUrl(): string {
|
||||
return `${configContext.JUNO_ENDPOINT}/api/analytics`;
|
||||
}
|
||||
|
33
src/Localization/en/translations.json
Normal file
33
src/Localization/en/translations.json
Normal 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": {
|
||||
}
|
||||
}
|
||||
}
|
@ -288,12 +288,10 @@ export class TabRouteHandler {
|
||||
private _openSprocTabForResource(databaseId: string, collectionId: string, sprocId: string): void {
|
||||
this._executeActionHelper(() => {
|
||||
const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId);
|
||||
collection &&
|
||||
collection.expandCollection().then(() => {
|
||||
collection && collection.expandCollection();
|
||||
const storedProcedure = collection && collection.findStoredProcedureWithId(sprocId);
|
||||
storedProcedure && storedProcedure.open();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _openNewTriggerTabForResource(databaseId: string, collectionId: string): void {
|
||||
@ -319,12 +317,10 @@ export class TabRouteHandler {
|
||||
private _openTriggerTabForResource(databaseId: string, collectionId: string, triggerId: string): void {
|
||||
this._executeActionHelper(() => {
|
||||
const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId);
|
||||
collection &&
|
||||
collection.expandCollection().then(() => {
|
||||
collection && collection.expandCollection();
|
||||
const trigger = collection && collection.findTriggerWithId(triggerId);
|
||||
trigger && trigger.open();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _openNewUserDefinedFunctionTabForResource(databaseId: string, collectionId: string): void {
|
||||
@ -350,12 +346,10 @@ export class TabRouteHandler {
|
||||
private _openUserDefinedFunctionTabForResource(databaseId: string, collectionId: string, udfId: string): void {
|
||||
this._executeActionHelper(() => {
|
||||
const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId);
|
||||
collection &&
|
||||
collection.expandCollection().then(() => {
|
||||
collection && collection.expandCollection();
|
||||
const userDefinedFunction = collection && collection.findUserDefinedFunctionWithId(udfId);
|
||||
userDefinedFunction && userDefinedFunction.open();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _openConflictsTabForResource(databaseId: string, collectionId: string): void {
|
||||
|
@ -8,7 +8,7 @@ interface Decorator {
|
||||
}
|
||||
|
||||
interface InputOptionsBase {
|
||||
label: string;
|
||||
labelTKey: string;
|
||||
}
|
||||
|
||||
export interface NumberInputOptions extends InputOptionsBase {
|
||||
@ -19,17 +19,17 @@ export interface NumberInputOptions extends InputOptionsBase {
|
||||
}
|
||||
|
||||
export interface StringInputOptions extends InputOptionsBase {
|
||||
placeholder?: (() => Promise<string>) | string;
|
||||
placeholderTKey?: (() => Promise<string>) | string;
|
||||
}
|
||||
|
||||
export interface BooleanInputOptions extends InputOptionsBase {
|
||||
trueLabel: (() => Promise<string>) | string;
|
||||
falseLabel: (() => Promise<string>) | string;
|
||||
trueLabelTKey: (() => Promise<string>) | string;
|
||||
falseLabelTKey: (() => Promise<string>) | string;
|
||||
}
|
||||
|
||||
export interface ChoiceInputOptions extends InputOptionsBase {
|
||||
choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
|
||||
placeholder?: (() => Promise<string>) | string;
|
||||
placeholderTKey?: (() => Promise<string>) | string;
|
||||
}
|
||||
|
||||
export interface DescriptionDisplayOptions {
|
||||
@ -48,7 +48,7 @@ const isNumberInputOptions = (inputOptions: InputOptions): inputOptions is Numbe
|
||||
};
|
||||
|
||||
const isBooleanInputOptions = (inputOptions: InputOptions): inputOptions is BooleanInputOptions => {
|
||||
return "trueLabel" in inputOptions;
|
||||
return "trueLabelTKey" in inputOptions;
|
||||
};
|
||||
|
||||
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 => {
|
||||
if (isNumberInputOptions(inputOptions)) {
|
||||
return addToMap(
|
||||
{ name: "label", value: inputOptions.label },
|
||||
{ name: "labelTKey", value: inputOptions.labelTKey },
|
||||
{ name: "min", value: inputOptions.min },
|
||||
{ name: "max", value: inputOptions.max },
|
||||
{ name: "step", value: inputOptions.step },
|
||||
@ -100,22 +100,22 @@ export const Values = (inputOptions: InputOptions): PropertyDecorator => {
|
||||
);
|
||||
} else if (isBooleanInputOptions(inputOptions)) {
|
||||
return addToMap(
|
||||
{ name: "label", value: inputOptions.label },
|
||||
{ name: "trueLabel", value: inputOptions.trueLabel },
|
||||
{ name: "falseLabel", value: inputOptions.falseLabel }
|
||||
{ name: "labelTKey", value: inputOptions.labelTKey },
|
||||
{ name: "trueLabelTKey", value: inputOptions.trueLabelTKey },
|
||||
{ name: "falseLabelTKey", value: inputOptions.falseLabelTKey }
|
||||
);
|
||||
} else if (isChoiceInputOptions(inputOptions)) {
|
||||
return addToMap(
|
||||
{ name: "label", value: inputOptions.label },
|
||||
{ name: "placeholder", value: inputOptions.placeholder },
|
||||
{ name: "labelTKey", value: inputOptions.labelTKey },
|
||||
{ name: "placeholderTKey", value: inputOptions.placeholderTKey },
|
||||
{ name: "choices", value: inputOptions.choices }
|
||||
);
|
||||
} else if (isDescriptionDisplayOptions(inputOptions)) {
|
||||
return addToMap({ name: "description", value: inputOptions.description });
|
||||
} else {
|
||||
return addToMap(
|
||||
{ name: "label", value: inputOptions.label },
|
||||
{ name: "placeholder", value: inputOptions.placeholder }
|
||||
{ name: "labelTKey", value: inputOptions.labelTKey },
|
||||
{ name: "placeholderTKey", value: inputOptions.placeholderTKey }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -16,10 +16,22 @@ export interface InitializeResponse {
|
||||
dbThroughput: number;
|
||||
}
|
||||
|
||||
export const getMaxThroughput = async (): Promise<number> => {
|
||||
export const getMaxCollectionThroughput = async (): Promise<number> => {
|
||||
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 (
|
||||
regions: Regions,
|
||||
enableLogging: boolean,
|
||||
@ -59,6 +71,6 @@ export const onRefreshSelfServeExample = async (): Promise<RefreshResult> => {
|
||||
const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded";
|
||||
return {
|
||||
isUpdateInProgress: isUpdateInProgress,
|
||||
notificationMessage: "Self Serve Example successfully refreshing",
|
||||
notificationMessage: "RefreshMessage",
|
||||
};
|
||||
};
|
||||
|
@ -10,7 +10,16 @@ import {
|
||||
SelfServeNotificationType,
|
||||
SmartUiInput,
|
||||
} 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[] = [
|
||||
{ label: "North Central US", key: Regions.NorthCentralUS },
|
||||
@ -19,11 +28,11 @@ const regionDropdownItems: ChoiceItem[] = [
|
||||
];
|
||||
|
||||
const selfServeExampleInfo: Info = {
|
||||
message: "This is a self serve class",
|
||||
messageTKey: "ClassInfo",
|
||||
};
|
||||
|
||||
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> => {
|
||||
@ -50,7 +59,7 @@ const onEnableDbLevelThroughputChange = (
|
||||
|
||||
const validate = (currentvalues: Map<string, SmartUiInput>): void => {
|
||||
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'
|
||||
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;
|
||||
dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined;
|
||||
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({
|
||||
description: {
|
||||
text: "This class sets collection and database throughput.",
|
||||
textTKey: "DescriptionText",
|
||||
link: {
|
||||
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"
|
||||
*/
|
||||
@OnChange(onRegionsChange)
|
||||
@Values({ label: "Regions", choices: regionDropdownItems, placeholder: "Select a region" })
|
||||
@Values({ labelTKey: "Regions", choices: regionDropdownItems, placeholderTKey: "RegionsPlaceholder" })
|
||||
regions: ChoiceItem;
|
||||
|
||||
@Values({
|
||||
label: "Enable Logging",
|
||||
trueLabel: "Enable",
|
||||
falseLabel: "Disable",
|
||||
labelTKey: "Enable Logging",
|
||||
trueLabelTKey: "Enable",
|
||||
falseLabelTKey: "Disable",
|
||||
})
|
||||
enableLogging: boolean;
|
||||
|
||||
@Values({
|
||||
label: "Account Name",
|
||||
placeholder: "Enter the account name",
|
||||
labelTKey: "Account Name",
|
||||
placeholderTKey: "AccountNamePlaceHolder",
|
||||
})
|
||||
accountName: string;
|
||||
|
||||
@Values({
|
||||
label: "Collection Throughput",
|
||||
min: 400,
|
||||
max: getMaxThroughput,
|
||||
labelTKey: "Collection Throughput",
|
||||
min: getMinCollectionThroughput,
|
||||
max: getMaxCollectionThroughput,
|
||||
step: 100,
|
||||
uiType: NumberUiType.Spinner,
|
||||
})
|
||||
@ -224,16 +236,16 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
||||
*/
|
||||
@OnChange(onEnableDbLevelThroughputChange)
|
||||
@Values({
|
||||
label: "Enable DB level throughput",
|
||||
trueLabel: "Enable",
|
||||
falseLabel: "Disable",
|
||||
labelTKey: "Enable DB level throughput",
|
||||
trueLabelTKey: "Enable",
|
||||
falseLabelTKey: "Disable",
|
||||
})
|
||||
enableDbLevelThroughput: boolean;
|
||||
|
||||
@Values({
|
||||
label: "Database Throughput",
|
||||
min: 400,
|
||||
max: getMaxThroughput,
|
||||
labelTKey: "Database Throughput",
|
||||
min: getMinDatabaseThroughput,
|
||||
max: getMaxDatabaseThroughput,
|
||||
step: 100,
|
||||
uiType: NumberUiType.Slider,
|
||||
})
|
||||
|
@ -34,17 +34,17 @@ describe("SelfServeComponent", () => {
|
||||
root: {
|
||||
id: "root",
|
||||
info: {
|
||||
message: "Start at $24/mo per database",
|
||||
messageTKey: "Start at $24/mo per database",
|
||||
link: {
|
||||
href: "https://aka.ms/azure-cosmos-db-pricing",
|
||||
text: "More Details",
|
||||
textTKey: "More Details",
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: "throughput",
|
||||
input: {
|
||||
label: "Throughput (input)",
|
||||
labelTKey: "Throughput (input)",
|
||||
dataFieldName: "throughput",
|
||||
type: "number",
|
||||
min: 400,
|
||||
@ -57,7 +57,7 @@ describe("SelfServeComponent", () => {
|
||||
{
|
||||
id: "containerId",
|
||||
input: {
|
||||
label: "Container id",
|
||||
labelTKey: "Container id",
|
||||
dataFieldName: "containerId",
|
||||
type: "string",
|
||||
},
|
||||
@ -65,9 +65,9 @@ describe("SelfServeComponent", () => {
|
||||
{
|
||||
id: "analyticalStore",
|
||||
input: {
|
||||
label: "Analytical Store",
|
||||
trueLabel: "Enabled",
|
||||
falseLabel: "Disabled",
|
||||
labelTKey: "Analytical Store",
|
||||
trueLabelTKey: "Enabled",
|
||||
falseLabelTKey: "Disabled",
|
||||
defaultValue: true,
|
||||
dataFieldName: "analyticalStore",
|
||||
type: "boolean",
|
||||
@ -76,7 +76,7 @@ describe("SelfServeComponent", () => {
|
||||
{
|
||||
id: "database",
|
||||
input: {
|
||||
label: "Database",
|
||||
labelTKey: "Database",
|
||||
dataFieldName: "database",
|
||||
type: "object",
|
||||
choices: [
|
||||
|
@ -26,6 +26,9 @@ import {
|
||||
} from "./SelfServeTypes";
|
||||
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
||||
import { getMessageBarType } from "./SelfServeUtils";
|
||||
import { Translation } from "react-i18next";
|
||||
import { TFunction } from "i18next";
|
||||
import "../i18n";
|
||||
|
||||
export interface SelfServeComponentProps {
|
||||
descriptor: SelfServeDescriptor;
|
||||
@ -43,6 +46,8 @@ export interface SelfServeComponentState {
|
||||
}
|
||||
|
||||
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
|
||||
private smartUiGeneratorClassName: string;
|
||||
|
||||
componentDidMount(): void {
|
||||
this.performRefresh();
|
||||
this.initializeSmartUiComponent();
|
||||
@ -60,6 +65,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
notification: undefined,
|
||||
refreshResult: undefined,
|
||||
};
|
||||
this.smartUiGeneratorClassName = this.props.descriptor.root.id;
|
||||
}
|
||||
|
||||
private onError = (hasErrors: boolean): void => {
|
||||
@ -147,8 +153,8 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
currentValues: Map<string, SmartUiInput>,
|
||||
baselineValues: Map<string, SmartUiInput>
|
||||
): Promise<AnyDisplay> => {
|
||||
input.label = await this.getResolvedValue(input.label);
|
||||
input.placeholder = await this.getResolvedValue(input.placeholder);
|
||||
input.labelTKey = await this.getResolvedValue(input.labelTKey);
|
||||
input.placeholderTKey = await this.getResolvedValue(input.placeholderTKey);
|
||||
|
||||
switch (input.type) {
|
||||
case "string": {
|
||||
@ -177,8 +183,8 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
}
|
||||
case "boolean": {
|
||||
const booleanInput = input as BooleanInput;
|
||||
booleanInput.trueLabel = await this.getResolvedValue(booleanInput.trueLabel);
|
||||
booleanInput.falseLabel = await this.getResolvedValue(booleanInput.falseLabel);
|
||||
booleanInput.trueLabelTKey = await this.getResolvedValue(booleanInput.trueLabelTKey);
|
||||
booleanInput.falseLabelTKey = await this.getResolvedValue(booleanInput.falseLabelTKey);
|
||||
return booleanInput;
|
||||
}
|
||||
default: {
|
||||
@ -214,7 +220,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
onSavePromise.catch((error) => {
|
||||
this.setState({
|
||||
notification: {
|
||||
message: `Error: ${error.message}`,
|
||||
message: `${error.message}`,
|
||||
type: SelfServeNotificationType.error,
|
||||
},
|
||||
});
|
||||
@ -273,11 +279,15 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
this.setState({ isInitializing: false });
|
||||
};
|
||||
|
||||
private getCommandBarItems = (): ICommandBarItemProps[] => {
|
||||
public getCommonTranslation = (translationFunction: TFunction, key: string): string => {
|
||||
return translationFunction(`Common.${key}`);
|
||||
};
|
||||
|
||||
private getCommandBarItems = (translate: TFunction): ICommandBarItemProps[] => {
|
||||
return [
|
||||
{
|
||||
key: "save",
|
||||
text: "Save",
|
||||
text: this.getCommonTranslation(translate, "Save"),
|
||||
iconProps: { iconName: "Save" },
|
||||
split: true,
|
||||
disabled: this.isSaveButtonDisabled(),
|
||||
@ -285,7 +295,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
},
|
||||
{
|
||||
key: "discard",
|
||||
text: "Discard",
|
||||
text: this.getCommonTranslation(translate, "Discard"),
|
||||
iconProps: { iconName: "Undo" },
|
||||
split: true,
|
||||
disabled: this.isDiscardButtonDisabled(),
|
||||
@ -295,7 +305,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
text: "Refresh",
|
||||
text: this.getCommonTranslation(translate, "Refresh"),
|
||||
disabled: this.state.isInitializing,
|
||||
iconProps: { iconName: "Refresh" },
|
||||
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 {
|
||||
const containerStackTokens: IStackTokens = { childrenGap: 5 };
|
||||
if (this.state.compileErrorMessage) {
|
||||
return <MessageBar messageBarType={MessageBarType.error}>{this.state.compileErrorMessage}</MessageBar>;
|
||||
}
|
||||
return (
|
||||
<Translation>
|
||||
{(translate) => {
|
||||
const getTranslation = (key: string): string => {
|
||||
return translate(`${this.smartUiGeneratorClassName}.${key}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<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 ? (
|
||||
<Spinner
|
||||
size={SpinnerSize.large}
|
||||
@ -324,7 +349,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
<>
|
||||
{this.state.refreshResult?.isUpdateInProgress && (
|
||||
<MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}>
|
||||
{this.state.refreshResult.notificationMessage}
|
||||
{getTranslation(this.state.refreshResult.notificationMessage)}
|
||||
</MessageBar>
|
||||
)}
|
||||
{this.state.notification && (
|
||||
@ -333,7 +358,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
styles={{ root: { width: 400 } }}
|
||||
onDismiss={() => this.setState({ notification: undefined })}
|
||||
>
|
||||
{this.state.notification.message}
|
||||
{this.getNotificationMessageTranslation(getTranslation, this.state.notification.message)}
|
||||
</MessageBar>
|
||||
)}
|
||||
<SmartUiComponent
|
||||
@ -342,11 +367,15 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
currentValues={this.state.currentValues}
|
||||
onInputChange={this.onInputChange}
|
||||
onError={this.onError}
|
||||
getTranslation={getTranslation}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Translation>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,9 @@ interface BaseInput {
|
||||
dataFieldName: string;
|
||||
errorMessage?: string;
|
||||
type: InputTypeValue;
|
||||
label?: (() => Promise<string>) | string;
|
||||
labelTKey?: (() => Promise<string>) | string;
|
||||
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>;
|
||||
placeholder?: (() => Promise<string>) | string;
|
||||
placeholderTKey?: (() => Promise<string>) | string;
|
||||
}
|
||||
|
||||
export interface NumberInput extends BaseInput {
|
||||
@ -16,8 +16,8 @@ export interface NumberInput extends BaseInput {
|
||||
}
|
||||
|
||||
export interface BooleanInput extends BaseInput {
|
||||
trueLabel: (() => Promise<string>) | string;
|
||||
falseLabel: (() => Promise<string>) | string;
|
||||
trueLabelTKey: (() => Promise<string>) | string;
|
||||
falseLabelTKey: (() => Promise<string>) | string;
|
||||
defaultValue?: boolean;
|
||||
}
|
||||
|
||||
@ -92,18 +92,18 @@ export type ChoiceItem = { label: string; key: string };
|
||||
export type InputType = number | string | boolean | ChoiceItem;
|
||||
|
||||
export interface Info {
|
||||
message: string;
|
||||
messageTKey: string;
|
||||
link?: {
|
||||
href: string;
|
||||
text: string;
|
||||
textTKey: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Description {
|
||||
text: string;
|
||||
textTKey: string;
|
||||
link?: {
|
||||
href: string;
|
||||
text: string;
|
||||
textTKey: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,7 @@ describe("SelfServeUtils", () => {
|
||||
id: "dbThroughput",
|
||||
dataFieldName: "dbThroughput",
|
||||
type: "number",
|
||||
label: "Database Throughput",
|
||||
labelTKey: "Database Throughput",
|
||||
min: 1,
|
||||
max: 5,
|
||||
step: 1,
|
||||
@ -71,7 +71,7 @@ describe("SelfServeUtils", () => {
|
||||
id: "collThroughput",
|
||||
dataFieldName: "collThroughput",
|
||||
type: "number",
|
||||
label: "Coll Throughput",
|
||||
labelTKey: "Coll Throughput",
|
||||
min: 1,
|
||||
max: 5,
|
||||
step: 1,
|
||||
@ -84,7 +84,7 @@ describe("SelfServeUtils", () => {
|
||||
id: "invalidThroughput",
|
||||
dataFieldName: "invalidThroughput",
|
||||
type: "boolean",
|
||||
label: "Invalid Coll Throughput",
|
||||
labelTKey: "Invalid Coll Throughput",
|
||||
min: 1,
|
||||
max: 5,
|
||||
step: 1,
|
||||
@ -98,8 +98,8 @@ describe("SelfServeUtils", () => {
|
||||
id: "collName",
|
||||
dataFieldName: "collName",
|
||||
type: "string",
|
||||
label: "Coll Name",
|
||||
placeholder: "placeholder text",
|
||||
labelTKey: "Coll Name",
|
||||
placeholderTKey: "placeholder text",
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -108,9 +108,9 @@ describe("SelfServeUtils", () => {
|
||||
id: "enableLogging",
|
||||
dataFieldName: "enableLogging",
|
||||
type: "boolean",
|
||||
label: "Enable Logging",
|
||||
trueLabel: "Enable",
|
||||
falseLabel: "Disable",
|
||||
labelTKey: "Enable Logging",
|
||||
trueLabelTKey: "Enable",
|
||||
falseLabelTKey: "Disable",
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -119,8 +119,8 @@ describe("SelfServeUtils", () => {
|
||||
id: "invalidEnableLogging",
|
||||
dataFieldName: "invalidEnableLogging",
|
||||
type: "boolean",
|
||||
label: "Invalid Enable Logging",
|
||||
placeholder: "placeholder text",
|
||||
labelTKey: "Invalid Enable Logging",
|
||||
placeholderTKey: "placeholder text",
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -129,7 +129,7 @@ describe("SelfServeUtils", () => {
|
||||
id: "regions",
|
||||
dataFieldName: "regions",
|
||||
type: "object",
|
||||
label: "Regions",
|
||||
labelTKey: "Regions",
|
||||
choices: [
|
||||
{ label: "South West US", key: "SWUS" },
|
||||
{ label: "North Central US", key: "NCUS" },
|
||||
@ -143,14 +143,14 @@ describe("SelfServeUtils", () => {
|
||||
id: "invalidRegions",
|
||||
dataFieldName: "invalidRegions",
|
||||
type: "object",
|
||||
label: "Invalid Regions",
|
||||
placeholder: "placeholder text",
|
||||
labelTKey: "Invalid Regions",
|
||||
placeholderTKey: "placeholder text",
|
||||
},
|
||||
],
|
||||
]);
|
||||
const expectedDescriptor = {
|
||||
root: {
|
||||
id: "root",
|
||||
id: "TestClass",
|
||||
children: [
|
||||
{
|
||||
id: "dbThroughput",
|
||||
@ -158,7 +158,7 @@ describe("SelfServeUtils", () => {
|
||||
id: "dbThroughput",
|
||||
dataFieldName: "dbThroughput",
|
||||
type: "number",
|
||||
label: "Database Throughput",
|
||||
labelTKey: "Database Throughput",
|
||||
min: 1,
|
||||
max: 5,
|
||||
step: 1,
|
||||
@ -172,7 +172,7 @@ describe("SelfServeUtils", () => {
|
||||
id: "collThroughput",
|
||||
dataFieldName: "collThroughput",
|
||||
type: "number",
|
||||
label: "Coll Throughput",
|
||||
labelTKey: "Coll Throughput",
|
||||
min: 1,
|
||||
max: 5,
|
||||
step: 1,
|
||||
@ -186,7 +186,7 @@ describe("SelfServeUtils", () => {
|
||||
id: "invalidThroughput",
|
||||
dataFieldName: "invalidThroughput",
|
||||
type: "boolean",
|
||||
label: "Invalid Coll Throughput",
|
||||
labelTKey: "Invalid Coll Throughput",
|
||||
min: 1,
|
||||
max: 5,
|
||||
step: 1,
|
||||
@ -201,8 +201,8 @@ describe("SelfServeUtils", () => {
|
||||
id: "collName",
|
||||
dataFieldName: "collName",
|
||||
type: "string",
|
||||
label: "Coll Name",
|
||||
placeholder: "placeholder text",
|
||||
labelTKey: "Coll Name",
|
||||
placeholderTKey: "placeholder text",
|
||||
},
|
||||
children: [] as Node[],
|
||||
},
|
||||
@ -212,9 +212,9 @@ describe("SelfServeUtils", () => {
|
||||
id: "enableLogging",
|
||||
dataFieldName: "enableLogging",
|
||||
type: "boolean",
|
||||
label: "Enable Logging",
|
||||
trueLabel: "Enable",
|
||||
falseLabel: "Disable",
|
||||
labelTKey: "Enable Logging",
|
||||
trueLabelTKey: "Enable",
|
||||
falseLabelTKey: "Disable",
|
||||
},
|
||||
children: [] as Node[],
|
||||
},
|
||||
@ -224,8 +224,8 @@ describe("SelfServeUtils", () => {
|
||||
id: "invalidEnableLogging",
|
||||
dataFieldName: "invalidEnableLogging",
|
||||
type: "boolean",
|
||||
label: "Invalid Enable Logging",
|
||||
placeholder: "placeholder text",
|
||||
labelTKey: "Invalid Enable Logging",
|
||||
placeholderTKey: "placeholder text",
|
||||
errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidEnableLogging'.",
|
||||
},
|
||||
children: [] as Node[],
|
||||
@ -236,7 +236,7 @@ describe("SelfServeUtils", () => {
|
||||
id: "regions",
|
||||
dataFieldName: "regions",
|
||||
type: "object",
|
||||
label: "Regions",
|
||||
labelTKey: "Regions",
|
||||
choices: [
|
||||
{ label: "South West US", key: "SWUS" },
|
||||
{ label: "North Central US", key: "NCUS" },
|
||||
@ -251,8 +251,8 @@ describe("SelfServeUtils", () => {
|
||||
id: "invalidRegions",
|
||||
dataFieldName: "invalidRegions",
|
||||
type: "object",
|
||||
label: "Invalid Regions",
|
||||
placeholder: "placeholder text",
|
||||
labelTKey: "Invalid Regions",
|
||||
placeholderTKey: "placeholder text",
|
||||
errorMessage: "label and choices are required for Choice input 'invalidRegions'.",
|
||||
},
|
||||
children: [] as Node[],
|
||||
@ -270,7 +270,7 @@ describe("SelfServeUtils", () => {
|
||||
"invalidRegions",
|
||||
],
|
||||
};
|
||||
const descriptor = mapToSmartUiDescriptor(context);
|
||||
const descriptor = mapToSmartUiDescriptor("TestClass", context);
|
||||
expect(descriptor).toEqual(expectedDescriptor);
|
||||
});
|
||||
});
|
||||
|
@ -32,14 +32,14 @@ export interface DecoratorProperties {
|
||||
id: string;
|
||||
info?: (() => Promise<Info>) | Info;
|
||||
type?: InputTypeValue;
|
||||
label?: (() => Promise<string>) | string;
|
||||
placeholder?: (() => Promise<string>) | string;
|
||||
labelTKey?: (() => Promise<string>) | string;
|
||||
placeholderTKey?: (() => Promise<string>) | string;
|
||||
dataFieldName?: string;
|
||||
min?: (() => Promise<number>) | number;
|
||||
max?: (() => Promise<number>) | number;
|
||||
step?: (() => Promise<number>) | number;
|
||||
trueLabel?: (() => Promise<string>) | string;
|
||||
falseLabel?: (() => Promise<string>) | string;
|
||||
trueLabelTKey?: (() => Promise<string>) | string;
|
||||
falseLabelTKey?: (() => Promise<string>) | string;
|
||||
choices?: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
|
||||
uiType?: string;
|
||||
errorMessage?: string;
|
||||
@ -100,18 +100,21 @@ export const updateContextWithDecorator = <T extends keyof DecoratorProperties,
|
||||
|
||||
export const buildSmartUiDescriptor = (className: string, target: unknown): void => {
|
||||
const context = Reflect.getMetadata(className, target) as Map<string, DecoratorProperties>;
|
||||
const smartUiDescriptor = mapToSmartUiDescriptor(context);
|
||||
const smartUiDescriptor = mapToSmartUiDescriptor(className, context);
|
||||
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");
|
||||
context.delete("root");
|
||||
const inputNames: string[] = [];
|
||||
|
||||
const smartUiDescriptor: SelfServeDescriptor = {
|
||||
root: {
|
||||
id: "root",
|
||||
id: className,
|
||||
info: root?.info,
|
||||
children: [],
|
||||
},
|
||||
@ -147,7 +150,7 @@ const addToDescriptor = (
|
||||
const getInput = (value: DecoratorProperties): AnyDisplay => {
|
||||
switch (value.type) {
|
||||
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}'.`;
|
||||
}
|
||||
return value as NumberInput;
|
||||
@ -155,17 +158,17 @@ const getInput = (value: DecoratorProperties): AnyDisplay => {
|
||||
if (value.description) {
|
||||
return value as DescriptionDisplay;
|
||||
}
|
||||
if (!value.label) {
|
||||
if (!value.labelTKey) {
|
||||
value.errorMessage = `label is required for string input '${value.id}'.`;
|
||||
}
|
||||
return value as StringInput;
|
||||
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}'.`;
|
||||
}
|
||||
return value as BooleanInput;
|
||||
default:
|
||||
if (!value.label || !value.choices) {
|
||||
if (!value.labelTKey || !value.choices) {
|
||||
value.errorMessage = `label and choices are required for Choice input '${value.id}'.`;
|
||||
}
|
||||
return value as ChoiceInput;
|
||||
|
@ -62,10 +62,10 @@ export default class SqlX extends SelfServeBaseClass {
|
||||
|
||||
@Values({
|
||||
description: {
|
||||
text: "Provisioning dedicated gateways for SqlX accounts.",
|
||||
textTKey: "Provisioning dedicated gateways for SqlX accounts.",
|
||||
link: {
|
||||
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)
|
||||
@Values({
|
||||
label: "Dedicated Gateway",
|
||||
trueLabel: "Enable",
|
||||
falseLabel: "Disable",
|
||||
labelTKey: "Dedicated Gateway",
|
||||
trueLabelTKey: "Enable",
|
||||
falseLabelTKey: "Disable",
|
||||
})
|
||||
enableDedicatedGateway: boolean;
|
||||
|
||||
@Values({
|
||||
label: "SKUs",
|
||||
labelTKey: "SKUs",
|
||||
choices: getSkus,
|
||||
placeholder: "Select SKUs",
|
||||
placeholderTKey: "Select SKUs",
|
||||
})
|
||||
sku: ChoiceItem;
|
||||
|
||||
@Values({
|
||||
label: "Number of instances",
|
||||
labelTKey: "Number of instances",
|
||||
min: getInstancesMin,
|
||||
max: getInstancesMax,
|
||||
step: 1,
|
||||
|
@ -1,686 +1,21 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelfServeComponent message bar and spinner snapshots 1`] = `
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"overflowX": "auto",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"padding": 10,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledCommandBarBase
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"disabled": true,
|
||||
"iconProps": Object {
|
||||
"iconName": "Save",
|
||||
},
|
||||
"key": "save",
|
||||
"onClick": [Function],
|
||||
"split": true,
|
||||
"text": "Save",
|
||||
},
|
||||
Object {
|
||||
"disabled": true,
|
||||
"iconProps": Object {
|
||||
"iconName": "Undo",
|
||||
},
|
||||
"key": "discard",
|
||||
"onClick": [Function],
|
||||
"split": true,
|
||||
"text": "Discard",
|
||||
},
|
||||
Object {
|
||||
"disabled": false,
|
||||
"iconProps": Object {
|
||||
"iconName": "Refresh",
|
||||
},
|
||||
"key": "refresh",
|
||||
"onClick": [Function],
|
||||
"split": true,
|
||||
"text": "Refresh",
|
||||
},
|
||||
]
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"paddingLeft": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<StyledMessageBarBase
|
||||
messageBarType={0}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": 400,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
refresh performed successfully
|
||||
</StyledMessageBarBase>
|
||||
<StyledMessageBarBase
|
||||
messageBarType={0}
|
||||
onDismiss={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": 400,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
submitted successfully
|
||||
</StyledMessageBarBase>
|
||||
<SmartUiComponent
|
||||
currentValues={
|
||||
Map {
|
||||
"throughput" => Object {
|
||||
"value": 450,
|
||||
},
|
||||
"analyticalStore" => Object {
|
||||
"value": false,
|
||||
},
|
||||
"database" => Object {
|
||||
"value": "db2",
|
||||
},
|
||||
}
|
||||
}
|
||||
descriptor={
|
||||
Object {
|
||||
"initialize": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [],
|
||||
Array [],
|
||||
Array [],
|
||||
Array [],
|
||||
Array [],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"inputNames": Array [
|
||||
"throughput",
|
||||
"analyticalStore",
|
||||
"database",
|
||||
],
|
||||
"onRefresh": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [],
|
||||
Array [],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"onSave": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Map {
|
||||
"throughput" => Object {
|
||||
"value": 450,
|
||||
},
|
||||
"analyticalStore" => Object {
|
||||
"value": false,
|
||||
},
|
||||
"database" => Object {
|
||||
"value": "db2",
|
||||
},
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Map {
|
||||
"throughput" => Object {
|
||||
"value": 450,
|
||||
},
|
||||
"analyticalStore" => Object {
|
||||
"value": false,
|
||||
},
|
||||
"database" => Object {
|
||||
"value": "db2",
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"root": Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"id": "throughput",
|
||||
"info": undefined,
|
||||
"input": Object {
|
||||
"dataFieldName": "throughput",
|
||||
"defaultValue": 400,
|
||||
"label": "Throughput (input)",
|
||||
"max": 500,
|
||||
"min": 400,
|
||||
"placeholder": undefined,
|
||||
"step": 10,
|
||||
"type": "number",
|
||||
"uiType": "Spinner",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "containerId",
|
||||
"info": undefined,
|
||||
"input": Object {
|
||||
"dataFieldName": "containerId",
|
||||
"label": "Container id",
|
||||
"placeholder": undefined,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "analyticalStore",
|
||||
"info": undefined,
|
||||
"input": Object {
|
||||
"dataFieldName": "analyticalStore",
|
||||
"defaultValue": true,
|
||||
"falseLabel": "Disabled",
|
||||
"label": "Analytical Store",
|
||||
"placeholder": undefined,
|
||||
"trueLabel": "Enabled",
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "database",
|
||||
"info": undefined,
|
||||
"input": Object {
|
||||
"choices": Array [
|
||||
Object {
|
||||
"key": "db1",
|
||||
"label": "Database 1",
|
||||
},
|
||||
Object {
|
||||
"key": "db2",
|
||||
"label": "Database 2",
|
||||
},
|
||||
Object {
|
||||
"key": "db3",
|
||||
"label": "Database 3",
|
||||
},
|
||||
],
|
||||
"dataFieldName": "database",
|
||||
"defaultKey": "db2",
|
||||
"label": "Database",
|
||||
"placeholder": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
],
|
||||
"id": "root",
|
||||
"info": Object {
|
||||
"link": Object {
|
||||
"href": "https://aka.ms/azure-cosmos-db-pricing",
|
||||
"text": "More Details",
|
||||
},
|
||||
"message": "Start at $24/mo per database",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
disabled={true}
|
||||
onError={[Function]}
|
||||
onInputChange={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
<Translation>
|
||||
<Component />
|
||||
</Translation>
|
||||
`;
|
||||
|
||||
exports[`SelfServeComponent message bar and spinner snapshots 2`] = `
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"overflowX": "auto",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"padding": 10,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledCommandBarBase
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"disabled": true,
|
||||
"iconProps": Object {
|
||||
"iconName": "Save",
|
||||
},
|
||||
"key": "save",
|
||||
"onClick": [Function],
|
||||
"split": true,
|
||||
"text": "Save",
|
||||
},
|
||||
Object {
|
||||
"disabled": true,
|
||||
"iconProps": Object {
|
||||
"iconName": "Undo",
|
||||
},
|
||||
"key": "discard",
|
||||
"onClick": [Function],
|
||||
"split": true,
|
||||
"text": "Discard",
|
||||
},
|
||||
Object {
|
||||
"disabled": false,
|
||||
"iconProps": Object {
|
||||
"iconName": "Refresh",
|
||||
},
|
||||
"key": "refresh",
|
||||
"onClick": [Function],
|
||||
"split": true,
|
||||
"text": "Refresh",
|
||||
},
|
||||
]
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"paddingLeft": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<StyledMessageBarBase
|
||||
messageBarType={0}
|
||||
onDismiss={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": 400,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
submitted successfully
|
||||
</StyledMessageBarBase>
|
||||
<SmartUiComponent
|
||||
currentValues={
|
||||
Map {
|
||||
"throughput" => Object {
|
||||
"value": 450,
|
||||
},
|
||||
"analyticalStore" => Object {
|
||||
"value": false,
|
||||
},
|
||||
"database" => Object {
|
||||
"value": "db2",
|
||||
},
|
||||
}
|
||||
}
|
||||
descriptor={
|
||||
Object {
|
||||
"initialize": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [],
|
||||
Array [],
|
||||
Array [],
|
||||
Array [],
|
||||
Array [],
|
||||
Array [],
|
||||
Array [],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"inputNames": Array [
|
||||
"throughput",
|
||||
"analyticalStore",
|
||||
"database",
|
||||
],
|
||||
"onRefresh": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [],
|
||||
Array [],
|
||||
Array [],
|
||||
Array [],
|
||||
Array [],
|
||||
Array [],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"onSave": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Map {
|
||||
"throughput" => Object {
|
||||
"value": 450,
|
||||
},
|
||||
"analyticalStore" => Object {
|
||||
"value": false,
|
||||
},
|
||||
"database" => Object {
|
||||
"value": "db2",
|
||||
},
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Map {
|
||||
"throughput" => Object {
|
||||
"value": 450,
|
||||
},
|
||||
"analyticalStore" => Object {
|
||||
"value": false,
|
||||
},
|
||||
"database" => Object {
|
||||
"value": "db2",
|
||||
},
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Map {
|
||||
"throughput" => Object {
|
||||
"value": 450,
|
||||
},
|
||||
"analyticalStore" => Object {
|
||||
"value": false,
|
||||
},
|
||||
"database" => Object {
|
||||
"value": "db2",
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"root": Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"id": "throughput",
|
||||
"info": undefined,
|
||||
"input": Object {
|
||||
"dataFieldName": "throughput",
|
||||
"defaultValue": 400,
|
||||
"label": "Throughput (input)",
|
||||
"max": 500,
|
||||
"min": 400,
|
||||
"placeholder": undefined,
|
||||
"step": 10,
|
||||
"type": "number",
|
||||
"uiType": "Spinner",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "containerId",
|
||||
"info": undefined,
|
||||
"input": Object {
|
||||
"dataFieldName": "containerId",
|
||||
"label": "Container id",
|
||||
"placeholder": undefined,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "analyticalStore",
|
||||
"info": undefined,
|
||||
"input": Object {
|
||||
"dataFieldName": "analyticalStore",
|
||||
"defaultValue": true,
|
||||
"falseLabel": "Disabled",
|
||||
"label": "Analytical Store",
|
||||
"placeholder": undefined,
|
||||
"trueLabel": "Enabled",
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "database",
|
||||
"info": undefined,
|
||||
"input": Object {
|
||||
"choices": Array [
|
||||
Object {
|
||||
"key": "db1",
|
||||
"label": "Database 1",
|
||||
},
|
||||
Object {
|
||||
"key": "db2",
|
||||
"label": "Database 2",
|
||||
},
|
||||
Object {
|
||||
"key": "db3",
|
||||
"label": "Database 3",
|
||||
},
|
||||
],
|
||||
"dataFieldName": "database",
|
||||
"defaultKey": "db2",
|
||||
"label": "Database",
|
||||
"placeholder": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
],
|
||||
"id": "root",
|
||||
"info": Object {
|
||||
"link": Object {
|
||||
"href": "https://aka.ms/azure-cosmos-db-pricing",
|
||||
"text": "More Details",
|
||||
},
|
||||
"message": "Start at $24/mo per database",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
disabled={false}
|
||||
onError={[Function]}
|
||||
onInputChange={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
<Translation>
|
||||
<Component />
|
||||
</Translation>
|
||||
`;
|
||||
|
||||
exports[`SelfServeComponent message bar and spinner snapshots 3`] = `
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"overflowX": "auto",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"padding": 10,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledCommandBarBase
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"disabled": true,
|
||||
"iconProps": Object {
|
||||
"iconName": "Save",
|
||||
},
|
||||
"key": "save",
|
||||
"onClick": [Function],
|
||||
"split": true,
|
||||
"text": "Save",
|
||||
},
|
||||
Object {
|
||||
"disabled": true,
|
||||
"iconProps": Object {
|
||||
"iconName": "Undo",
|
||||
},
|
||||
"key": "discard",
|
||||
"onClick": [Function],
|
||||
"split": true,
|
||||
"text": "Discard",
|
||||
},
|
||||
Object {
|
||||
"disabled": true,
|
||||
"iconProps": Object {
|
||||
"iconName": "Refresh",
|
||||
},
|
||||
"key": "refresh",
|
||||
"onClick": [Function],
|
||||
"split": true,
|
||||
"text": "Refresh",
|
||||
},
|
||||
]
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"paddingLeft": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<StyledSpinnerBase
|
||||
size={3}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"height": "100%",
|
||||
"justifyContent": "center",
|
||||
"textAlign": "center",
|
||||
"width": "100%",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
<Translation>
|
||||
<Component />
|
||||
</Translation>
|
||||
`;
|
||||
|
||||
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`] = `
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"overflowX": "auto",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"padding": 10,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledCommandBarBase
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"disabled": true,
|
||||
"iconProps": Object {
|
||||
"iconName": "Save",
|
||||
},
|
||||
"key": "save",
|
||||
"onClick": [Function],
|
||||
"split": true,
|
||||
"text": "Save",
|
||||
},
|
||||
Object {
|
||||
"disabled": true,
|
||||
"iconProps": Object {
|
||||
"iconName": "Undo",
|
||||
},
|
||||
"key": "discard",
|
||||
"onClick": [Function],
|
||||
"split": true,
|
||||
"text": "Discard",
|
||||
},
|
||||
Object {
|
||||
"disabled": false,
|
||||
"iconProps": Object {
|
||||
"iconName": "Refresh",
|
||||
},
|
||||
"key": "refresh",
|
||||
"onClick": [Function],
|
||||
"split": true,
|
||||
"text": "Refresh",
|
||||
},
|
||||
]
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"paddingLeft": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<SmartUiComponent
|
||||
currentValues={
|
||||
Map {
|
||||
"throughput" => Object {
|
||||
"value": 450,
|
||||
},
|
||||
"analyticalStore" => Object {
|
||||
"value": false,
|
||||
},
|
||||
"database" => Object {
|
||||
"value": "db2",
|
||||
},
|
||||
}
|
||||
}
|
||||
descriptor={
|
||||
Object {
|
||||
"initialize": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"inputNames": Array [
|
||||
"throughput",
|
||||
"analyticalStore",
|
||||
"database",
|
||||
],
|
||||
"onRefresh": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [],
|
||||
],
|
||||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": Promise {},
|
||||
},
|
||||
],
|
||||
},
|
||||
"onSave": [MockFunction],
|
||||
"root": Object {
|
||||
"children": Array [
|
||||
Object {
|
||||
"id": "throughput",
|
||||
"info": undefined,
|
||||
"input": Object {
|
||||
"dataFieldName": "throughput",
|
||||
"defaultValue": 400,
|
||||
"label": "Throughput (input)",
|
||||
"max": 500,
|
||||
"min": 400,
|
||||
"placeholder": undefined,
|
||||
"step": 10,
|
||||
"type": "number",
|
||||
"uiType": "Spinner",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "containerId",
|
||||
"info": undefined,
|
||||
"input": Object {
|
||||
"dataFieldName": "containerId",
|
||||
"label": "Container id",
|
||||
"placeholder": undefined,
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "analyticalStore",
|
||||
"info": undefined,
|
||||
"input": Object {
|
||||
"dataFieldName": "analyticalStore",
|
||||
"defaultValue": true,
|
||||
"falseLabel": "Disabled",
|
||||
"label": "Analytical Store",
|
||||
"placeholder": undefined,
|
||||
"trueLabel": "Enabled",
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "database",
|
||||
"info": undefined,
|
||||
"input": Object {
|
||||
"choices": Array [
|
||||
Object {
|
||||
"key": "db1",
|
||||
"label": "Database 1",
|
||||
},
|
||||
Object {
|
||||
"key": "db2",
|
||||
"label": "Database 2",
|
||||
},
|
||||
Object {
|
||||
"key": "db3",
|
||||
"label": "Database 3",
|
||||
},
|
||||
],
|
||||
"dataFieldName": "database",
|
||||
"defaultKey": "db2",
|
||||
"label": "Database",
|
||||
"placeholder": undefined,
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
],
|
||||
"id": "root",
|
||||
"info": Object {
|
||||
"link": Object {
|
||||
"href": "https://aka.ms/azure-cosmos-db-pricing",
|
||||
"text": "More Details",
|
||||
},
|
||||
"message": "Start at $24/mo per database",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
disabled={false}
|
||||
onError={[Function]}
|
||||
onInputChange={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
</div>
|
||||
<Translation>
|
||||
<Component />
|
||||
</Translation>
|
||||
`;
|
||||
|
@ -41,12 +41,13 @@ export function useKnockoutExplorer(config: ConfigContext, explorerParams: Explo
|
||||
if (config) {
|
||||
if (config.platform === Platform.Hosted) {
|
||||
await configureHosted(config);
|
||||
applyExplorerBindings(explorer);
|
||||
} else if (config.platform === Platform.Emulator) {
|
||||
configureEmulator();
|
||||
applyExplorerBindings(explorer);
|
||||
} else if (config.platform === Platform.Portal) {
|
||||
configurePortal();
|
||||
}
|
||||
applyExplorerBindings(explorer);
|
||||
}
|
||||
};
|
||||
effect();
|
||||
@ -237,13 +238,14 @@ function configurePortal() {
|
||||
);
|
||||
console.dir(message);
|
||||
explorer.configure(message);
|
||||
applyExplorerBindings(explorer);
|
||||
}
|
||||
}
|
||||
|
||||
// In the Portal, configuration of Explorer happens via iframe message
|
||||
window.addEventListener(
|
||||
"message",
|
||||
(event) => {
|
||||
console.dir(event);
|
||||
if (isInvalidParentFrameOrigin(event)) {
|
||||
return;
|
||||
}
|
||||
@ -265,6 +267,7 @@ function configurePortal() {
|
||||
}
|
||||
|
||||
explorer.configure(inputs);
|
||||
applyExplorerBindings(explorer);
|
||||
}
|
||||
},
|
||||
false
|
||||
|
31
src/i18n.ts
Normal file
31
src/i18n.ts
Normal 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,
|
||||
},
|
||||
});
|
@ -37,6 +37,7 @@ export const uploadNotebookIfNotExist = async (frame: Frame, notebookName: strin
|
||||
|
||||
export const getNotebookNode = async (frame: Frame, uploadNotebookName: string): Promise<ElementHandle<Element>> => {
|
||||
const notebookResourceTree = await frame.waitForSelector(".notebookResourceTree");
|
||||
await frame.waitFor(RENDER_DELAY);
|
||||
let currentNotebookNode: ElementHandle<Element>;
|
||||
|
||||
const treeNodeHeaders = await notebookResourceTree.$$(".treeNodeHeader");
|
||||
|
51
utils/cleanupDBs.js
Normal file
51
utils/cleanupDBs.js
Normal 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);
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user