diff --git a/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx b/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx index 91bc16815..ec3298812 100644 --- a/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx +++ b/src/Explorer/Controls/SmartUi/SmartUiComponent.test.tsx @@ -1,7 +1,7 @@ -import React from "react"; import { shallow } from "enzyme"; +import React from "react"; +import { DescriptionType, NumberUiType, SmartUiInput } from "../../../SelfServe/SelfServeTypes"; import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent"; -import { NumberUiType, SmartUiInput, DescriptionType } from "../../../SelfServe/SelfServeTypes"; describe("SmartUiComponent", () => { const exampleData: SmartUiDescriptor = { @@ -97,9 +97,9 @@ describe("SmartUiComponent", () => { dataFieldName: "database", type: "object", choices: [ - { label: "Database 1", key: "db1" }, - { label: "Database 2", key: "db2" }, - { label: "Database 3", key: "db3" }, + { labelTKey: "Database 1", key: "db1" }, + { labelTKey: "Database 2", key: "db2" }, + { labelTKey: "Database 3", key: "db3" }, ], defaultKey: "db2", }, diff --git a/src/Explorer/Controls/SmartUi/SmartUiComponent.tsx b/src/Explorer/Controls/SmartUi/SmartUiComponent.tsx index 4ca14a266..34e7be8bb 100644 --- a/src/Explorer/Controls/SmartUi/SmartUiComponent.tsx +++ b/src/Explorer/Controls/SmartUi/SmartUiComponent.tsx @@ -334,7 +334,7 @@ export class SmartUiComponent extends React.Component ({ key: c.key, - text: this.props.getTranslation(c.label), + text: this.props.getTranslation(c.labelTKey), }))} styles={{ root: { width: 400 }, diff --git a/src/Localization/en/Common.json b/src/Localization/en/Common.json new file mode 100644 index 000000000..0e58fa4d4 --- /dev/null +++ b/src/Localization/en/Common.json @@ -0,0 +1,5 @@ +{ + "Save": "Save", + "Discard": "Discard", + "Refresh": "Refesh" +} \ No newline at end of file diff --git a/src/Localization/en/SelfServeExample.json b/src/Localization/en/SelfServeExample.json new file mode 100644 index 000000000..2d28615c7 --- /dev/null +++ b/src/Localization/en/SelfServeExample.json @@ -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." +} \ No newline at end of file diff --git a/src/Localization/en/SqlX.json b/src/Localization/en/SqlX.json new file mode 100644 index 000000000..465cba248 --- /dev/null +++ b/src/Localization/en/SqlX.json @@ -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." +} \ No newline at end of file diff --git a/src/Localization/en/translations.json b/src/Localization/en/translations.json deleted file mode 100644 index 44d4506cd..000000000 --- a/src/Localization/en/translations.json +++ /dev/null @@ -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." - } - } -} \ No newline at end of file diff --git a/src/SelfServe/Example/SelfServeExample.tsx b/src/SelfServe/Example/SelfServeExample.tsx index df994d0a7..a533a26f7 100644 --- a/src/SelfServe/Example/SelfServeExample.tsx +++ b/src/SelfServe/Example/SelfServeExample.tsx @@ -23,9 +23,9 @@ import { } from "./SelfServeExample.rp"; const regionDropdownItems: ChoiceItem[] = [ - { label: "North Central US", key: Regions.NorthCentralUS }, - { label: "West US", key: Regions.WestUS }, - { label: "East US 2", key: Regions.EastUS2 }, + { labelTKey: "NorthCentralUS", key: Regions.NorthCentralUS }, + { labelTKey: "WestUS", key: Regions.WestUS }, + { labelTKey: "EastUS2", key: Regions.EastUS2 }, ]; const regionDropdownInfo: Info = { diff --git a/src/SelfServe/SelfServe.tsx b/src/SelfServe/SelfServe.tsx index 4ad49b2f2..062d07289 100644 --- a/src/SelfServe/SelfServe.tsx +++ b/src/SelfServe/SelfServe.tsx @@ -2,10 +2,12 @@ import { Spinner, SpinnerSize } from "office-ui-fabric-react"; import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; import * as React from "react"; import ReactDOM from "react-dom"; +import { withTranslation } from "react-i18next"; import { normalizeArmEndpoint } from "../Common/EnvironmentUtility"; import { sendReadyMessage } from "../Common/MessageHandler"; import { configContext, updateConfigContext } from "../ConfigContext"; import { SelfServeFrameInputs } from "../Contracts/ViewModels"; +import i18n from "../i18n"; import { updateUserContext } from "../UserContext"; import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation"; import "./SelfServe.less"; @@ -14,14 +16,35 @@ import { SelfServeDescriptor } from "./SelfServeTypes"; import { SelfServeType } from "./SelfServeUtils"; initializeIcons(); +const loadTranslationFile = async (className: string): Promise => { + 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 => { + await loadTranslationFile("Common"); + await loadTranslationFile(className); +}; + const getDescriptor = async (selfServeType: SelfServeType): Promise => { switch (selfServeType) { case SelfServeType.example: { const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample"); + await loadTranslations(SelfServeExample.default.name); return new SelfServeExample.default().toSelfServeDescriptor(); } case SelfServeType.sqlx: { const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX"); + await loadTranslations(SqlX.default.name); return new SqlX.default().toSelfServeDescriptor(); } default: @@ -33,7 +56,8 @@ const renderComponent = (selfServeDescriptor: SelfServeDescriptor): JSX.Element if (!selfServeDescriptor) { return

Invalid self serve type!

; } - return ; + const SelfServeComponentTranslated = withTranslation()(SelfServeComponent); + return ; }; const renderSpinner = (): JSX.Element => { diff --git a/src/SelfServe/SelfServeComponent.test.tsx b/src/SelfServe/SelfServeComponent.test.tsx index 80ec112af..81bbb905b 100644 --- a/src/SelfServe/SelfServeComponent.test.tsx +++ b/src/SelfServe/SelfServeComponent.test.tsx @@ -1,5 +1,5 @@ -import React from "react"; import { shallow } from "enzyme"; +import React from "react"; import { SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent"; import { NumberUiType, OnSaveResult, SelfServeDescriptor, SmartUiInput } from "./SelfServeTypes"; @@ -87,9 +87,9 @@ describe("SelfServeComponent", () => { dataFieldName: "database", type: "object", choices: [ - { label: "Database 1", key: "db1" }, - { label: "Database 2", key: "db2" }, - { label: "Database 3", key: "db3" }, + { labelTKey: "Database 1", key: "db1" }, + { labelTKey: "Database 2", key: "db2" }, + { labelTKey: "Database 3", key: "db3" }, ], defaultKey: "db2", }, @@ -106,7 +106,9 @@ describe("SelfServeComponent", () => { }; it("should render and honor save, discard, refresh actions", async () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); await new Promise((resolve) => setTimeout(resolve, 0)); expect(wrapper).toMatchSnapshot(); @@ -158,7 +160,9 @@ describe("SelfServeComponent", () => { }); it("getResolvedValue", async () => { - const wrapper = shallow(); + const wrapper = shallow( + + ); await new Promise((resolve) => setTimeout(resolve, 0)); const selfServeComponent = wrapper.instance() as SelfServeComponent; @@ -179,7 +183,9 @@ describe("SelfServeComponent", () => { it("message bar and spinner snapshots", async () => { const newDescriptor = { ...exampleData, onRefresh: onRefreshIsUpdatingMock }; - let wrapper = shallow(); + let wrapper = shallow( + + ); await new Promise((resolve) => setTimeout(resolve, 0)); let selfServeComponent = wrapper.instance() as SelfServeComponent; selfServeComponent.onSaveButtonClick(); @@ -187,7 +193,9 @@ describe("SelfServeComponent", () => { expect(wrapper).toMatchSnapshot(); newDescriptor.onRefresh = onRefreshMock; - wrapper = shallow(); + wrapper = shallow( + + ); await new Promise((resolve) => setTimeout(resolve, 0)); selfServeComponent = wrapper.instance() as SelfServeComponent; selfServeComponent.onSaveButtonClick(); diff --git a/src/SelfServe/SelfServeComponent.tsx b/src/SelfServe/SelfServeComponent.tsx index 649dc2bdc..0de97c8bd 100644 --- a/src/SelfServe/SelfServeComponent.tsx +++ b/src/SelfServe/SelfServeComponent.tsx @@ -8,15 +8,15 @@ import { Spinner, SpinnerSize, Stack, + Text, } from "office-ui-fabric-react"; import promiseRetry, { AbortError } from "p-retry"; import React from "react"; -import { Translation } from "react-i18next"; +import { WithTranslation } from "react-i18next"; import * as _ from "underscore"; import { sendMessage } from "../Common/MessageHandler"; import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts"; import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent"; -import "../i18n"; import { commandBarItemStyles, commandBarStyles, containerStackTokens, separatorStyles } from "./SelfServeStyles"; import { AnyDisplay, @@ -57,7 +57,7 @@ interface PortalNotificationContent { }; } -export interface SelfServeComponentProps { +export interface SelfServeComponentProps extends WithTranslation { descriptor: SelfServeDescriptor; } @@ -108,6 +108,9 @@ export class SelfServeComponent extends React.Component { @@ -391,8 +394,8 @@ export class SelfServeComponent extends React.Component { - const translationKey = `${prefix}.${messageKey}`; + private getTranslation = (messageKey: string, namespace = `${this.smartUiGeneratorClassName}`): string => { + const translationKey = `${namespace}:${messageKey}`; const translation = this.translationFunction ? this.translationFunction(translationKey) : messageKey; if (translation === translationKey) { return messageKey; @@ -441,53 +444,45 @@ export class SelfServeComponent extends React.Component{this.state.compileErrorMessage}; + return ( + + {this.state.compileErrorMessage} + + ); } return ( - - {(translate) => { - if (!this.translationFunction) { - this.translationFunction = translate; - } - - return ( -
- - - - - - {this.state.isInitializing ? ( - - ) : ( - <> - {this.state.notification && ( - this.setState({ notification: undefined }) - : undefined - } - > - {this.state.notification.message} - - )} - - - )} - -
- ); - }} -
+
+ + + + + + {this.state.isInitializing ? ( + + ) : ( + <> + {this.state.notification && ( + this.setState({ notification: undefined }) : undefined + } + > + {this.state.notification.message} + + )} + + + )} + +
); } } diff --git a/src/SelfServe/SelfServeTelemetryProcessor.ts b/src/SelfServe/SelfServeTelemetryProcessor.ts new file mode 100644 index 000000000..f26b05a1e --- /dev/null +++ b/src/SelfServe/SelfServeTelemetryProcessor.ts @@ -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 }; +}; diff --git a/src/SelfServe/SelfServeTypes.ts b/src/SelfServe/SelfServeTypes.ts index 611d19364..a86b74c04 100644 --- a/src/SelfServe/SelfServeTypes.ts +++ b/src/SelfServe/SelfServeTypes.ts @@ -98,7 +98,7 @@ export enum NumberUiType { Slider = "Slider", } -export type ChoiceItem = { label: string; key: string }; +export type ChoiceItem = { labelTKey: string; key: string }; export type InputType = number | string | boolean | ChoiceItem | Description; @@ -157,3 +157,9 @@ export interface RefreshResult { export interface RefreshParams { retryIntervalInMs: number; } + +export interface SelfServeTelemetryMessage { + selfServeClassName: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: any; +} diff --git a/src/SelfServe/SelfServeUtils.test.tsx b/src/SelfServe/SelfServeUtils.test.tsx index e1fe1a3f8..0efe7bd5f 100644 --- a/src/SelfServe/SelfServeUtils.test.tsx +++ b/src/SelfServe/SelfServeUtils.test.tsx @@ -131,9 +131,9 @@ describe("SelfServeUtils", () => { type: "object", labelTKey: "Regions", choices: [ - { label: "South West US", key: "SWUS" }, - { label: "North Central US", key: "NCUS" }, - { label: "East US 2", key: "EUS2" }, + { labelTKey: "South West US", key: "SWUS" }, + { labelTKey: "North Central US", key: "NCUS" }, + { labelTKey: "East US 2", key: "EUS2" }, ], }, ], @@ -238,9 +238,9 @@ describe("SelfServeUtils", () => { type: "object", labelTKey: "Regions", choices: [ - { label: "South West US", key: "SWUS" }, - { label: "North Central US", key: "NCUS" }, - { label: "East US 2", key: "EUS2" }, + { labelTKey: "South West US", key: "SWUS" }, + { labelTKey: "North Central US", key: "NCUS" }, + { labelTKey: "East US 2", key: "EUS2" }, ], }, children: [] as Node[], diff --git a/src/SelfServe/SelfServeUtils.tsx b/src/SelfServe/SelfServeUtils.tsx index 8d1cec9a3..8106153a2 100644 --- a/src/SelfServe/SelfServeUtils.tsx +++ b/src/SelfServe/SelfServeUtils.tsx @@ -195,5 +195,5 @@ export const generateBladeLink = (blade: BladeType): string => { const subscriptionId = userContext.subscriptionId; const resourceGroupName = userContext.resourceGroup; 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}`; }; diff --git a/src/SelfServe/SqlX/SqlX.tsx b/src/SelfServe/SqlX/SqlX.tsx index e8308b651..41d855fee 100644 --- a/src/SelfServe/SqlX/SqlX.tsx +++ b/src/SelfServe/SqlX/SqlX.tsx @@ -1,4 +1,5 @@ import { IsDisplayable, OnChange, RefreshOptions, Values } from "../Decorators"; +import { trace } from "../SelfServeTelemetryProcessor"; import { ChoiceItem, Description, @@ -147,10 +148,10 @@ const onEnableDedicatedGatewayChange = ( }; const skuDropDownItems: ChoiceItem[] = [ - { label: "CosmosD4s", key: CosmosD4s }, - { label: "CosmosD8s", key: CosmosD8s }, - { label: "CosmosD16s", key: CosmosD16s }, - { label: "CosmosD32s", key: CosmosD32s }, + { labelTKey: "CosmosD4s", key: CosmosD4s }, + { labelTKey: "CosmosD8s", key: CosmosD8s }, + { labelTKey: "CosmosD16s", key: CosmosD16s }, + { labelTKey: "CosmosD32s", key: CosmosD32s }, ]; const getSkus = async (): Promise => { @@ -176,6 +177,8 @@ export default class SqlX extends SelfServeBaseClass { currentValues: Map, baselineValues: Map ): Promise => { + trace({ selfServeClassName: "SqlX" }); + const dedicatedGatewayCurrentlyEnabled = currentValues.get("enableDedicatedGateway")?.value as boolean; const dedicatedGatewayOriginallyEnabled = baselineValues.get("enableDedicatedGateway")?.value as boolean; diff --git a/src/SelfServe/__snapshots__/SelfServeComponent.test.tsx.snap b/src/SelfServe/__snapshots__/SelfServeComponent.test.tsx.snap index 09c5ae177..1ba9afb95 100644 --- a/src/SelfServe/__snapshots__/SelfServeComponent.test.tsx.snap +++ b/src/SelfServe/__snapshots__/SelfServeComponent.test.tsx.snap @@ -1,33 +1,1050 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SelfServeComponent message bar and spinner snapshots 1`] = ` - - - +
+ + + + + + + + refresh performed successfully + + + Object { + "disabled": false, + "hidden": false, + "value": 450, + }, + "analyticalStore" => Object { + "disabled": false, + "hidden": false, + "value": false, + }, + "database" => Object { + "disabled": false, + "hidden": false, + "value": "db2", + }, + } + } + descriptor={ + Object { + "initialize": [MockFunction] { + "calls": Array [ + Array [], + Array [], + Array [], + ], + "results": Array [ + 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 [], + ], + "results": Array [ + 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 { + "disabled": false, + "hidden": false, + "value": 450, + }, + "analyticalStore" => Object { + "disabled": false, + "hidden": false, + "value": false, + }, + "database" => Object { + "disabled": false, + "hidden": false, + "value": "db2", + }, + }, + Map { + "throughput" => Object { + "disabled": false, + "hidden": false, + "value": 450, + }, + "analyticalStore" => Object { + "disabled": false, + "hidden": false, + "value": false, + }, + "database" => Object { + "disabled": false, + "hidden": false, + "value": "db2", + }, + }, + ], + Array [ + Map { + "throughput" => Object { + "disabled": false, + "hidden": false, + "value": 450, + }, + "analyticalStore" => Object { + "disabled": false, + "hidden": false, + "value": false, + }, + "database" => Object { + "disabled": false, + "hidden": false, + "value": "db2", + }, + }, + Map { + "throughput" => Object { + "disabled": false, + "hidden": false, + "value": 450, + }, + "analyticalStore" => Object { + "disabled": false, + "hidden": false, + "value": false, + }, + "database" => Object { + "disabled": false, + "hidden": false, + "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, + "labelTKey": "Throughput (input)", + "max": 500, + "min": 400, + "placeholderTKey": undefined, + "step": 10, + "type": "number", + "uiType": "Spinner", + }, + }, + Object { + "id": "containerId", + "info": undefined, + "input": Object { + "dataFieldName": "containerId", + "labelTKey": "Container id", + "placeholderTKey": undefined, + "type": "string", + }, + }, + Object { + "id": "analyticalStore", + "info": undefined, + "input": Object { + "dataFieldName": "analyticalStore", + "defaultValue": true, + "falseLabelTKey": "Disabled", + "labelTKey": "Analytical Store", + "placeholderTKey": undefined, + "trueLabelTKey": "Enabled", + "type": "boolean", + }, + }, + Object { + "id": "database", + "info": undefined, + "input": Object { + "choices": Array [ + Object { + "key": "db1", + "labelTKey": "Database 1", + }, + Object { + "key": "db2", + "labelTKey": "Database 2", + }, + Object { + "key": "db3", + "labelTKey": "Database 3", + }, + ], + "dataFieldName": "database", + "defaultKey": "db2", + "labelTKey": "Database", + "placeholderTKey": undefined, + "type": "object", + }, + }, + ], + "id": "root", + "info": Object { + "link": Object { + "href": "https://aka.ms/azure-cosmos-db-pricing", + "textTKey": "More Details", + }, + "messageTKey": "Start at $24/mo per database", + }, + }, + } + } + disabled={true} + getTranslation={[Function]} + onError={[Function]} + onInputChange={[Function]} + /> + +
`; exports[`SelfServeComponent message bar and spinner snapshots 2`] = ` - - - +
+ + + + + + Object { + "disabled": false, + "hidden": false, + "value": 450, + }, + "analyticalStore" => Object { + "disabled": false, + "hidden": false, + "value": false, + }, + "database" => Object { + "disabled": false, + "hidden": false, + "value": "db2", + }, + } + } + descriptor={ + Object { + "initialize": [MockFunction] { + "calls": 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 {}, + }, + ], + }, + "inputNames": Array [ + "throughput", + "analyticalStore", + "database", + ], + "onRefresh": [MockFunction] { + "calls": Array [ + 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 {}, + }, + Object { + "type": "return", + "value": Promise {}, + }, + ], + }, + "onSave": [MockFunction] { + "calls": Array [ + Array [ + Map { + "throughput" => Object { + "disabled": false, + "hidden": false, + "value": 450, + }, + "analyticalStore" => Object { + "disabled": false, + "hidden": false, + "value": false, + }, + "database" => Object { + "disabled": false, + "hidden": false, + "value": "db2", + }, + }, + Map { + "throughput" => Object { + "disabled": false, + "hidden": false, + "value": 450, + }, + "analyticalStore" => Object { + "disabled": false, + "hidden": false, + "value": false, + }, + "database" => Object { + "disabled": false, + "hidden": false, + "value": "db2", + }, + }, + ], + Array [ + Map { + "throughput" => Object { + "disabled": false, + "hidden": false, + "value": 450, + }, + "analyticalStore" => Object { + "disabled": false, + "hidden": false, + "value": false, + }, + "database" => Object { + "disabled": false, + "hidden": false, + "value": "db2", + }, + }, + Map { + "throughput" => Object { + "disabled": false, + "hidden": false, + "value": 450, + }, + "analyticalStore" => Object { + "disabled": false, + "hidden": false, + "value": false, + }, + "database" => Object { + "disabled": false, + "hidden": false, + "value": "db2", + }, + }, + ], + Array [ + Map { + "throughput" => Object { + "disabled": false, + "hidden": false, + "value": 450, + }, + "analyticalStore" => Object { + "disabled": false, + "hidden": false, + "value": false, + }, + "database" => Object { + "disabled": false, + "hidden": false, + "value": "db2", + }, + }, + Map { + "throughput" => Object { + "disabled": false, + "hidden": false, + "value": 450, + }, + "analyticalStore" => Object { + "disabled": false, + "hidden": false, + "value": false, + }, + "database" => Object { + "disabled": false, + "hidden": false, + "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, + "labelTKey": "Throughput (input)", + "max": 500, + "min": 400, + "placeholderTKey": undefined, + "step": 10, + "type": "number", + "uiType": "Spinner", + }, + }, + Object { + "id": "containerId", + "info": undefined, + "input": Object { + "dataFieldName": "containerId", + "labelTKey": "Container id", + "placeholderTKey": undefined, + "type": "string", + }, + }, + Object { + "id": "analyticalStore", + "info": undefined, + "input": Object { + "dataFieldName": "analyticalStore", + "defaultValue": true, + "falseLabelTKey": "Disabled", + "labelTKey": "Analytical Store", + "placeholderTKey": undefined, + "trueLabelTKey": "Enabled", + "type": "boolean", + }, + }, + Object { + "id": "database", + "info": undefined, + "input": Object { + "choices": Array [ + Object { + "key": "db1", + "labelTKey": "Database 1", + }, + Object { + "key": "db2", + "labelTKey": "Database 2", + }, + Object { + "key": "db3", + "labelTKey": "Database 3", + }, + ], + "dataFieldName": "database", + "defaultKey": "db2", + "labelTKey": "Database", + "placeholderTKey": undefined, + "type": "object", + }, + }, + ], + "id": "root", + "info": Object { + "link": Object { + "href": "https://aka.ms/azure-cosmos-db-pricing", + "textTKey": "More Details", + }, + "messageTKey": "Start at $24/mo per database", + }, + }, + } + } + disabled={false} + getTranslation={[Function]} + onError={[Function]} + onInputChange={[Function]} + /> + +
`; exports[`SelfServeComponent message bar and spinner snapshots 3`] = ` - - - +
+ + + + + + + +
`; exports[`SelfServeComponent message bar and spinner snapshots 4`] = ` - sample error message + + sample error message + `; exports[`SelfServeComponent should render and honor save, discard, refresh actions 1`] = ` - - - +
+ + + + + + Object { + "disabled": false, + "hidden": false, + "value": 450, + }, + "analyticalStore" => Object { + "disabled": false, + "hidden": false, + "value": false, + }, + "database" => Object { + "disabled": false, + "hidden": false, + "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, + "labelTKey": "Throughput (input)", + "max": 500, + "min": 400, + "placeholderTKey": undefined, + "step": 10, + "type": "number", + "uiType": "Spinner", + }, + }, + Object { + "id": "containerId", + "info": undefined, + "input": Object { + "dataFieldName": "containerId", + "labelTKey": "Container id", + "placeholderTKey": undefined, + "type": "string", + }, + }, + Object { + "id": "analyticalStore", + "info": undefined, + "input": Object { + "dataFieldName": "analyticalStore", + "defaultValue": true, + "falseLabelTKey": "Disabled", + "labelTKey": "Analytical Store", + "placeholderTKey": undefined, + "trueLabelTKey": "Enabled", + "type": "boolean", + }, + }, + Object { + "id": "database", + "info": undefined, + "input": Object { + "choices": Array [ + Object { + "key": "db1", + "labelTKey": "Database 1", + }, + Object { + "key": "db2", + "labelTKey": "Database 2", + }, + Object { + "key": "db3", + "labelTKey": "Database 3", + }, + ], + "dataFieldName": "database", + "defaultKey": "db2", + "labelTKey": "Database", + "placeholderTKey": undefined, + "type": "object", + }, + }, + ], + "id": "root", + "info": Object { + "link": Object { + "href": "https://aka.ms/azure-cosmos-db-pricing", + "textTKey": "More Details", + }, + "messageTKey": "Start at $24/mo per database", + }, + }, + } + } + disabled={false} + getTranslation={[Function]} + onError={[Function]} + onInputChange={[Function]} + /> + +
`; diff --git a/src/Shared/Telemetry/TelemetryConstants.ts b/src/Shared/Telemetry/TelemetryConstants.ts index 9f28a87c5..f14cfaf7e 100644 --- a/src/Shared/Telemetry/TelemetryConstants.ts +++ b/src/Shared/Telemetry/TelemetryConstants.ts @@ -114,6 +114,7 @@ export enum Action { NotebooksGalleryPublicGalleryCount, NotebooksGalleryFavoritesCount, NotebooksGalleryPublishedCount, + SelfServe, } export const ActionModifiers = { diff --git a/src/i18n.ts b/src/i18n.ts index f91c17138..37d0d7cdf 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -1,22 +1,14 @@ 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: ",", @@ -29,3 +21,5 @@ i18n useSuspense: false, }, }); + +export default i18n;