Added localization for the Self Serve Model (#406)

* added localization for selfserve model

* added comment

* addressed PR comments

* fixed format errors

* Addressed PR comments
This commit is contained in:
Srinath Narayanan 2021-01-28 11:17:02 -08:00 committed by GitHub
parent f8ede0cc1e
commit 6aaddd9c60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 367 additions and 1042 deletions

46
package-lock.json generated
View File

@ -11665,6 +11665,14 @@
} }
} }
}, },
"html-parse-stringify2": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz",
"integrity": "sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o=",
"requires": {
"void-elements": "^2.0.1"
}
},
"html-to-react": { "html-to-react": {
"version": "1.4.5", "version": "1.4.5",
"resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.4.5.tgz", "resolved": "https://registry.npmjs.org/html-to-react/-/html-to-react-1.4.5.tgz",
@ -11850,6 +11858,30 @@
"integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==",
"dev": true "dev": true
}, },
"i18next": {
"version": "19.8.4",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-19.8.4.tgz",
"integrity": "sha512-FfVPNWv+felJObeZ6DSXZkj9QM1Ivvh7NcFCgA8XPtJWHz0iXVa9BUy+QY8EPrCLE+vWgDfV/sc96BgXVo6HAA==",
"requires": {
"@babel/runtime": "^7.12.0"
}
},
"i18next-browser-languagedetector": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.0.1.tgz",
"integrity": "sha512-3H+OsNQn3FciomUU0d4zPFHsvJv4X66lBelXk9hnIDYDsveIgT7dWZ3/VvcSlpKk9lvCK770blRZ/CwHMXZqWw==",
"requires": {
"@babel/runtime": "^7.5.5"
}
},
"i18next-http-backend": {
"version": "1.0.23",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-1.0.23.tgz",
"integrity": "sha512-2iXwUmawM4kozvGN+k7G9u/bYQdgqtTXVK0cWvLSOpUCTaW30ZzRhgu0FBfinb71XjwUEvdqb95jNrEFjrwGKw==",
"requires": {
"node-fetch": "2.6.1"
}
},
"iconv-lite": { "iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -17900,6 +17932,15 @@
"prop-types": "^15.6.1" "prop-types": "^15.6.1"
} }
}, },
"react-i18next": {
"version": "11.8.5",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.8.5.tgz",
"integrity": "sha512-2jY/8NkhNv2KWBnZuhHxTn13aMxAbvhiDUNskm+1xVVnrPId78l8fA7fCyVeO3XU1kptM0t4MtvxV1Nu08cjLw==",
"requires": {
"@babel/runtime": "^7.3.1",
"html-parse-stringify2": "2.0.1"
}
},
"react-is": { "react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -21373,6 +21414,11 @@
"integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
"dev": true "dev": true
}, },
"void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
"integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w="
},
"w3c-hr-time": { "w3c-hr-time": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",

View File

@ -64,6 +64,9 @@
"eslint-plugin-react": "7.20.0", "eslint-plugin-react": "7.20.0",
"hasher": "1.2.0", "hasher": "1.2.0",
"html2canvas": "1.0.0-rc.5", "html2canvas": "1.0.0-rc.5",
"i18next": "19.8.4",
"i18next-browser-languagedetector": "6.0.1",
"i18next-http-backend": "1.0.23",
"immutable": "4.0.0-rc.12", "immutable": "4.0.0-rc.12",
"is-ci": "2.0.0", "is-ci": "2.0.0",
"jquery": "3.5.1", "jquery": "3.5.1",
@ -86,6 +89,7 @@
"react-dnd-html5-backend": "9.4.0", "react-dnd-html5-backend": "9.4.0",
"react-dom": "16.13.1", "react-dom": "16.13.1",
"react-hotkeys": "2.0.0", "react-hotkeys": "2.0.0",
"react-i18next": "11.8.5",
"react-notification-system": "0.2.17", "react-notification-system": "0.2.17",
"react-redux": "7.1.3", "react-redux": "7.1.3",
"redux": "4.0.4", "redux": "4.0.4",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

31
src/i18n.ts Normal file
View File

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