Added support for self serve telemetry + Localization fixes (#580)

* initial telemetry commit

* Added localization changes

* moved telemetrymessage types to selfservetypes

* fixed compile errors

* fixed failing test

* changed translation file format

* Addressed PR comments

* modified test
This commit is contained in:
Srinath Narayanan 2021-03-30 10:11:43 -07:00 committed by GitHub
parent 63e13cdabe
commit 6cdac3c53b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1306 additions and 193 deletions

View File

@ -1,7 +1,7 @@
import React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react";
import { DescriptionType, NumberUiType, SmartUiInput } from "../../../SelfServe/SelfServeTypes";
import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent"; import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent";
import { NumberUiType, SmartUiInput, DescriptionType } from "../../../SelfServe/SelfServeTypes";
describe("SmartUiComponent", () => { describe("SmartUiComponent", () => {
const exampleData: SmartUiDescriptor = { const exampleData: SmartUiDescriptor = {
@ -97,9 +97,9 @@ describe("SmartUiComponent", () => {
dataFieldName: "database", dataFieldName: "database",
type: "object", type: "object",
choices: [ choices: [
{ label: "Database 1", key: "db1" }, { labelTKey: "Database 1", key: "db1" },
{ label: "Database 2", key: "db2" }, { labelTKey: "Database 2", key: "db2" },
{ label: "Database 3", key: "db3" }, { labelTKey: "Database 3", key: "db3" },
], ],
defaultKey: "db2", defaultKey: "db2",
}, },

View File

@ -334,7 +334,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
dropdownWidth="auto" dropdownWidth="auto"
options={choices.map((c) => ({ options={choices.map((c) => ({
key: c.key, key: c.key,
text: this.props.getTranslation(c.label), text: this.props.getTranslation(c.labelTKey),
}))} }))}
styles={{ styles={{
root: { width: 400 }, root: { width: 400 },

View File

@ -0,0 +1,5 @@
{
"Save": "Save",
"Discard": "Discard",
"Refresh": "Refesh"
}

View File

@ -0,0 +1,31 @@
{
"NorthCentralUS": "North Central US",
"WestUS": "West US",
"EastUS2": "East US 2",
"Current Region": "Current Region",
"RegionDropdownInfo": "More regions can be added in the future.",
"RegionsAndAccountNameValidationError": "Regions and account name should not be empty.",
"DbThroughputValidationError": "Please update throughput for database.",
"DescriptionLabel": "Description",
"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 Database Level Throughput",
"Database Throughput": "Database Throughput",
"UpdateInProgressMessage": "Data is being updated",
"UpdateCompletedMessageTitle": "Update succeeded",
"UpdateCompletedMessageText": "Data update completed.",
"SubmissionMessageSuccessTitle": "Update started",
"SubmissionMessageForNewRegionText": "Data update started. Region changed.",
"SubmissionMessageForSameRegionText": "Data update started. Region not changed.",
"SubmissionMessageErrorTitle": "Data update failed",
"SubmissionMessageErrorText": "Data update failed because of errors.",
"OnSaveFailureMessage": "Data save operation not currently permitted."
}

View File

@ -0,0 +1,52 @@
{
"DedicatedGatewayDescription": "Provision a dedicated gateway cluster for your Azure Cosmos DB account. A dedicated gateway is compute that is a front-end to data in your Azure Cosmos DB account. Your dedicated gateway automatically includes the integrated cache, which can improve read performance.",
"DedicatedGateway": "Dedicated Gateway",
"Enable": "Enable",
"Disable": "Disable",
"LearnAboutDedicatedGateway": "Learn more about dedicated gateway.",
"DeprovisioningDetailsText": "Learn more about deprovisioning the dedicated gateway.",
"DedicatedGatewayPricing": "Learn more about dedicated gateway pricing.",
"SKUs": "SKUs",
"SKUsPlaceHolder": "Select SKUs",
"NumberOfInstances": "Number of instances",
"CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)",
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)",
"CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)",
"CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)",
"CreateMessage": "Dedicated gateway resource is being created.",
"CreateInitializeTitle": "Provisioning resource",
"CreateInitializeMessage": "Dedicated gateway resource will be provisioned.",
"CreateSuccessTitle": "Resource provisioned",
"CreateSuccesseMessage": "Dedicated gateway resource provisioned.",
"CreateFailureTitle": "Failed to provision resource",
"CreateFailureMessage": "Dedicated gateway resource provisioning failed.",
"UpdateMessage": "Dedicated gateway resource is being updated.",
"UpdateInitializeTitle": "Updating resource",
"UpdateInitializeMessage": "Dedicated gateway resource will be updated.",
"UpdateSuccessTitle": "Resource updated",
"UpdateSuccesseMessage": "Dedicated gateway resource updated.",
"UpdateFailureTitle": "Failed to update resource",
"UpdateFailureMessage": "Dedicated gateway resource updation failed.",
"DeleteMessage": "Dedicated gateway resource is being deleted.",
"DeleteInitializeTitle": "Deleting resource",
"DeleteInitializeMessage": "Dedicated gateway resource will be deleted.",
"DeleteSuccessTitle": "Resource deleted",
"DeleteSuccesseMessage": "Dedicated gateway resource deleted.",
"DeleteFailureTitle": "Failed to delete resource",
"DeleteFailureMessage": "Dedicated gateway resource deletion failed.",
"CannotSave": "Cannot save the changes to the Dedicated gateway resource at the moment.",
"DedicatedGatewayEndpoint": "Dedicated gatewayEndpoint",
"NoValue": "",
"SKUDetails": "SKU Details:",
"CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory",
"CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory",
"CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory",
"CosmosD32Details": "General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory",
"Cost": "Cost",
"CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.",
"ConnectionString": "Connection String",
"ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ",
"KeysBlade": "the keys blade",
"WarningBannerOnUpdate": "Adding or modifying dedicated gateway instances may affect your bill.",
"WarningBannerOnDelete": "After deprovisioning the dedicated gateway, you must update any applications using the old dedicated gateway connection string."
}

View File

@ -1,92 +0,0 @@
{
"translations": {
"Common": {
"Save": "Save",
"Discard": "Discard",
"Refresh": "Refesh"
},
"SelfServeExample": {
"North Central US": "North Central US",
"West US": "West US",
"East US 2": "East US 2",
"Current Region": "Current Region",
"RegionDropdownInfo": "More regions can be added in the future.",
"RegionsAndAccountNameValidationError": "Regions and account name should not be empty.",
"DbThroughputValidationError": "Please update throughput for database.",
"DescriptionLabel": "Description",
"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 Database Level Throughput",
"Database Throughput": "Database Throughput",
"UpdateInProgressMessage": "Data is being updated",
"UpdateCompletedMessageTitle": "Update succeeded",
"UpdateCompletedMessageText": "Data update completed.",
"SubmissionMessageSuccessTitle": "Update started",
"SubmissionMessageForNewRegionText": "Data update started. Region changed.",
"SubmissionMessageForSameRegionText": "Data update started. Region not changed.",
"SubmissionMessageErrorTitle": "Data update failed",
"SubmissionMessageErrorText": "Data update failed because of errors.",
"OnSaveFailureMessage": "Data save operation not currently permitted."
},
"SqlX": {
"DedicatedGatewayDescription": "Provision a dedicated gateway cluster for your Azure Cosmos DB account. A dedicated gateway is compute that is a front-end to data in your Azure Cosmos DB account. Your dedicated gateway automatically includes the integrated cache, which can improve read performance. ",
"DedicatedGateway": "Dedicated Gateway",
"Enable": "Enable",
"Disable": "Disable",
"LearnAboutDedicatedGateway": "Learn more about dedicated gateway.",
"DeprovisioningDetailsText": "Learn more about deprovisioning the dedicated gateway.",
"DedicatedGatewayPricing": "Learn more about dedicated gateway pricing",
"SKUs": "SKUs",
"SKUsPlaceHolder": "Select SKUs",
"NumberOfInstances": "Number of instances",
"CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)",
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)",
"CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)",
"CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)",
"CreateMessage": "Dedicated gateway resource is being created.",
"CreateInitializeTitle": "Provisioning resource",
"CreateInitializeMessage": "Dedicated gateway resource will be provisioned.",
"CreateSuccessTitle": "Resource provisioned",
"CreateSuccesseMessage": "Dedicated gateway resource provisioned.",
"CreateFailureTitle": "Failed to provision resource",
"CreateFailureMessage": "Dedicated gateway resource provisioning failed.",
"UpdateMessage": "Dedicated gateway resource is being updated.",
"UpdateInitializeTitle": "Updating resource",
"UpdateInitializeMessage": "Dedicated gateway resource will be updated.",
"UpdateSuccessTitle": "Resource updated",
"UpdateSuccesseMessage": "Dedicated gateway resource updated.",
"UpdateFailureTitle": "Failed to update resource",
"UpdateFailureMessage": "Dedicated gateway resource updation failed.",
"DeleteMessage": "Dedicated gateway resource is being deleted.",
"DeleteInitializeTitle": "Deleting resource",
"DeleteInitializeMessage": "Dedicated gateway resource will be deleted.",
"DeleteSuccessTitle": "Resource deleted",
"DeleteSuccesseMessage": "Dedicated gateway resource deleted.",
"DeleteFailureTitle": "Failed to delete resource",
"DeleteFailureMessage": "Dedicated gateway resource deletion failed.",
"CannotSave": "Cannot save the changes to the Dedicated gateway resource at the moment",
"DedicatedGatewayEndpoint": "Dedicated gatewayEndpoint",
"NoValue": "",
"SKUDetails": "SKU Details: ",
"CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory",
"CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory",
"CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory",
"CosmosD32Details": "General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory",
"Cost": "Cost",
"CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.",
"ConnectionString": "Connection String",
"ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ",
"KeysBlade": "the keys blade",
"WarningBannerOnUpdate": "Adding or modifying dedicated gateway instances may affect your bill.",
"WarningBannerOnDelete": "After deprovisioning the dedicated gateway, you must update any applications using the old dedicated gateway connection string."
}
}
}

View File

@ -23,9 +23,9 @@ import {
} from "./SelfServeExample.rp"; } from "./SelfServeExample.rp";
const regionDropdownItems: ChoiceItem[] = [ const regionDropdownItems: ChoiceItem[] = [
{ label: "North Central US", key: Regions.NorthCentralUS }, { labelTKey: "NorthCentralUS", key: Regions.NorthCentralUS },
{ label: "West US", key: Regions.WestUS }, { labelTKey: "WestUS", key: Regions.WestUS },
{ label: "East US 2", key: Regions.EastUS2 }, { labelTKey: "EastUS2", key: Regions.EastUS2 },
]; ];
const regionDropdownInfo: Info = { const regionDropdownInfo: Info = {

View File

@ -2,10 +2,12 @@ import { Spinner, SpinnerSize } from "office-ui-fabric-react";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import * as React from "react"; import * as React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { withTranslation } from "react-i18next";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import { sendReadyMessage } from "../Common/MessageHandler"; import { sendReadyMessage } from "../Common/MessageHandler";
import { configContext, updateConfigContext } from "../ConfigContext"; import { configContext, updateConfigContext } from "../ConfigContext";
import { SelfServeFrameInputs } from "../Contracts/ViewModels"; import { SelfServeFrameInputs } from "../Contracts/ViewModels";
import i18n from "../i18n";
import { updateUserContext } from "../UserContext"; import { updateUserContext } from "../UserContext";
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation"; import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
import "./SelfServe.less"; import "./SelfServe.less";
@ -14,14 +16,35 @@ import { SelfServeDescriptor } from "./SelfServeTypes";
import { SelfServeType } from "./SelfServeUtils"; import { SelfServeType } from "./SelfServeUtils";
initializeIcons(); initializeIcons();
const loadTranslationFile = async (className: string): Promise<void> => {
const language = i18n.languages[0];
const fileName = `${className}.json`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let translations: any;
try {
translations = await import(`../Localization/${language}/${fileName}`);
} catch (e) {
translations = await import(`../Localization/en/${fileName}`);
}
i18n.addResourceBundle(language, className, translations.default, true);
};
const loadTranslations = async (className: string): Promise<void> => {
await loadTranslationFile("Common");
await loadTranslationFile(className);
};
const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => { const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
switch (selfServeType) { switch (selfServeType) {
case SelfServeType.example: { case SelfServeType.example: {
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample"); const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
await loadTranslations(SelfServeExample.default.name);
return new SelfServeExample.default().toSelfServeDescriptor(); return new SelfServeExample.default().toSelfServeDescriptor();
} }
case SelfServeType.sqlx: { case SelfServeType.sqlx: {
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX"); const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
await loadTranslations(SqlX.default.name);
return new SqlX.default().toSelfServeDescriptor(); return new SqlX.default().toSelfServeDescriptor();
} }
default: default:
@ -33,7 +56,8 @@ const renderComponent = (selfServeDescriptor: SelfServeDescriptor): JSX.Element
if (!selfServeDescriptor) { if (!selfServeDescriptor) {
return <h1>Invalid self serve type!</h1>; return <h1>Invalid self serve type!</h1>;
} }
return <SelfServeComponent descriptor={selfServeDescriptor} />; const SelfServeComponentTranslated = withTranslation()(SelfServeComponent);
return <SelfServeComponentTranslated descriptor={selfServeDescriptor} />;
}; };
const renderSpinner = (): JSX.Element => { const renderSpinner = (): JSX.Element => {

View File

@ -1,5 +1,5 @@
import React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import React from "react";
import { SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent"; import { SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent";
import { NumberUiType, OnSaveResult, SelfServeDescriptor, SmartUiInput } from "./SelfServeTypes"; import { NumberUiType, OnSaveResult, SelfServeDescriptor, SmartUiInput } from "./SelfServeTypes";
@ -87,9 +87,9 @@ describe("SelfServeComponent", () => {
dataFieldName: "database", dataFieldName: "database",
type: "object", type: "object",
choices: [ choices: [
{ label: "Database 1", key: "db1" }, { labelTKey: "Database 1", key: "db1" },
{ label: "Database 2", key: "db2" }, { labelTKey: "Database 2", key: "db2" },
{ label: "Database 3", key: "db3" }, { labelTKey: "Database 3", key: "db3" },
], ],
defaultKey: "db2", defaultKey: "db2",
}, },
@ -106,7 +106,9 @@ describe("SelfServeComponent", () => {
}; };
it("should render and honor save, discard, refresh actions", async () => { it("should render and honor save, discard, refresh actions", async () => {
const wrapper = shallow(<SelfServeComponent descriptor={exampleData} />); const wrapper = shallow(
<SelfServeComponent descriptor={exampleData} t={undefined} i18n={undefined} tReady={undefined} />
);
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
@ -158,7 +160,9 @@ describe("SelfServeComponent", () => {
}); });
it("getResolvedValue", async () => { it("getResolvedValue", async () => {
const wrapper = shallow(<SelfServeComponent descriptor={exampleData} />); const wrapper = shallow(
<SelfServeComponent descriptor={exampleData} t={undefined} i18n={undefined} tReady={undefined} />
);
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
const selfServeComponent = wrapper.instance() as SelfServeComponent; const selfServeComponent = wrapper.instance() as SelfServeComponent;
@ -179,7 +183,9 @@ describe("SelfServeComponent", () => {
it("message bar and spinner snapshots", async () => { it("message bar and spinner snapshots", async () => {
const newDescriptor = { ...exampleData, onRefresh: onRefreshIsUpdatingMock }; const newDescriptor = { ...exampleData, onRefresh: onRefreshIsUpdatingMock };
let wrapper = shallow(<SelfServeComponent descriptor={newDescriptor} />); let wrapper = shallow(
<SelfServeComponent descriptor={newDescriptor} t={undefined} i18n={undefined} tReady={undefined} />
);
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
let selfServeComponent = wrapper.instance() as SelfServeComponent; let selfServeComponent = wrapper.instance() as SelfServeComponent;
selfServeComponent.onSaveButtonClick(); selfServeComponent.onSaveButtonClick();
@ -187,7 +193,9 @@ describe("SelfServeComponent", () => {
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
newDescriptor.onRefresh = onRefreshMock; newDescriptor.onRefresh = onRefreshMock;
wrapper = shallow(<SelfServeComponent descriptor={newDescriptor} />); wrapper = shallow(
<SelfServeComponent descriptor={newDescriptor} t={undefined} i18n={undefined} tReady={undefined} />
);
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
selfServeComponent = wrapper.instance() as SelfServeComponent; selfServeComponent = wrapper.instance() as SelfServeComponent;
selfServeComponent.onSaveButtonClick(); selfServeComponent.onSaveButtonClick();

View File

@ -8,15 +8,15 @@ import {
Spinner, Spinner,
SpinnerSize, SpinnerSize,
Stack, Stack,
Text,
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import promiseRetry, { AbortError } from "p-retry"; import promiseRetry, { AbortError } from "p-retry";
import React from "react"; import React from "react";
import { Translation } from "react-i18next"; import { WithTranslation } from "react-i18next";
import * as _ from "underscore"; import * as _ from "underscore";
import { sendMessage } from "../Common/MessageHandler"; import { sendMessage } from "../Common/MessageHandler";
import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts"; import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts";
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent"; import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
import "../i18n";
import { commandBarItemStyles, commandBarStyles, containerStackTokens, separatorStyles } from "./SelfServeStyles"; import { commandBarItemStyles, commandBarStyles, containerStackTokens, separatorStyles } from "./SelfServeStyles";
import { import {
AnyDisplay, AnyDisplay,
@ -57,7 +57,7 @@ interface PortalNotificationContent {
}; };
} }
export interface SelfServeComponentProps { export interface SelfServeComponentProps extends WithTranslation {
descriptor: SelfServeDescriptor; descriptor: SelfServeDescriptor;
} }
@ -108,6 +108,9 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
this.retryIntervalInMs = SelfServeComponent.defaultRetryIntervalInMs; this.retryIntervalInMs = SelfServeComponent.defaultRetryIntervalInMs;
} }
this.retryOptions = { forever: true, maxTimeout: this.retryIntervalInMs, minTimeout: this.retryIntervalInMs }; this.retryOptions = { forever: true, maxTimeout: this.retryIntervalInMs, minTimeout: this.retryIntervalInMs };
// translation function passed to SelfServeComponent
this.translationFunction = this.props.t;
} }
private onError = (hasErrors: boolean): void => { private onError = (hasErrors: boolean): void => {
@ -391,8 +394,8 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
return this.getTranslation(key, "Common"); return this.getTranslation(key, "Common");
}; };
private getTranslation = (messageKey: string, prefix = `${this.smartUiGeneratorClassName}`): string => { private getTranslation = (messageKey: string, namespace = `${this.smartUiGeneratorClassName}`): string => {
const translationKey = `${prefix}.${messageKey}`; const translationKey = `${namespace}:${messageKey}`;
const translation = this.translationFunction ? this.translationFunction(translationKey) : messageKey; const translation = this.translationFunction ? this.translationFunction(translationKey) : messageKey;
if (translation === translationKey) { if (translation === translationKey) {
return messageKey; return messageKey;
@ -441,53 +444,45 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
public render(): JSX.Element { public render(): JSX.Element {
if (this.state.compileErrorMessage) { if (this.state.compileErrorMessage) {
return <MessageBar messageBarType={MessageBarType.error}>{this.state.compileErrorMessage}</MessageBar>; return (
<MessageBar messageBarType={MessageBarType.error}>
<Text>{this.state.compileErrorMessage}</Text>
</MessageBar>
);
} }
return ( return (
<Translation> <div style={{ overflowX: "auto" }}>
{(translate) => { <Stack tokens={containerStackTokens}>
if (!this.translationFunction) { <Stack.Item>
this.translationFunction = translate; <CommandBar styles={commandBarStyles} items={this.getCommandBarItems()} />
} <Separator styles={separatorStyles} />
</Stack.Item>
return ( {this.state.isInitializing ? (
<div style={{ overflowX: "auto" }}> <Spinner size={SpinnerSize.large} />
<Stack tokens={containerStackTokens}> ) : (
<Stack.Item> <>
<CommandBar styles={commandBarStyles} items={this.getCommandBarItems()} /> {this.state.notification && (
<Separator styles={separatorStyles} /> <MessageBar
</Stack.Item> messageBarType={this.state.notification.type}
{this.state.isInitializing ? ( onDismiss={
<Spinner size={SpinnerSize.large} /> this.state.notification.isCancellable ? () => this.setState({ notification: undefined }) : undefined
) : ( }
<> >
{this.state.notification && ( <Text>{this.state.notification.message}</Text>
<MessageBar </MessageBar>
messageBarType={this.state.notification.type} )}
onDismiss={ <SmartUiComponent
this.state.notification.isCancellable disabled={this.state.refreshResult?.isUpdateInProgress || this.state.isSaving}
? () => this.setState({ notification: undefined }) descriptor={this.state.root as SmartUiDescriptor}
: undefined currentValues={this.state.currentValues}
} onInputChange={this.onInputChange}
> onError={this.onError}
{this.state.notification.message} getTranslation={this.getTranslation}
</MessageBar> />
)} </>
<SmartUiComponent )}
disabled={this.state.refreshResult?.isUpdateInProgress || this.state.isSaving} </Stack>
descriptor={this.state.root as SmartUiDescriptor} </div>
currentValues={this.state.currentValues}
onInputChange={this.onInputChange}
onError={this.onError}
getTranslation={this.getTranslation}
/>
</>
)}
</Stack>
</div>
);
}}
</Translation>
); );
} }
} }

View File

@ -0,0 +1,69 @@
import { sendMessage } from "../Common/MessageHandler";
import { configContext } from "../ConfigContext";
import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts";
import { appInsights } from "../Shared/appInsights";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import { userContext } from "../UserContext";
import { SelfServeTelemetryMessage } from "./SelfServeTypes";
const action = Action.SelfServe;
export const trace = (data: SelfServeTelemetryMessage): void => {
sendSelfServeTelemetryMessage(ActionModifiers.Mark, data);
appInsights.trackEvent({ name: Action[action] }, decorateData(data, ActionModifiers.Mark));
};
export const traceStart = (data: SelfServeTelemetryMessage): number => {
const timestamp: number = Date.now();
sendSelfServeTelemetryMessage(ActionModifiers.Start, data);
appInsights.startTrackEvent(Action[action]);
return timestamp;
};
export const traceSuccess = (data: SelfServeTelemetryMessage, timestamp?: number): void => {
sendSelfServeTelemetryMessage(ActionModifiers.Success, data, timestamp || Date.now());
appInsights.stopTrackEvent(Action[action], decorateData(data, ActionModifiers.Success));
};
export const traceFailure = (data: SelfServeTelemetryMessage, timestamp?: number): void => {
sendSelfServeTelemetryMessage(ActionModifiers.Failed, data, timestamp || Date.now());
appInsights.stopTrackEvent(Action[action], decorateData(data, ActionModifiers.Failed));
};
export const traceCancel = (data: SelfServeTelemetryMessage, timestamp?: number): void => {
sendSelfServeTelemetryMessage(ActionModifiers.Cancel, data, timestamp || Date.now());
appInsights.stopTrackEvent(Action[action], decorateData(data, ActionModifiers.Cancel));
};
const sendSelfServeTelemetryMessage = (
actionModifier: string,
data: SelfServeTelemetryMessage,
timeStamp?: number
): void => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dataToSend: any = {
type: SelfServeMessageTypes.TelemetryInfo,
data: {
action: Action[action],
actionModifier: actionModifier,
data: JSON.stringify(decorateData(data)),
},
};
if (timeStamp) {
dataToSend.data.timeStamp = timeStamp;
}
sendMessage(dataToSend);
};
const decorateData = (data: SelfServeTelemetryMessage, actionModifier?: string) => {
return {
databaseAccountName: userContext.databaseAccount?.name,
defaultExperience: userContext.defaultExperience,
authType: userContext.authType,
subscriptionId: userContext.subscriptionId,
platform: configContext.platform,
env: process.env.NODE_ENV,
actionModifier,
...data,
} as { [key: string]: string };
};

View File

@ -98,7 +98,7 @@ export enum NumberUiType {
Slider = "Slider", Slider = "Slider",
} }
export type ChoiceItem = { label: string; key: string }; export type ChoiceItem = { labelTKey: string; key: string };
export type InputType = number | string | boolean | ChoiceItem | Description; export type InputType = number | string | boolean | ChoiceItem | Description;
@ -157,3 +157,9 @@ export interface RefreshResult {
export interface RefreshParams { export interface RefreshParams {
retryIntervalInMs: number; retryIntervalInMs: number;
} }
export interface SelfServeTelemetryMessage {
selfServeClassName: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any;
}

View File

@ -131,9 +131,9 @@ describe("SelfServeUtils", () => {
type: "object", type: "object",
labelTKey: "Regions", labelTKey: "Regions",
choices: [ choices: [
{ label: "South West US", key: "SWUS" }, { labelTKey: "South West US", key: "SWUS" },
{ label: "North Central US", key: "NCUS" }, { labelTKey: "North Central US", key: "NCUS" },
{ label: "East US 2", key: "EUS2" }, { labelTKey: "East US 2", key: "EUS2" },
], ],
}, },
], ],
@ -238,9 +238,9 @@ describe("SelfServeUtils", () => {
type: "object", type: "object",
labelTKey: "Regions", labelTKey: "Regions",
choices: [ choices: [
{ label: "South West US", key: "SWUS" }, { labelTKey: "South West US", key: "SWUS" },
{ label: "North Central US", key: "NCUS" }, { labelTKey: "North Central US", key: "NCUS" },
{ label: "East US 2", key: "EUS2" }, { labelTKey: "East US 2", key: "EUS2" },
], ],
}, },
children: [] as Node[], children: [] as Node[],

View File

@ -195,5 +195,5 @@ export const generateBladeLink = (blade: BladeType): string => {
const subscriptionId = userContext.subscriptionId; const subscriptionId = userContext.subscriptionId;
const resourceGroupName = userContext.resourceGroup; const resourceGroupName = userContext.resourceGroup;
const databaseAccountName = userContext.databaseAccount.name; const databaseAccountName = userContext.databaseAccount.name;
return `https://portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}/${blade}`; return `${document.referrer}#@microsoft.onmicrosoft.com/resource/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}/${blade}`;
}; };

View File

@ -1,4 +1,5 @@
import { IsDisplayable, OnChange, RefreshOptions, Values } from "../Decorators"; import { IsDisplayable, OnChange, RefreshOptions, Values } from "../Decorators";
import { trace } from "../SelfServeTelemetryProcessor";
import { import {
ChoiceItem, ChoiceItem,
Description, Description,
@ -147,10 +148,10 @@ const onEnableDedicatedGatewayChange = (
}; };
const skuDropDownItems: ChoiceItem[] = [ const skuDropDownItems: ChoiceItem[] = [
{ label: "CosmosD4s", key: CosmosD4s }, { labelTKey: "CosmosD4s", key: CosmosD4s },
{ label: "CosmosD8s", key: CosmosD8s }, { labelTKey: "CosmosD8s", key: CosmosD8s },
{ label: "CosmosD16s", key: CosmosD16s }, { labelTKey: "CosmosD16s", key: CosmosD16s },
{ label: "CosmosD32s", key: CosmosD32s }, { labelTKey: "CosmosD32s", key: CosmosD32s },
]; ];
const getSkus = async (): Promise<ChoiceItem[]> => { const getSkus = async (): Promise<ChoiceItem[]> => {
@ -176,6 +177,8 @@ export default class SqlX extends SelfServeBaseClass {
currentValues: Map<string, SmartUiInput>, currentValues: Map<string, SmartUiInput>,
baselineValues: Map<string, SmartUiInput> baselineValues: Map<string, SmartUiInput>
): Promise<OnSaveResult> => { ): Promise<OnSaveResult> => {
trace({ selfServeClassName: "SqlX" });
const dedicatedGatewayCurrentlyEnabled = currentValues.get("enableDedicatedGateway")?.value as boolean; const dedicatedGatewayCurrentlyEnabled = currentValues.get("enableDedicatedGateway")?.value as boolean;
const dedicatedGatewayOriginallyEnabled = baselineValues.get("enableDedicatedGateway")?.value as boolean; const dedicatedGatewayOriginallyEnabled = baselineValues.get("enableDedicatedGateway")?.value as boolean;

File diff suppressed because it is too large Load Diff

View File

@ -114,6 +114,7 @@ export enum Action {
NotebooksGalleryPublicGalleryCount, NotebooksGalleryPublicGalleryCount,
NotebooksGalleryFavoritesCount, NotebooksGalleryFavoritesCount,
NotebooksGalleryPublishedCount, NotebooksGalleryPublishedCount,
SelfServe,
} }
export const ActionModifiers = { export const ActionModifiers = {

View File

@ -1,22 +1,14 @@
import i18n from "i18next"; import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector"; import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next";
import XHR from "i18next-http-backend";
import EnglishTranslations from "./Localization/en/translations.json";
i18n i18n
.use(XHR)
.use(LanguageDetector) .use(LanguageDetector)
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
resources: {
en: EnglishTranslations,
},
fallbackLng: "en", fallbackLng: "en",
detection: { order: ["navigator", "cookie", "localStorage", "sessionStorage", "querystring", "htmlTag"] }, detection: { order: ["navigator", "cookie", "localStorage", "sessionStorage", "querystring", "htmlTag"] },
debug: process.env.NODE_ENV === "development", debug: process.env.NODE_ENV === "development",
ns: ["translations"],
defaultNS: "translations",
keySeparator: ".", keySeparator: ".",
interpolation: { interpolation: {
formatSeparator: ",", formatSeparator: ",",
@ -29,3 +21,5 @@ i18n
useSuspense: false, useSuspense: false,
}, },
}); });
export default i18n;