mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-22 10:21:37 +00:00
Added more selfserve changes (#443)
* exposed baselineValues * added getOnSaveNotification * disable UI when onSave is taking place * added optional polling * Added portal notifications * minor edits * added label for description * Added correlationids and polling of refresh * added label tooltip * removed ClassInfo decorator * Added dynamic decription * added info and warninf types for description * promise retry changes * compile errors fixed * merged sqlxEdits * undid sqlx changes * added completed notification * passed retryInterval in notif options * added polling on landing on the page * edits for error display * added link generation * addressed PR comments * modified test * fixed compilation error
This commit is contained in:
committed by
GitHub
parent
c1b74266eb
commit
ecdc41ada9
@@ -1,4 +1,4 @@
|
||||
import { ChoiceItem, Description, Info, InputType, NumberUiType, SmartUiInput } from "./SelfServeTypes";
|
||||
import { ChoiceItem, Description, Info, InputType, NumberUiType, SmartUiInput, RefreshParams } from "./SelfServeTypes";
|
||||
import { addPropertyToMap, DecoratorProperties, buildSmartUiDescriptor } from "./SelfServeUtils";
|
||||
|
||||
type ValueOf<T> = T[keyof T];
|
||||
@@ -33,7 +33,9 @@ export interface ChoiceInputOptions extends InputOptionsBase {
|
||||
}
|
||||
|
||||
export interface DescriptionDisplayOptions {
|
||||
labelTKey?: string;
|
||||
description?: (() => Promise<Description>) | Description;
|
||||
isDynamicDescription?: boolean;
|
||||
}
|
||||
|
||||
type InputOptions =
|
||||
@@ -56,7 +58,7 @@ const isChoiceInputOptions = (inputOptions: InputOptions): inputOptions is Choic
|
||||
};
|
||||
|
||||
const isDescriptionDisplayOptions = (inputOptions: InputOptions): inputOptions is DescriptionDisplayOptions => {
|
||||
return "description" in inputOptions;
|
||||
return "description" in inputOptions || "isDynamicDescription" in inputOptions;
|
||||
};
|
||||
|
||||
const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
|
||||
@@ -80,7 +82,11 @@ const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
|
||||
};
|
||||
|
||||
export const OnChange = (
|
||||
onChange: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>
|
||||
onChange: (
|
||||
newValue: InputType,
|
||||
currentState: Map<string, SmartUiInput>,
|
||||
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||
) => Map<string, SmartUiInput>
|
||||
): PropertyDecorator => {
|
||||
return addToMap({ name: "onChange", value: onChange });
|
||||
};
|
||||
@@ -111,7 +117,11 @@ export const Values = (inputOptions: InputOptions): PropertyDecorator => {
|
||||
{ name: "choices", value: inputOptions.choices }
|
||||
);
|
||||
} else if (isDescriptionDisplayOptions(inputOptions)) {
|
||||
return addToMap({ name: "description", value: inputOptions.description });
|
||||
return addToMap(
|
||||
{ name: "labelTKey", value: inputOptions.labelTKey },
|
||||
{ name: "description", value: inputOptions.description },
|
||||
{ name: "isDynamicDescription", value: inputOptions.isDynamicDescription }
|
||||
);
|
||||
} else {
|
||||
return addToMap(
|
||||
{ name: "labelTKey", value: inputOptions.labelTKey },
|
||||
@@ -126,8 +136,8 @@ export const IsDisplayable = (): ClassDecorator => {
|
||||
};
|
||||
};
|
||||
|
||||
export const ClassInfo = (info: (() => Promise<Info>) | Info): ClassDecorator => {
|
||||
export const RefreshOptions = (refreshParams: RefreshParams): ClassDecorator => {
|
||||
return (target) => {
|
||||
addPropertyToMap(target.prototype, "root", target.name, "info", info);
|
||||
addPropertyToMap(target.prototype, "root", target.name, "refreshParams", refreshParams);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -64,13 +64,20 @@ export const initialize = async (): Promise<InitializeResponse> => {
|
||||
};
|
||||
|
||||
export const onRefreshSelfServeExample = async (): Promise<RefreshResult> => {
|
||||
const refreshCountString = SessionStorageUtility.getEntry("refreshCount");
|
||||
const refreshCount = refreshCountString ? parseInt(refreshCountString) : 0;
|
||||
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroup = userContext.resourceGroup;
|
||||
const databaseAccountName = userContext.databaseAccount.name;
|
||||
const databaseAccountGetResults = await get(subscriptionId, resourceGroup, databaseAccountName);
|
||||
const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded";
|
||||
|
||||
const progressToBeSent = refreshCount % 5 === 0 ? isUpdateInProgress : true;
|
||||
SessionStorageUtility.setEntry("refreshCount", (refreshCount + 1).toString());
|
||||
|
||||
return {
|
||||
isUpdateInProgress: isUpdateInProgress,
|
||||
notificationMessage: "RefreshMessage",
|
||||
isUpdateInProgress: progressToBeSent,
|
||||
updateInProgressMessageTKey: "UpdateInProgressMessage",
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { PropertyInfo, OnChange, Values, IsDisplayable, ClassInfo } from "../Decorators";
|
||||
import { PropertyInfo, OnChange, Values, IsDisplayable, RefreshOptions } from "../Decorators";
|
||||
import {
|
||||
ChoiceItem,
|
||||
Description,
|
||||
DescriptionType,
|
||||
Info,
|
||||
InputType,
|
||||
NumberUiType,
|
||||
OnSaveResult,
|
||||
RefreshResult,
|
||||
SelfServeBaseClass,
|
||||
SelfServeNotification,
|
||||
SelfServeNotificationType,
|
||||
SmartUiInput,
|
||||
} from "../SelfServeTypes";
|
||||
import {
|
||||
@@ -27,16 +28,19 @@ const regionDropdownItems: ChoiceItem[] = [
|
||||
{ label: "East US 2", key: Regions.EastUS2 },
|
||||
];
|
||||
|
||||
const selfServeExampleInfo: Info = {
|
||||
messageTKey: "ClassInfo",
|
||||
};
|
||||
|
||||
const regionDropdownInfo: Info = {
|
||||
messageTKey: "RegionDropdownInfo",
|
||||
};
|
||||
|
||||
const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: InputType): Map<string, SmartUiInput> => {
|
||||
const onRegionsChange = (newValue: InputType, currentState: Map<string, SmartUiInput>): Map<string, SmartUiInput> => {
|
||||
currentState.set("regions", { value: newValue });
|
||||
|
||||
const currentRegionText = `current region selected is ${newValue}`;
|
||||
currentState.set("currentRegionText", {
|
||||
value: { textTKey: currentRegionText, type: DescriptionType.Text } as Description,
|
||||
hidden: false,
|
||||
});
|
||||
|
||||
const currentEnableLogging = currentState.get("enableLogging");
|
||||
if (newValue === Regions.NorthCentralUS) {
|
||||
currentState.set("enableLogging", { value: false, disabled: true });
|
||||
@@ -47,8 +51,8 @@ const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: Inpu
|
||||
};
|
||||
|
||||
const onEnableDbLevelThroughputChange = (
|
||||
currentState: Map<string, SmartUiInput>,
|
||||
newValue: InputType
|
||||
newValue: InputType,
|
||||
currentState: Map<string, SmartUiInput>
|
||||
): Map<string, SmartUiInput> => {
|
||||
currentState.set("enableDbLevelThroughput", { value: newValue });
|
||||
const currentDbThroughput = currentState.get("dbThroughput");
|
||||
@@ -57,9 +61,15 @@ const onEnableDbLevelThroughputChange = (
|
||||
return currentState;
|
||||
};
|
||||
|
||||
const validate = (currentvalues: Map<string, SmartUiInput>): void => {
|
||||
const validate = (
|
||||
currentvalues: Map<string, SmartUiInput>,
|
||||
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||
): void => {
|
||||
if (currentvalues.get("dbThroughput") === baselineValues.get("dbThroughput")) {
|
||||
throw new Error("DbThroughputValidationError");
|
||||
}
|
||||
if (!currentvalues.get("regions").value || !currentvalues.get("accountName").value) {
|
||||
throw new Error("ValidationError");
|
||||
throw new Error("RegionsAndAccountNameValidationError");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -86,12 +96,12 @@ const validate = (currentvalues: Map<string, SmartUiInput>): void => {
|
||||
*/
|
||||
@IsDisplayable()
|
||||
/*
|
||||
@ClassInfo()
|
||||
- optional
|
||||
- input: Info | () => Promise<Info>
|
||||
- role: Display an Info bar as the first element of the UI.
|
||||
@RefreshOptions()
|
||||
- role: Passes the refresh options to be used by the self serve model.
|
||||
- inputs:
|
||||
retryIntervalInMs - The time interval between refresh attempts when an update in ongoing.
|
||||
*/
|
||||
@ClassInfo(selfServeExampleInfo)
|
||||
@RefreshOptions({ retryIntervalInMs: 2000 })
|
||||
export default class SelfServeExample extends SelfServeBaseClass {
|
||||
/*
|
||||
onRefresh()
|
||||
@@ -109,18 +119,21 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
||||
|
||||
/*
|
||||
onSave()
|
||||
- input: (currentValues: Map<string, InputType>) => Promise<void>
|
||||
- input: (currentValues: Map<string, InputType>, baselineValues: ReadonlyMap<string, SmartUiInput>) => Promise<string>
|
||||
- role: Callback that is triggerred when the submit button is clicked. You should perform your rest API
|
||||
calls here using the data from the different inputs passed as a Map to this callback function.
|
||||
|
||||
In this example, the onSave callback simply sets the value for keys corresponding to the field name
|
||||
in the SessionStorage.
|
||||
- returns: SelfServeNotification -
|
||||
message: The message to be displayed in the message bar after the onSave is completed
|
||||
type: The type of message bar to be used (info, warning, error)
|
||||
in the SessionStorage. It uses the currentValues and baselineValues maps to perform custom validations
|
||||
as well.
|
||||
|
||||
- returns: The initialize, success and failure messages to be displayed in the Portal Notification blade after the operation is completed.
|
||||
*/
|
||||
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<SelfServeNotification> => {
|
||||
validate(currentValues);
|
||||
public onSave = async (
|
||||
currentValues: Map<string, SmartUiInput>,
|
||||
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||
): Promise<OnSaveResult> => {
|
||||
validate(currentValues, baselineValues);
|
||||
const regions = Regions[currentValues.get("regions")?.value as keyof typeof Regions];
|
||||
const enableLogging = currentValues.get("enableLogging")?.value as boolean;
|
||||
const accountName = currentValues.get("accountName")?.value as string;
|
||||
@@ -128,8 +141,48 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
||||
const enableDbLevelThroughput = currentValues.get("enableDbLevelThroughput")?.value as boolean;
|
||||
let dbThroughput = currentValues.get("dbThroughput")?.value as number;
|
||||
dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined;
|
||||
await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput);
|
||||
return { message: "SubmissionMessage", type: SelfServeNotificationType.info };
|
||||
try {
|
||||
await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput);
|
||||
if (currentValues.get("regions") === baselineValues.get("regions")) {
|
||||
return {
|
||||
operationStatusUrl: undefined,
|
||||
portalNotification: {
|
||||
initialize: {
|
||||
titleTKey: "SubmissionMessageSuccessTitle",
|
||||
messageTKey: "SubmissionMessageForSameRegionText",
|
||||
},
|
||||
success: {
|
||||
titleTKey: "UpdateCompletedMessageTitle",
|
||||
messageTKey: "UpdateCompletedMessageText",
|
||||
},
|
||||
failure: {
|
||||
titleTKey: "SubmissionMessageErrorTitle",
|
||||
messageTKey: "SubmissionMessageErrorText",
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
operationStatusUrl: undefined,
|
||||
portalNotification: {
|
||||
initialize: {
|
||||
titleTKey: "SubmissionMessageSuccessTitle",
|
||||
messageTKey: "SubmissionMessageForNewRegionText",
|
||||
},
|
||||
success: {
|
||||
titleTKey: "UpdateCompletedMessageTitle",
|
||||
messageTKey: "UpdateCompletedMessageText",
|
||||
},
|
||||
failure: {
|
||||
titleTKey: "SubmissionMessageErrorTitle",
|
||||
messageTKey: "SubmissionMessageErrorText",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error("OnSaveFailureMessage");
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -150,6 +203,11 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
||||
public initialize = async (): Promise<Map<string, SmartUiInput>> => {
|
||||
const initializeResponse = await initialize();
|
||||
const defaults = new Map<string, SmartUiInput>();
|
||||
const currentRegionText = `current region selected is ${initializeResponse.regions}`;
|
||||
defaults.set("currentRegionText", {
|
||||
value: { textTKey: currentRegionText, type: DescriptionType.Text } as Description,
|
||||
hidden: false,
|
||||
});
|
||||
defaults.set("regions", { value: initializeResponse.regions });
|
||||
defaults.set("enableLogging", { value: initializeResponse.enableLogging });
|
||||
const accountName = initializeResponse.accountName;
|
||||
@@ -172,15 +230,24 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
||||
e) Text (with optional hyperlink) for descriptions
|
||||
*/
|
||||
@Values({
|
||||
labelTKey: "DescriptionLabel",
|
||||
description: {
|
||||
textTKey: "DescriptionText",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
||||
href: "https://aka.ms/cosmos-create-account-portal",
|
||||
textTKey: "DecriptionLinkText",
|
||||
},
|
||||
},
|
||||
})
|
||||
description: string;
|
||||
|
||||
@Values({
|
||||
labelTKey: "Current Region",
|
||||
isDynamicDescription: true,
|
||||
})
|
||||
currentRegionText: string;
|
||||
|
||||
/*
|
||||
@PropertyInfo()
|
||||
- optional
|
||||
@@ -192,8 +259,8 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
||||
/*
|
||||
@OnChange()
|
||||
- optional
|
||||
- input: (currentValues: Map<string, InputType>, newValue: InputType) => Map<string, InputType>
|
||||
- role: Takes a Map of current values and the newValue for this property as inputs. This is called when a property,
|
||||
- input: (currentValues: Map<string, InputType>, newValue: InputType, baselineValues: ReadonlyMap<string, SmartUiInput>) => Map<string, InputType>
|
||||
- role: Takes a Map of current values, the newValue for this property and a ReadonlyMap of baselineValues as inputs. This is called when a property,
|
||||
say prop1, changes its value in the UI. This can be used to
|
||||
a) Change the value (and reflect it in the UI) for prop2 based on prop1.
|
||||
b) Change the visibility for prop2 in the UI, based on prop1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import { SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent";
|
||||
import { NumberUiType, SelfServeDescriptor, SelfServeNotificationType, SmartUiInput } from "./SelfServeTypes";
|
||||
import { NumberUiType, OnSaveResult, SelfServeDescriptor, SmartUiInput } from "./SelfServeTypes";
|
||||
|
||||
describe("SelfServeComponent", () => {
|
||||
const defaultValues = new Map<string, SmartUiInput>([
|
||||
@@ -17,13 +17,20 @@ describe("SelfServeComponent", () => {
|
||||
|
||||
const initializeMock = jest.fn(async () => new Map(defaultValues));
|
||||
const onSaveMock = jest.fn(async () => {
|
||||
return { message: "submitted successfully", type: SelfServeNotificationType.info };
|
||||
return {
|
||||
operationStatusUrl: undefined,
|
||||
} as OnSaveResult;
|
||||
});
|
||||
const refreshResult = {
|
||||
isUpdateInProgress: false,
|
||||
updateInProgressMessageTKey: "refresh performed successfully",
|
||||
};
|
||||
|
||||
const onRefreshMock = jest.fn(async () => {
|
||||
return { isUpdateInProgress: false, notificationMessage: "refresh performed successfully" };
|
||||
return { ...refreshResult };
|
||||
});
|
||||
const onRefreshIsUpdatingMock = jest.fn(async () => {
|
||||
return { isUpdateInProgress: true, notificationMessage: "refresh performed successfully" };
|
||||
return { ...refreshResult, isUpdateInProgress: true };
|
||||
});
|
||||
|
||||
const exampleData: SelfServeDescriptor = {
|
||||
@@ -136,16 +143,15 @@ describe("SelfServeComponent", () => {
|
||||
wrapper.update();
|
||||
state = wrapper.state() as SelfServeComponentState;
|
||||
isEqual(state.baselineValues, updatedValues);
|
||||
selfServeComponent.resetBaselineValues();
|
||||
selfServeComponent.updateBaselineValues();
|
||||
state = wrapper.state() as SelfServeComponentState;
|
||||
isEqual(state.baselineValues, defaultValues);
|
||||
isEqual(state.currentValues, state.baselineValues);
|
||||
|
||||
// clicking refresh calls onRefresh. If component is not updating, it calls initialize() as well
|
||||
// clicking refresh calls onRefresh.
|
||||
selfServeComponent.onRefreshClicked();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(onRefreshMock).toHaveBeenCalledTimes(2);
|
||||
expect(initializeMock).toHaveBeenCalledTimes(2);
|
||||
|
||||
selfServeComponent.onSaveButtonClick();
|
||||
expect(onSaveMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -15,20 +15,45 @@ import {
|
||||
InputType,
|
||||
RefreshResult,
|
||||
SelfServeDescriptor,
|
||||
SelfServeNotification,
|
||||
SmartUiInput,
|
||||
DescriptionDisplay,
|
||||
StringInput,
|
||||
NumberInput,
|
||||
BooleanInput,
|
||||
ChoiceInput,
|
||||
SelfServeNotificationType,
|
||||
} 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";
|
||||
import { sendMessage } from "../Common/MessageHandler";
|
||||
import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts";
|
||||
import promiseRetry, { AbortError } from "p-retry";
|
||||
|
||||
interface SelfServeNotification {
|
||||
message: string;
|
||||
type: MessageBarType;
|
||||
isCancellable: boolean;
|
||||
}
|
||||
|
||||
interface PortalNotificationContent {
|
||||
retryIntervalInMs: number;
|
||||
operationStatusUrl: string;
|
||||
portalNotification?: {
|
||||
initialize: {
|
||||
title: string;
|
||||
message: string;
|
||||
};
|
||||
success: {
|
||||
title: string;
|
||||
message: string;
|
||||
};
|
||||
failure: {
|
||||
title: string;
|
||||
message: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface SelfServeComponentProps {
|
||||
descriptor: SelfServeDescriptor;
|
||||
@@ -39,17 +64,26 @@ export interface SelfServeComponentState {
|
||||
currentValues: Map<string, SmartUiInput>;
|
||||
baselineValues: Map<string, SmartUiInput>;
|
||||
isInitializing: boolean;
|
||||
isSaving: boolean;
|
||||
hasErrors: boolean;
|
||||
compileErrorMessage: string;
|
||||
notification: SelfServeNotification;
|
||||
refreshResult: RefreshResult;
|
||||
notification: SelfServeNotification;
|
||||
}
|
||||
|
||||
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
|
||||
private static readonly defaultRetryIntervalInMs = 30000;
|
||||
private smartUiGeneratorClassName: string;
|
||||
private retryIntervalInMs: number;
|
||||
private retryOptions: promiseRetry.Options;
|
||||
private translationFunction: TFunction;
|
||||
|
||||
componentDidMount(): void {
|
||||
this.performRefresh();
|
||||
this.performRefresh().then(() => {
|
||||
if (this.state.refreshResult?.isUpdateInProgress) {
|
||||
promiseRetry(() => this.pollRefresh(), this.retryOptions);
|
||||
}
|
||||
});
|
||||
this.initializeSmartUiComponent();
|
||||
}
|
||||
|
||||
@@ -60,12 +94,18 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
currentValues: new Map(),
|
||||
baselineValues: new Map(),
|
||||
isInitializing: true,
|
||||
isSaving: false,
|
||||
hasErrors: false,
|
||||
compileErrorMessage: undefined,
|
||||
notification: undefined,
|
||||
refreshResult: undefined,
|
||||
notification: undefined,
|
||||
};
|
||||
this.smartUiGeneratorClassName = this.props.descriptor.root.id;
|
||||
this.retryIntervalInMs = this.props.descriptor.refreshParams?.retryIntervalInMs;
|
||||
if (!this.retryIntervalInMs) {
|
||||
this.retryIntervalInMs = SelfServeComponent.defaultRetryIntervalInMs;
|
||||
}
|
||||
this.retryOptions = { forever: true, maxTimeout: this.retryIntervalInMs, minTimeout: this.retryIntervalInMs };
|
||||
}
|
||||
|
||||
private onError = (hasErrors: boolean): void => {
|
||||
@@ -109,7 +149,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
this.setState({ currentValues, baselineValues });
|
||||
};
|
||||
|
||||
public resetBaselineValues = (): void => {
|
||||
public updateBaselineValues = (): void => {
|
||||
const currentValues = this.state.currentValues;
|
||||
let baselineValues = this.state.baselineValues;
|
||||
for (const key of currentValues.keys()) {
|
||||
@@ -204,7 +244,11 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
|
||||
private onInputChange = (input: AnyDisplay, newValue: InputType) => {
|
||||
if (input.onChange) {
|
||||
const newValues = input.onChange(this.state.currentValues, newValue);
|
||||
const newValues = input.onChange(
|
||||
newValue,
|
||||
this.state.currentValues,
|
||||
this.state.baselineValues as ReadonlyMap<string, SmartUiInput>
|
||||
);
|
||||
this.setState({ currentValues: newValues });
|
||||
} else {
|
||||
const dataFieldName = input.dataFieldName;
|
||||
@@ -215,42 +259,60 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
}
|
||||
};
|
||||
|
||||
public performSave = async (): Promise<void> => {
|
||||
this.setState({ isSaving: true, notification: undefined });
|
||||
try {
|
||||
const onSaveResult = await this.props.descriptor.onSave(
|
||||
this.state.currentValues,
|
||||
this.state.baselineValues as ReadonlyMap<string, SmartUiInput>
|
||||
);
|
||||
if (onSaveResult.portalNotification) {
|
||||
const requestInitializedPortalNotification = onSaveResult.portalNotification.initialize;
|
||||
const requestSucceededPortalNotification = onSaveResult.portalNotification.success;
|
||||
const requestFailedPortalNotification = onSaveResult.portalNotification.failure;
|
||||
|
||||
this.sendNotificationMessage({
|
||||
retryIntervalInMs: this.retryIntervalInMs,
|
||||
operationStatusUrl: onSaveResult.operationStatusUrl,
|
||||
portalNotification: {
|
||||
initialize: {
|
||||
title: this.getTranslation(requestInitializedPortalNotification.titleTKey),
|
||||
message: this.getTranslation(requestInitializedPortalNotification.messageTKey),
|
||||
},
|
||||
success: {
|
||||
title: this.getTranslation(requestSucceededPortalNotification.titleTKey),
|
||||
message: this.getTranslation(requestSucceededPortalNotification.messageTKey),
|
||||
},
|
||||
failure: {
|
||||
title: this.getTranslation(requestFailedPortalNotification.titleTKey),
|
||||
message: this.getTranslation(requestFailedPortalNotification.messageTKey),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
promiseRetry(() => this.pollRefresh(), this.retryOptions);
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
notification: {
|
||||
type: MessageBarType.error,
|
||||
isCancellable: true,
|
||||
message: this.getTranslation(error.message),
|
||||
},
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
this.setState({ isSaving: false });
|
||||
}
|
||||
await this.onRefreshClicked();
|
||||
this.updateBaselineValues();
|
||||
};
|
||||
|
||||
public onSaveButtonClick = (): void => {
|
||||
const onSavePromise = this.props.descriptor.onSave(this.state.currentValues);
|
||||
onSavePromise.catch((error) => {
|
||||
this.setState({
|
||||
notification: {
|
||||
message: `${error.message}`,
|
||||
type: SelfServeNotificationType.error,
|
||||
},
|
||||
});
|
||||
});
|
||||
onSavePromise.then((notification: SelfServeNotification) => {
|
||||
this.setState({
|
||||
notification: {
|
||||
message: notification.message,
|
||||
type: notification.type,
|
||||
},
|
||||
});
|
||||
this.resetBaselineValues();
|
||||
this.onRefreshClicked();
|
||||
});
|
||||
this.performSave();
|
||||
};
|
||||
|
||||
public isDiscardButtonDisabled = (): boolean => {
|
||||
for (const key of this.state.currentValues.keys()) {
|
||||
const currentValue = JSON.stringify(this.state.currentValues.get(key));
|
||||
const baselineValue = JSON.stringify(this.state.baselineValues.get(key));
|
||||
|
||||
if (currentValue !== baselineValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
public isSaveButtonDisabled = (): boolean => {
|
||||
if (this.state.hasErrors) {
|
||||
if (this.state.isSaving) {
|
||||
return true;
|
||||
}
|
||||
for (const key of this.state.currentValues.keys()) {
|
||||
@@ -264,38 +326,84 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
return true;
|
||||
};
|
||||
|
||||
private performRefresh = async (): Promise<RefreshResult> => {
|
||||
public isSaveButtonDisabled = (): boolean => {
|
||||
if (this.state.hasErrors || this.state.isSaving) {
|
||||
return true;
|
||||
}
|
||||
for (const key of this.state.currentValues.keys()) {
|
||||
const currentValue = JSON.stringify(this.state.currentValues.get(key));
|
||||
const baselineValue = JSON.stringify(this.state.baselineValues.get(key));
|
||||
|
||||
if (currentValue !== baselineValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private performRefresh = async (): Promise<void> => {
|
||||
const refreshResult = await this.props.descriptor.onRefresh();
|
||||
this.setState({ refreshResult: { ...refreshResult } });
|
||||
return refreshResult;
|
||||
let updateInProgressNotification: SelfServeNotification;
|
||||
if (this.state.refreshResult?.isUpdateInProgress && !refreshResult.isUpdateInProgress) {
|
||||
await this.initializeSmartUiComponent();
|
||||
}
|
||||
if (refreshResult.isUpdateInProgress) {
|
||||
updateInProgressNotification = {
|
||||
type: MessageBarType.info,
|
||||
isCancellable: false,
|
||||
message: this.getTranslation(refreshResult.updateInProgressMessageTKey),
|
||||
};
|
||||
}
|
||||
this.setState({
|
||||
refreshResult: { ...refreshResult },
|
||||
notification: updateInProgressNotification,
|
||||
});
|
||||
};
|
||||
|
||||
public onRefreshClicked = async (): Promise<void> => {
|
||||
this.setState({ isInitializing: true });
|
||||
const refreshResult = await this.performRefresh();
|
||||
if (!refreshResult.isUpdateInProgress) {
|
||||
this.initializeSmartUiComponent();
|
||||
}
|
||||
await this.performRefresh();
|
||||
this.setState({ isInitializing: false });
|
||||
};
|
||||
|
||||
public getCommonTranslation = (translationFunction: TFunction, key: string): string => {
|
||||
return translationFunction(`Common.${key}`);
|
||||
public pollRefresh = async (): Promise<void> => {
|
||||
try {
|
||||
await this.performRefresh();
|
||||
} catch (error) {
|
||||
throw new AbortError(error);
|
||||
}
|
||||
const refreshResult = this.state.refreshResult;
|
||||
if (refreshResult.isUpdateInProgress) {
|
||||
throw new Error("update in progress. retrying ...");
|
||||
}
|
||||
};
|
||||
|
||||
private getCommandBarItems = (translate: TFunction): ICommandBarItemProps[] => {
|
||||
public getCommonTranslation = (key: string): string => {
|
||||
return this.getTranslation(key, "Common");
|
||||
};
|
||||
|
||||
private getTranslation = (messageKey: string, prefix = `${this.smartUiGeneratorClassName}`): string => {
|
||||
const translationKey = `${prefix}.${messageKey}`;
|
||||
const translation = this.translationFunction ? this.translationFunction(translationKey) : messageKey;
|
||||
if (translation === translationKey) {
|
||||
return messageKey;
|
||||
}
|
||||
return translation;
|
||||
};
|
||||
|
||||
private getCommandBarItems = (): ICommandBarItemProps[] => {
|
||||
return [
|
||||
{
|
||||
key: "save",
|
||||
text: this.getCommonTranslation(translate, "Save"),
|
||||
text: this.getCommonTranslation("Save"),
|
||||
iconProps: { iconName: "Save" },
|
||||
split: true,
|
||||
disabled: this.isSaveButtonDisabled(),
|
||||
onClick: this.onSaveButtonClick,
|
||||
onClick: () => this.onSaveButtonClick(),
|
||||
},
|
||||
{
|
||||
key: "discard",
|
||||
text: this.getCommonTranslation(translate, "Discard"),
|
||||
text: this.getCommonTranslation("Discard"),
|
||||
iconProps: { iconName: "Undo" },
|
||||
split: true,
|
||||
disabled: this.isDiscardButtonDisabled(),
|
||||
@@ -305,7 +413,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
},
|
||||
{
|
||||
key: "refresh",
|
||||
text: this.getCommonTranslation(translate, "Refresh"),
|
||||
text: this.getCommonTranslation("Refresh"),
|
||||
disabled: this.state.isInitializing,
|
||||
iconProps: { iconName: "Refresh" },
|
||||
split: true,
|
||||
@@ -316,12 +424,11 @@ 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;
|
||||
private sendNotificationMessage = (portalNotificationContent: PortalNotificationContent): void => {
|
||||
sendMessage({
|
||||
type: SelfServeMessageTypes.Notification,
|
||||
data: { portalNotificationContent },
|
||||
});
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
@@ -332,14 +439,14 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
return (
|
||||
<Translation>
|
||||
{(translate) => {
|
||||
const getTranslation = (key: string): string => {
|
||||
return translate(`${this.smartUiGeneratorClassName}.${key}`);
|
||||
};
|
||||
if (!this.translationFunction) {
|
||||
this.translationFunction = translate;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ overflowX: "auto" }}>
|
||||
<Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}>
|
||||
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems(translate)} />
|
||||
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems()} />
|
||||
{this.state.isInitializing ? (
|
||||
<Spinner
|
||||
size={SpinnerSize.large}
|
||||
@@ -347,27 +454,25 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{this.state.refreshResult?.isUpdateInProgress && (
|
||||
<MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}>
|
||||
{getTranslation(this.state.refreshResult.notificationMessage)}
|
||||
</MessageBar>
|
||||
)}
|
||||
{this.state.notification && (
|
||||
<MessageBar
|
||||
messageBarType={getMessageBarType(this.state.notification.type)}
|
||||
styles={{ root: { width: 400 } }}
|
||||
onDismiss={() => this.setState({ notification: undefined })}
|
||||
messageBarType={this.state.notification.type}
|
||||
onDismiss={
|
||||
this.state.notification.isCancellable
|
||||
? () => this.setState({ notification: undefined })
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{this.getNotificationMessageTranslation(getTranslation, this.state.notification.message)}
|
||||
{this.state.notification.message}
|
||||
</MessageBar>
|
||||
)}
|
||||
<SmartUiComponent
|
||||
disabled={this.state.refreshResult?.isUpdateInProgress}
|
||||
disabled={this.state.refreshResult?.isUpdateInProgress || this.state.isSaving}
|
||||
descriptor={this.state.root as SmartUiDescriptor}
|
||||
currentValues={this.state.currentValues}
|
||||
onInputChange={this.onInputChange}
|
||||
onError={this.onError}
|
||||
getTranslation={getTranslation}
|
||||
getTranslation={this.getTranslation}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,11 @@ interface BaseInput {
|
||||
errorMessage?: string;
|
||||
type: InputTypeValue;
|
||||
labelTKey?: (() => Promise<string>) | string;
|
||||
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>;
|
||||
onChange?: (
|
||||
newValue: InputType,
|
||||
currentState: Map<string, SmartUiInput>,
|
||||
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||
) => Map<string, SmartUiInput>;
|
||||
placeholderTKey?: (() => Promise<string>) | string;
|
||||
}
|
||||
|
||||
@@ -44,16 +48,23 @@ export interface Node {
|
||||
export interface SelfServeDescriptor {
|
||||
root: Node;
|
||||
initialize?: () => Promise<Map<string, SmartUiInput>>;
|
||||
onSave?: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>;
|
||||
onSave?: (
|
||||
currentValues: Map<string, SmartUiInput>,
|
||||
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||
) => Promise<OnSaveResult>;
|
||||
inputNames?: string[];
|
||||
onRefresh?: () => Promise<RefreshResult>;
|
||||
refreshParams?: RefreshParams;
|
||||
}
|
||||
|
||||
export type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
|
||||
|
||||
export abstract class SelfServeBaseClass {
|
||||
public abstract initialize: () => Promise<Map<string, SmartUiInput>>;
|
||||
public abstract onSave: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>;
|
||||
public abstract onSave: (
|
||||
currentValues: Map<string, SmartUiInput>,
|
||||
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||
) => Promise<OnSaveResult>;
|
||||
public abstract onRefresh: () => Promise<RefreshResult>;
|
||||
|
||||
public toSelfServeDescriptor(): SelfServeDescriptor {
|
||||
@@ -70,7 +81,7 @@ export abstract class SelfServeBaseClass {
|
||||
throw new Error(`onRefresh() was not declared for the class '${className}'`);
|
||||
}
|
||||
if (!selfServeDescriptor?.root) {
|
||||
throw new Error(`@SmartUi decorator was not declared for the class '${className}'`);
|
||||
throw new Error(`@IsDisplayable decorator was not declared for the class '${className}'`);
|
||||
}
|
||||
|
||||
selfServeDescriptor.initialize = this.initialize;
|
||||
@@ -89,7 +100,7 @@ export enum NumberUiType {
|
||||
|
||||
export type ChoiceItem = { label: string; key: string };
|
||||
|
||||
export type InputType = number | string | boolean | ChoiceItem;
|
||||
export type InputType = number | string | boolean | ChoiceItem | Description;
|
||||
|
||||
export interface Info {
|
||||
messageTKey: string;
|
||||
@@ -99,8 +110,15 @@ export interface Info {
|
||||
};
|
||||
}
|
||||
|
||||
export enum DescriptionType {
|
||||
Text,
|
||||
InfoMessageBar,
|
||||
WarningMessageBar,
|
||||
}
|
||||
|
||||
export interface Description {
|
||||
textTKey: string;
|
||||
type: DescriptionType;
|
||||
link?: {
|
||||
href: string;
|
||||
textTKey: string;
|
||||
@@ -113,18 +131,29 @@ export interface SmartUiInput {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export enum SelfServeNotificationType {
|
||||
info = "info",
|
||||
warning = "warning",
|
||||
error = "error",
|
||||
}
|
||||
|
||||
export interface SelfServeNotification {
|
||||
message: string;
|
||||
type: SelfServeNotificationType;
|
||||
export interface OnSaveResult {
|
||||
operationStatusUrl: string;
|
||||
portalNotification?: {
|
||||
initialize: {
|
||||
titleTKey: string;
|
||||
messageTKey: string;
|
||||
};
|
||||
success: {
|
||||
titleTKey: string;
|
||||
messageTKey: string;
|
||||
};
|
||||
failure: {
|
||||
titleTKey: string;
|
||||
messageTKey: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface RefreshResult {
|
||||
isUpdateInProgress: boolean;
|
||||
notificationMessage: string;
|
||||
updateInProgressMessageTKey: string;
|
||||
}
|
||||
|
||||
export interface RefreshParams {
|
||||
retryIntervalInMs: number;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { NumberUiType, RefreshResult, SelfServeBaseClass, SelfServeNotification, SmartUiInput } from "./SelfServeTypes";
|
||||
import { NumberUiType, OnSaveResult, RefreshResult, SelfServeBaseClass, SmartUiInput } from "./SelfServeTypes";
|
||||
import { DecoratorProperties, mapToSmartUiDescriptor, updateContextWithDecorator } from "./SelfServeUtils";
|
||||
|
||||
describe("SelfServeUtils", () => {
|
||||
it("initialize should be declared for self serve classes", () => {
|
||||
class Test extends SelfServeBaseClass {
|
||||
public initialize: () => Promise<Map<string, SmartUiInput>>;
|
||||
public onSave: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>;
|
||||
public onSave: (currentValues: Map<string, SmartUiInput>) => Promise<OnSaveResult>;
|
||||
public onRefresh: () => Promise<RefreshResult>;
|
||||
}
|
||||
expect(() => new Test().toSelfServeDescriptor()).toThrow("initialize() was not declared for the class 'Test'");
|
||||
@@ -14,7 +14,7 @@ describe("SelfServeUtils", () => {
|
||||
it("onSave should be declared for self serve classes", () => {
|
||||
class Test extends SelfServeBaseClass {
|
||||
public initialize = jest.fn();
|
||||
public onSave: () => Promise<SelfServeNotification>;
|
||||
public onSave: () => Promise<OnSaveResult>;
|
||||
public onRefresh: () => Promise<RefreshResult>;
|
||||
}
|
||||
expect(() => new Test().toSelfServeDescriptor()).toThrow("onSave() was not declared for the class 'Test'");
|
||||
@@ -29,14 +29,14 @@ describe("SelfServeUtils", () => {
|
||||
expect(() => new Test().toSelfServeDescriptor()).toThrow("onRefresh() was not declared for the class 'Test'");
|
||||
});
|
||||
|
||||
it("@SmartUi decorator must be present for self serve classes", () => {
|
||||
it("@IsDisplayable decorator must be present for self serve classes", () => {
|
||||
class Test extends SelfServeBaseClass {
|
||||
public initialize = jest.fn();
|
||||
public onSave = jest.fn();
|
||||
public onRefresh = jest.fn();
|
||||
}
|
||||
expect(() => new Test().toSelfServeDescriptor()).toThrow(
|
||||
"@SmartUi decorator was not declared for the class 'Test'"
|
||||
"@IsDisplayable decorator was not declared for the class 'Test'"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { MessageBarType } from "office-ui-fabric-react";
|
||||
import "reflect-metadata";
|
||||
import {
|
||||
Node,
|
||||
@@ -15,8 +14,9 @@ import {
|
||||
SelfServeDescriptor,
|
||||
SmartUiInput,
|
||||
StringInput,
|
||||
SelfServeNotificationType,
|
||||
RefreshParams,
|
||||
} from "./SelfServeTypes";
|
||||
import { userContext } from "../UserContext";
|
||||
|
||||
export enum SelfServeType {
|
||||
// No self serve type passed, launch explorer
|
||||
@@ -28,6 +28,14 @@ export enum SelfServeType {
|
||||
sqlx = "sqlx",
|
||||
}
|
||||
|
||||
export enum BladeType {
|
||||
SqlKeys = "keys",
|
||||
MongoKeys = "mongoDbKeys",
|
||||
CassandraKeys = "cassandraDbKeys",
|
||||
GremlinKeys = "keys",
|
||||
TableKeys = "tableKeys",
|
||||
}
|
||||
|
||||
export interface DecoratorProperties {
|
||||
id: string;
|
||||
info?: (() => Promise<Info>) | Info;
|
||||
@@ -44,9 +52,13 @@ export interface DecoratorProperties {
|
||||
uiType?: string;
|
||||
errorMessage?: string;
|
||||
description?: (() => Promise<Description>) | Description;
|
||||
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>;
|
||||
onSave?: (currentValues: Map<string, SmartUiInput>) => Promise<void>;
|
||||
initialize?: () => Promise<Map<string, SmartUiInput>>;
|
||||
isDynamicDescription?: boolean;
|
||||
refreshParams?: RefreshParams;
|
||||
onChange?: (
|
||||
newValue: InputType,
|
||||
currentState: Map<string, SmartUiInput>,
|
||||
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||
) => Map<string, SmartUiInput>;
|
||||
}
|
||||
|
||||
const setValue = <T extends keyof DecoratorProperties, K extends DecoratorProperties[T]>(
|
||||
@@ -83,7 +95,7 @@ export const updateContextWithDecorator = <T extends keyof DecoratorProperties,
|
||||
descriptorValue: K
|
||||
): void => {
|
||||
if (!(context instanceof Map)) {
|
||||
throw new Error(`@SmartUi should be the first decorator for the class '${className}'.`);
|
||||
throw new Error(`@IsDisplayable should be the first decorator for the class '${className}'.`);
|
||||
}
|
||||
|
||||
const propertyObject = context.get(propertyName) ?? { id: propertyName };
|
||||
@@ -108,16 +120,17 @@ export const mapToSmartUiDescriptor = (
|
||||
className: string,
|
||||
context: Map<string, DecoratorProperties>
|
||||
): SelfServeDescriptor => {
|
||||
const inputNames: string[] = [];
|
||||
const root = context.get("root");
|
||||
context.delete("root");
|
||||
const inputNames: string[] = [];
|
||||
|
||||
const smartUiDescriptor: SelfServeDescriptor = {
|
||||
root: {
|
||||
id: className,
|
||||
info: root?.info,
|
||||
info: undefined,
|
||||
children: [],
|
||||
},
|
||||
refreshParams: root?.refreshParams,
|
||||
};
|
||||
|
||||
while (context.size > 0) {
|
||||
@@ -155,7 +168,10 @@ const getInput = (value: DecoratorProperties): AnyDisplay => {
|
||||
}
|
||||
return value as NumberInput;
|
||||
case "string":
|
||||
if (value.description) {
|
||||
if (value.description || value.isDynamicDescription) {
|
||||
if (value.description && value.isDynamicDescription) {
|
||||
value.errorMessage = `dynamic descriptions should not have defaults set here.`;
|
||||
}
|
||||
return value as DescriptionDisplay;
|
||||
}
|
||||
if (!value.labelTKey) {
|
||||
@@ -175,13 +191,9 @@ const getInput = (value: DecoratorProperties): AnyDisplay => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getMessageBarType = (type: SelfServeNotificationType): MessageBarType => {
|
||||
switch (type) {
|
||||
case SelfServeNotificationType.info:
|
||||
return MessageBarType.info;
|
||||
case SelfServeNotificationType.warning:
|
||||
return MessageBarType.warning;
|
||||
case SelfServeNotificationType.error:
|
||||
return MessageBarType.error;
|
||||
}
|
||||
export const generateBladeLink = (blade: BladeType): string => {
|
||||
const subscriptionId = userContext.subscriptionId;
|
||||
const resourceGroupName = userContext.resourceGroup;
|
||||
const databaseAccountName = userContext.databaseAccount.name;
|
||||
return `www.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}/${blade}`;
|
||||
};
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { IsDisplayable, OnChange, Values } from "../Decorators";
|
||||
import {
|
||||
ChoiceItem,
|
||||
DescriptionType,
|
||||
InputType,
|
||||
NumberUiType,
|
||||
OnSaveResult,
|
||||
RefreshResult,
|
||||
SelfServeBaseClass,
|
||||
SelfServeNotification,
|
||||
SmartUiInput,
|
||||
} from "../SelfServeTypes";
|
||||
import { refreshDedicatedGatewayProvisioning } from "./SqlX.rp";
|
||||
|
||||
const onEnableDedicatedGatewayChange = (
|
||||
currentState: Map<string, SmartUiInput>,
|
||||
newValue: InputType
|
||||
newValue: InputType,
|
||||
currentState: Map<string, SmartUiInput>
|
||||
): Map<string, SmartUiInput> => {
|
||||
const sku = currentState.get("sku");
|
||||
const instances = currentState.get("instances");
|
||||
@@ -49,7 +50,7 @@ export default class SqlX extends SelfServeBaseClass {
|
||||
return refreshDedicatedGatewayProvisioning();
|
||||
};
|
||||
|
||||
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<SelfServeNotification> => {
|
||||
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<OnSaveResult> => {
|
||||
validate(currentValues);
|
||||
// TODO: add pre processing logic before calling the updateDedicatedGatewayProvisioning() RP call.
|
||||
throw new Error(`onSave not implemented. No. of properties to save: ${currentValues.size}`);
|
||||
@@ -63,6 +64,7 @@ export default class SqlX extends SelfServeBaseClass {
|
||||
@Values({
|
||||
description: {
|
||||
textTKey: "Provisioning dedicated gateways for SqlX accounts.",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
||||
textTKey: "Learn more about dedicated gateway.",
|
||||
|
||||
Reference in New Issue
Block a user