mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-10 04:56:56 +00:00
Compare commits
17 Commits
aad-fix
...
v-yiqcao/n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b000115afe | ||
|
|
88d8200c14 | ||
|
|
6aaddd9c60 | ||
|
|
e6eb59a7b2 | ||
|
|
f8ede0cc1e | ||
|
|
bddb288a89 | ||
|
|
a14d20a88e | ||
|
|
f1db1ed978 | ||
|
|
86a483c3a4 | ||
|
|
263262a040 | ||
|
|
9590e8da3c | ||
|
|
bd4d8da065 | ||
|
|
1d2a7663f5 | ||
|
|
59ec18cd9b | ||
|
|
49bf8c60db | ||
|
|
b0b973b21a | ||
|
|
3529e80f0d |
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
@@ -9,6 +9,20 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
jobs:
|
jobs:
|
||||||
|
codemetrics:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: "Log Code Metrics"
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js 12.x
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 12.x
|
||||||
|
- run: npm ci
|
||||||
|
- run: node utils/codeMetrics.js
|
||||||
|
env:
|
||||||
|
CODE_METRICS_APP_ID: ${{ secrets.CODE_METRICS_APP_ID }}
|
||||||
compile:
|
compile:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: "Compile TypeScript"
|
name: "Compile TypeScript"
|
||||||
@@ -142,6 +156,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
npm start &
|
npm start &
|
||||||
|
node utils/cleanupDBs.js
|
||||||
npm run wait-for-server
|
npm run wait-for-server
|
||||||
npm run test:e2e
|
npm run test:e2e
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -1694,6 +1694,7 @@ input::-webkit-calendar-picker-indicator {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
max-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contextual-pane .paneErrorDetailsContainer {
|
.contextual-pane .paneErrorDetailsContainer {
|
||||||
@@ -2083,7 +2084,7 @@ a:link {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4247
package-lock.json
generated
4247
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -10,10 +10,10 @@
|
|||||||
"@azure/identity": "1.2.1",
|
"@azure/identity": "1.2.1",
|
||||||
"@babel/plugin-proposal-class-properties": "7.12.1",
|
"@babel/plugin-proposal-class-properties": "7.12.1",
|
||||||
"@babel/plugin-proposal-decorators": "7.12.12",
|
"@babel/plugin-proposal-decorators": "7.12.12",
|
||||||
"@jupyterlab/services": "6.0.0-rc.2",
|
"@jupyterlab/services": "6.0.2",
|
||||||
"@jupyterlab/terminal": "3.0.0-rc.2",
|
"@jupyterlab/terminal": "3.0.3",
|
||||||
"@microsoft/applicationinsights-web": "2.5.9",
|
"@microsoft/applicationinsights-web": "2.5.9",
|
||||||
"@nteract/commutable": "7.3.2",
|
"@nteract/commutable": "7.4.2",
|
||||||
"@nteract/connected-components": "6.8.2",
|
"@nteract/connected-components": "6.8.2",
|
||||||
"@nteract/core": "15.1.0",
|
"@nteract/core": "15.1.0",
|
||||||
"@nteract/data-explorer": "8.0.3",
|
"@nteract/data-explorer": "8.0.3",
|
||||||
@@ -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",
|
||||||
@@ -124,7 +128,7 @@
|
|||||||
"@types/prop-types": "15.5.8",
|
"@types/prop-types": "15.5.8",
|
||||||
"@types/puppeteer": "3.0.1",
|
"@types/puppeteer": "3.0.1",
|
||||||
"@types/q": "1.5.1",
|
"@types/q": "1.5.1",
|
||||||
"@types/react": "16.9.56",
|
"@types/react": "17.0.0",
|
||||||
"@types/react-dom": "17.0.0",
|
"@types/react-dom": "17.0.0",
|
||||||
"@types/react-notification-system": "0.2.39",
|
"@types/react-notification-system": "0.2.39",
|
||||||
"@types/react-redux": "7.1.7",
|
"@types/react-redux": "7.1.7",
|
||||||
@@ -151,6 +155,7 @@
|
|||||||
"eslint-plugin-prefer-arrow": "1.2.2",
|
"eslint-plugin-prefer-arrow": "1.2.2",
|
||||||
"eslint-plugin-react-hooks": "4.2.0",
|
"eslint-plugin-react-hooks": "4.2.0",
|
||||||
"expose-loader": "0.7.5",
|
"expose-loader": "0.7.5",
|
||||||
|
"fast-glob": "3.2.5",
|
||||||
"file-loader": "2.0.0",
|
"file-loader": "2.0.0",
|
||||||
"fs-extra": "7.0.0",
|
"fs-extra": "7.0.0",
|
||||||
"html-loader": "0.5.5",
|
"html-loader": "0.5.5",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export enum Platform {
|
|||||||
Emulator = "Emulator",
|
Emulator = "Emulator",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConfigContext {
|
export interface ConfigContext {
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
allowedParentFrameOrigins: string[];
|
allowedParentFrameOrigins: string[];
|
||||||
gitSha?: string;
|
gitSha?: string;
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export enum MessageTypes {
|
|||||||
CreateWorkspace,
|
CreateWorkspace,
|
||||||
CreateSparkPool,
|
CreateSparkPool,
|
||||||
RefreshDatabaseAccount,
|
RefreshDatabaseAccount,
|
||||||
InitTestExplorer,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Versions, ActionContracts, Diagnostics };
|
export { Versions, ActionContracts, Diagnostics };
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
TriggerDefinition,
|
TriggerDefinition,
|
||||||
UserDefinedFunctionDefinition,
|
UserDefinedFunctionDefinition,
|
||||||
} from "@azure/cosmos";
|
} from "@azure/cosmos";
|
||||||
import Q from "q";
|
|
||||||
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||||
import Explorer from "../Explorer/Explorer";
|
import Explorer from "../Explorer/Explorer";
|
||||||
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
@@ -109,7 +108,7 @@ export interface CollectionBase extends TreeNode {
|
|||||||
|
|
||||||
onDocumentDBDocumentsClick(): void;
|
onDocumentDBDocumentsClick(): void;
|
||||||
onNewQueryClick(source: any, event: MouseEvent, queryText?: string): void;
|
onNewQueryClick(source: any, event: MouseEvent, queryText?: string): void;
|
||||||
expandCollection(): Q.Promise<any>;
|
expandCollection(): void;
|
||||||
collapseCollection(): void;
|
collapseCollection(): void;
|
||||||
getDatabase(): Database;
|
getDatabase(): Database;
|
||||||
}
|
}
|
||||||
@@ -176,7 +175,7 @@ export interface Collection extends CollectionBase {
|
|||||||
|
|
||||||
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
|
onDragOver(source: Collection, event: { originalEvent: DragEvent }): void;
|
||||||
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
|
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
|
||||||
uploadFiles(fileList: FileList): Q.Promise<UploadDetails>;
|
uploadFiles(fileList: FileList): Promise<UploadDetails>;
|
||||||
|
|
||||||
getLabel(): string;
|
getLabel(): string;
|
||||||
}
|
}
|
||||||
@@ -294,7 +293,7 @@ export interface DocumentsTabOptions extends TabOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsTabV2Options extends TabOptions {
|
export interface SettingsTabV2Options extends TabOptions {
|
||||||
getPendingNotification: Q.Promise<DataModels.Notification>;
|
getPendingNotification: Promise<DataModels.Notification>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConflictsTabOptions extends TabOptions {
|
export interface ConflictsTabOptions extends TabOptions {
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
|
|||||||
}));
|
}));
|
||||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||||
import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||||
import Q from "q";
|
|
||||||
jest.mock("../../../Common/dataAccess/updateOffer", () => ({
|
jest.mock("../../../Common/dataAccess/updateOffer", () => ({
|
||||||
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer),
|
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer),
|
||||||
}));
|
}));
|
||||||
@@ -47,9 +46,7 @@ describe("SettingsComponent", () => {
|
|||||||
hashLocation: "settings",
|
hashLocation: "settings",
|
||||||
isActive: ko.observable(false),
|
isActive: ko.observable(false),
|
||||||
onUpdateTabsButtons: undefined,
|
onUpdateTabsButtons: undefined,
|
||||||
getPendingNotification: Q.Promise<DataModels.Notification>(() => {
|
getPendingNotification: Promise.resolve(undefined),
|
||||||
return;
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -958,7 +958,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isMongoIndexingEnabled": [Function],
|
"isMongoIndexingEnabled": [Function],
|
||||||
"isNotebookEnabled": [Function],
|
"isNotebookEnabled": [Function],
|
||||||
"isNotebooksEnabledForAccount": [Function],
|
"isNotebooksEnabledForAccount": [Function],
|
||||||
"isNotificationConsoleExpanded": [Function],
|
|
||||||
"isPreferredApiCassandra": [Function],
|
"isPreferredApiCassandra": [Function],
|
||||||
"isPreferredApiDocumentDB": [Function],
|
"isPreferredApiDocumentDB": [Function],
|
||||||
"isPreferredApiGraph": [Function],
|
"isPreferredApiGraph": [Function],
|
||||||
@@ -1018,12 +1017,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"nonSystemDatabases": [Function],
|
"nonSystemDatabases": [Function],
|
||||||
"notebookBasePath": [Function],
|
"notebookBasePath": [Function],
|
||||||
"notebookServerInfo": [Function],
|
"notebookServerInfo": [Function],
|
||||||
"notificationConsoleComponentAdapter": NotificationConsoleComponentAdapter {
|
|
||||||
"consoleData": [Function],
|
|
||||||
"container": [Circular],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
"notificationConsoleData": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"onSwitchToConnectionString": [Function],
|
"onSwitchToConnectionString": [Function],
|
||||||
@@ -1129,6 +1122,9 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
},
|
},
|
||||||
"selfServeType": [Function],
|
"selfServeType": [Function],
|
||||||
"serverId": [Function],
|
"serverId": [Function],
|
||||||
|
"setInProgressConsoleDataIdToBeDeleted": undefined,
|
||||||
|
"setIsNotificationConsoleExpanded": undefined,
|
||||||
|
"setNotificationConsoleData": undefined,
|
||||||
"settingsPane": SettingsPane {
|
"settingsPane": SettingsPane {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"crossPartitionQueryEnabled": [Function],
|
"crossPartitionQueryEnabled": [Function],
|
||||||
@@ -2241,7 +2237,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isMongoIndexingEnabled": [Function],
|
"isMongoIndexingEnabled": [Function],
|
||||||
"isNotebookEnabled": [Function],
|
"isNotebookEnabled": [Function],
|
||||||
"isNotebooksEnabledForAccount": [Function],
|
"isNotebooksEnabledForAccount": [Function],
|
||||||
"isNotificationConsoleExpanded": [Function],
|
|
||||||
"isPreferredApiCassandra": [Function],
|
"isPreferredApiCassandra": [Function],
|
||||||
"isPreferredApiDocumentDB": [Function],
|
"isPreferredApiDocumentDB": [Function],
|
||||||
"isPreferredApiGraph": [Function],
|
"isPreferredApiGraph": [Function],
|
||||||
@@ -2301,12 +2296,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"nonSystemDatabases": [Function],
|
"nonSystemDatabases": [Function],
|
||||||
"notebookBasePath": [Function],
|
"notebookBasePath": [Function],
|
||||||
"notebookServerInfo": [Function],
|
"notebookServerInfo": [Function],
|
||||||
"notificationConsoleComponentAdapter": NotificationConsoleComponentAdapter {
|
|
||||||
"consoleData": [Function],
|
|
||||||
"container": [Circular],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
"notificationConsoleData": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"onSwitchToConnectionString": [Function],
|
"onSwitchToConnectionString": [Function],
|
||||||
@@ -2412,6 +2401,9 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
},
|
},
|
||||||
"selfServeType": [Function],
|
"selfServeType": [Function],
|
||||||
"serverId": [Function],
|
"serverId": [Function],
|
||||||
|
"setInProgressConsoleDataIdToBeDeleted": undefined,
|
||||||
|
"setIsNotificationConsoleExpanded": undefined,
|
||||||
|
"setNotificationConsoleData": undefined,
|
||||||
"settingsPane": SettingsPane {
|
"settingsPane": SettingsPane {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"crossPartitionQueryEnabled": [Function],
|
"crossPartitionQueryEnabled": [Function],
|
||||||
@@ -3537,7 +3529,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isMongoIndexingEnabled": [Function],
|
"isMongoIndexingEnabled": [Function],
|
||||||
"isNotebookEnabled": [Function],
|
"isNotebookEnabled": [Function],
|
||||||
"isNotebooksEnabledForAccount": [Function],
|
"isNotebooksEnabledForAccount": [Function],
|
||||||
"isNotificationConsoleExpanded": [Function],
|
|
||||||
"isPreferredApiCassandra": [Function],
|
"isPreferredApiCassandra": [Function],
|
||||||
"isPreferredApiDocumentDB": [Function],
|
"isPreferredApiDocumentDB": [Function],
|
||||||
"isPreferredApiGraph": [Function],
|
"isPreferredApiGraph": [Function],
|
||||||
@@ -3597,12 +3588,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"nonSystemDatabases": [Function],
|
"nonSystemDatabases": [Function],
|
||||||
"notebookBasePath": [Function],
|
"notebookBasePath": [Function],
|
||||||
"notebookServerInfo": [Function],
|
"notebookServerInfo": [Function],
|
||||||
"notificationConsoleComponentAdapter": NotificationConsoleComponentAdapter {
|
|
||||||
"consoleData": [Function],
|
|
||||||
"container": [Circular],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
"notificationConsoleData": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"onSwitchToConnectionString": [Function],
|
"onSwitchToConnectionString": [Function],
|
||||||
@@ -3708,6 +3693,9 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
},
|
},
|
||||||
"selfServeType": [Function],
|
"selfServeType": [Function],
|
||||||
"serverId": [Function],
|
"serverId": [Function],
|
||||||
|
"setInProgressConsoleDataIdToBeDeleted": undefined,
|
||||||
|
"setIsNotificationConsoleExpanded": undefined,
|
||||||
|
"setNotificationConsoleData": undefined,
|
||||||
"settingsPane": SettingsPane {
|
"settingsPane": SettingsPane {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"crossPartitionQueryEnabled": [Function],
|
"crossPartitionQueryEnabled": [Function],
|
||||||
@@ -4820,7 +4808,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"isMongoIndexingEnabled": [Function],
|
"isMongoIndexingEnabled": [Function],
|
||||||
"isNotebookEnabled": [Function],
|
"isNotebookEnabled": [Function],
|
||||||
"isNotebooksEnabledForAccount": [Function],
|
"isNotebooksEnabledForAccount": [Function],
|
||||||
"isNotificationConsoleExpanded": [Function],
|
|
||||||
"isPreferredApiCassandra": [Function],
|
"isPreferredApiCassandra": [Function],
|
||||||
"isPreferredApiDocumentDB": [Function],
|
"isPreferredApiDocumentDB": [Function],
|
||||||
"isPreferredApiGraph": [Function],
|
"isPreferredApiGraph": [Function],
|
||||||
@@ -4880,12 +4867,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"nonSystemDatabases": [Function],
|
"nonSystemDatabases": [Function],
|
||||||
"notebookBasePath": [Function],
|
"notebookBasePath": [Function],
|
||||||
"notebookServerInfo": [Function],
|
"notebookServerInfo": [Function],
|
||||||
"notificationConsoleComponentAdapter": NotificationConsoleComponentAdapter {
|
|
||||||
"consoleData": [Function],
|
|
||||||
"container": [Circular],
|
|
||||||
"parameters": [Function],
|
|
||||||
},
|
|
||||||
"notificationConsoleData": [Function],
|
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
"onRefreshResourcesClick": [Function],
|
"onRefreshResourcesClick": [Function],
|
||||||
"onSwitchToConnectionString": [Function],
|
"onSwitchToConnectionString": [Function],
|
||||||
@@ -4991,6 +4972,9 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
},
|
},
|
||||||
"selfServeType": [Function],
|
"selfServeType": [Function],
|
||||||
"serverId": [Function],
|
"serverId": [Function],
|
||||||
|
"setInProgressConsoleDataIdToBeDeleted": undefined,
|
||||||
|
"setIsNotificationConsoleExpanded": undefined,
|
||||||
|
"setNotificationConsoleData": undefined,
|
||||||
"settingsPane": SettingsPane {
|
"settingsPane": SettingsPane {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
"crossPartitionQueryEnabled": [Function],
|
"crossPartitionQueryEnabled": [Function],
|
||||||
|
|||||||
@@ -1,63 +1,78 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { SmartUiComponent, SmartUiDescriptor, UiType } from "./SmartUiComponent";
|
import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent";
|
||||||
|
import { NumberUiType, SmartUiInput } from "../../../SelfServe/SelfServeTypes";
|
||||||
|
|
||||||
describe("SmartUiComponent", () => {
|
describe("SmartUiComponent", () => {
|
||||||
const exampleData: SmartUiDescriptor = {
|
const exampleData: SmartUiDescriptor = {
|
||||||
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: "description",
|
||||||
|
input: {
|
||||||
|
dataFieldName: "description",
|
||||||
|
type: "string",
|
||||||
|
description: {
|
||||||
|
textTKey: "this is an example description text.",
|
||||||
|
link: {
|
||||||
|
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
||||||
|
textTKey: "Click here for more information.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "throughput",
|
id: "throughput",
|
||||||
input: {
|
input: {
|
||||||
label: "Throughput (input)",
|
labelTKey: "Throughput (input)",
|
||||||
dataFieldName: "throughput",
|
dataFieldName: "throughput",
|
||||||
type: "number",
|
type: "number",
|
||||||
min: 400,
|
min: 400,
|
||||||
max: 500,
|
max: 500,
|
||||||
step: 10,
|
step: 10,
|
||||||
defaultValue: 400,
|
defaultValue: 400,
|
||||||
uiType: UiType.Spinner,
|
uiType: NumberUiType.Spinner,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "throughput2",
|
id: "throughput2",
|
||||||
input: {
|
input: {
|
||||||
label: "Throughput (Slider)",
|
labelTKey: "Throughput (Slider)",
|
||||||
dataFieldName: "throughput2",
|
dataFieldName: "throughput2",
|
||||||
type: "number",
|
type: "number",
|
||||||
min: 400,
|
min: 400,
|
||||||
max: 500,
|
max: 500,
|
||||||
step: 10,
|
step: 10,
|
||||||
defaultValue: 400,
|
defaultValue: 400,
|
||||||
uiType: UiType.Slider,
|
uiType: NumberUiType.Slider,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "throughput3",
|
id: "throughput3",
|
||||||
input: {
|
input: {
|
||||||
label: "Throughput (invalid)",
|
labelTKey: "Throughput (invalid)",
|
||||||
dataFieldName: "throughput3",
|
dataFieldName: "throughput3",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
min: 400,
|
min: 400,
|
||||||
max: 500,
|
max: 500,
|
||||||
step: 10,
|
step: 10,
|
||||||
defaultValue: 400,
|
defaultValue: 400,
|
||||||
uiType: UiType.Spinner,
|
uiType: NumberUiType.Spinner,
|
||||||
errorMessage: "label, truelabel and falselabel are required for boolean input 'throughput3'",
|
errorMessage: "label, truelabel and falselabel are required for boolean input 'throughput3'",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "containerId",
|
id: "containerId",
|
||||||
input: {
|
input: {
|
||||||
label: "Container id",
|
labelTKey: "Container id",
|
||||||
dataFieldName: "containerId",
|
dataFieldName: "containerId",
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
@@ -65,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",
|
||||||
@@ -76,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: [
|
||||||
@@ -91,11 +106,64 @@ describe("SmartUiComponent", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should render", async () => {
|
it("should render and honor input's hidden, disabled state", async () => {
|
||||||
|
const currentValues = new Map<string, SmartUiInput>();
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<SmartUiComponent descriptor={exampleData} currentValues={new Map()} onInputChange={undefined} />
|
<SmartUiComponent
|
||||||
|
disabled={false}
|
||||||
|
descriptor={exampleData}
|
||||||
|
currentValues={currentValues}
|
||||||
|
onInputChange={jest.fn()}
|
||||||
|
onError={() => {
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
getTranslation={(key: string) => {
|
||||||
|
return key;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
expect(wrapper.exists("#containerId-textField-input")).toBeTruthy();
|
||||||
|
|
||||||
|
currentValues.set("containerId", { value: "container1", hidden: true });
|
||||||
|
wrapper.setProps({ currentValues });
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.exists("#containerId-textField-input")).toBeFalsy();
|
||||||
|
|
||||||
|
currentValues.set("containerId", { value: "container1", hidden: false, disabled: true });
|
||||||
|
wrapper.setProps({ currentValues });
|
||||||
|
wrapper.update();
|
||||||
|
const containerIdTextField = wrapper.find("#containerId-textField-input");
|
||||||
|
expect(containerIdTextField.props().disabled).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disable all inputs", async () => {
|
||||||
|
const wrapper = shallow(
|
||||||
|
<SmartUiComponent
|
||||||
|
disabled={true}
|
||||||
|
descriptor={exampleData}
|
||||||
|
currentValues={new Map()}
|
||||||
|
onInputChange={jest.fn()}
|
||||||
|
onError={() => {
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
getTranslation={(key: string) => {
|
||||||
|
return key;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
const throughputSpinner = wrapper.find("#throughput-spinner-input");
|
||||||
|
expect(throughputSpinner.props().disabled).toBeTruthy();
|
||||||
|
const throughput2Slider = wrapper.find("#throughput2-slider-input").childAt(0);
|
||||||
|
expect(throughput2Slider.props().disabled).toBeTruthy();
|
||||||
|
const containerIdTextField = wrapper.find("#containerId-textField-input");
|
||||||
|
expect(containerIdTextField.props().disabled).toBeTruthy();
|
||||||
|
const analyticalStoreToggle = wrapper.find("#analyticalStore-toggle-input");
|
||||||
|
expect(analyticalStoreToggle.props().disabled).toBeTruthy();
|
||||||
|
const databaseDropdown = wrapper.find("#database-dropdown-input");
|
||||||
|
expect(databaseDropdown.props().disabled).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,11 +5,20 @@ import { SpinButton } from "office-ui-fabric-react/lib/SpinButton";
|
|||||||
import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
|
import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
|
||||||
import { TextField } from "office-ui-fabric-react/lib/TextField";
|
import { TextField } from "office-ui-fabric-react/lib/TextField";
|
||||||
import { Text } from "office-ui-fabric-react/lib/Text";
|
import { Text } from "office-ui-fabric-react/lib/Text";
|
||||||
import { RadioSwitchComponent } from "../RadioSwitchComponent/RadioSwitchComponent";
|
|
||||||
import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack";
|
import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack";
|
||||||
import { Link, MessageBar, MessageBarType } from "office-ui-fabric-react";
|
import { Link, MessageBar, MessageBarType, Toggle } from "office-ui-fabric-react";
|
||||||
import * as InputUtils from "./InputUtils";
|
import * as InputUtils from "./InputUtils";
|
||||||
import "./SmartUiComponent.less";
|
import "./SmartUiComponent.less";
|
||||||
|
import {
|
||||||
|
ChoiceItem,
|
||||||
|
Description,
|
||||||
|
Info,
|
||||||
|
InputType,
|
||||||
|
InputTypeValue,
|
||||||
|
NumberUiType,
|
||||||
|
SmartUiInput,
|
||||||
|
} from "../../../SelfServe/SelfServeTypes";
|
||||||
|
import { TFunction } from "i18next";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic UX renderer
|
* Generic UX renderer
|
||||||
@@ -19,30 +28,15 @@ import "./SmartUiComponent.less";
|
|||||||
* - a descriptor of the UX.
|
* - a descriptor of the UX.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type InputTypeValue = "number" | "string" | "boolean" | "object";
|
interface BaseDisplay {
|
||||||
|
|
||||||
export enum UiType {
|
|
||||||
Spinner = "Spinner",
|
|
||||||
Slider = "Slider",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ChoiceItem = { label: string; key: string };
|
|
||||||
|
|
||||||
export type InputType = number | string | boolean | ChoiceItem;
|
|
||||||
|
|
||||||
export interface Info {
|
|
||||||
message: string;
|
|
||||||
link?: {
|
|
||||||
href: string;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BaseInput {
|
|
||||||
label: string;
|
|
||||||
dataFieldName: string;
|
dataFieldName: string;
|
||||||
|
errorMessage?: string;
|
||||||
type: InputTypeValue;
|
type: InputTypeValue;
|
||||||
placeholder?: string;
|
}
|
||||||
|
|
||||||
|
interface BaseInput extends BaseDisplay {
|
||||||
|
labelTKey: string;
|
||||||
|
placeholderTKey?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,12 +48,12 @@ interface NumberInput extends BaseInput {
|
|||||||
max: number;
|
max: number;
|
||||||
step: number;
|
step: number;
|
||||||
defaultValue?: number;
|
defaultValue?: number;
|
||||||
uiType: UiType;
|
uiType: NumberUiType;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BooleanInput extends BaseInput {
|
interface BooleanInput extends BaseInput {
|
||||||
trueLabel: string;
|
trueLabelTKey: string;
|
||||||
falseLabel: string;
|
falseLabelTKey: string;
|
||||||
defaultValue?: boolean;
|
defaultValue?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,12 +66,16 @@ interface ChoiceInput extends BaseInput {
|
|||||||
defaultKey?: string;
|
defaultKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AnyInput = NumberInput | BooleanInput | StringInput | ChoiceInput;
|
interface DescriptionDisplay extends BaseDisplay {
|
||||||
|
description: Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
|
||||||
|
|
||||||
interface Node {
|
interface Node {
|
||||||
id: string;
|
id: string;
|
||||||
info?: Info;
|
info?: Info;
|
||||||
input?: AnyInput;
|
input?: AnyDisplay;
|
||||||
children?: Node[];
|
children?: Node[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,11 +84,13 @@ export interface SmartUiDescriptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/************************** Component implementation starts here ************************************* */
|
/************************** Component implementation starts here ************************************* */
|
||||||
|
|
||||||
export interface SmartUiComponentProps {
|
export interface SmartUiComponentProps {
|
||||||
descriptor: SmartUiDescriptor;
|
descriptor: SmartUiDescriptor;
|
||||||
currentValues: Map<string, InputType>;
|
currentValues: Map<string, SmartUiInput>;
|
||||||
onInputChange: (input: AnyInput, newValue: InputType) => void;
|
onInputChange: (input: AnyDisplay, newValue: InputType) => void;
|
||||||
|
onError: (hasError: boolean) => void;
|
||||||
|
disabled: boolean;
|
||||||
|
getTranslation: TFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SmartUiComponentState {
|
interface SmartUiComponentState {
|
||||||
@@ -98,12 +98,22 @@ interface SmartUiComponentState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SmartUiComponent extends React.Component<SmartUiComponentProps, SmartUiComponentState> {
|
export class SmartUiComponent extends React.Component<SmartUiComponentProps, SmartUiComponentState> {
|
||||||
|
private shouldCheckErrors = true;
|
||||||
private static readonly labelStyle = {
|
private static readonly labelStyle = {
|
||||||
color: "#393939",
|
color: "#393939",
|
||||||
fontFamily: "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
fontFamily: "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
componentDidUpdate(): void {
|
||||||
|
if (!this.shouldCheckErrors) {
|
||||||
|
this.shouldCheckErrors = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.props.onError(this.state.errors.size > 0);
|
||||||
|
this.shouldCheckErrors = false;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: SmartUiComponentProps) {
|
constructor(props: SmartUiComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -113,11 +123,11 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
|
|
||||||
private renderInfo(info: Info): JSX.Element {
|
private renderInfo(info: Info): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<MessageBar>
|
<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>
|
||||||
@@ -125,17 +135,20 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderTextInput(input: StringInput): JSX.Element {
|
private renderTextInput(input: StringInput): JSX.Element {
|
||||||
const value = this.props.currentValues.get(input.dataFieldName) as string;
|
const value = this.props.currentValues.get(input.dataFieldName)?.value as string;
|
||||||
|
const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
|
||||||
return (
|
return (
|
||||||
<div className="stringInputContainer">
|
<div className="stringInputContainer">
|
||||||
<TextField
|
<TextField
|
||||||
id={`${input.dataFieldName}-textBox-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}
|
||||||
onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
|
onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
|
||||||
styles={{
|
styles={{
|
||||||
|
root: { width: 400 },
|
||||||
subComponentStyles: {
|
subComponentStyles: {
|
||||||
label: {
|
label: {
|
||||||
root: {
|
root: {
|
||||||
@@ -150,13 +163,27 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderDescription(input: DescriptionDisplay): JSX.Element {
|
||||||
|
const description = input.description;
|
||||||
|
return (
|
||||||
|
<Text id={`${input.dataFieldName}-text-display`}>
|
||||||
|
{this.props.getTranslation(input.description.textTKey)}{" "}
|
||||||
|
{description.link && (
|
||||||
|
<Link target="_blank" href={input.description.link.href}>
|
||||||
|
{this.props.getTranslation(input.description.link.textTKey)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private clearError(dataFieldName: string): void {
|
private clearError(dataFieldName: string): void {
|
||||||
const { errors } = this.state;
|
const { errors } = this.state;
|
||||||
errors.delete(dataFieldName);
|
errors.delete(dataFieldName);
|
||||||
this.setState({ errors });
|
this.setState({ errors });
|
||||||
}
|
}
|
||||||
|
|
||||||
private onValidate = (input: AnyInput, value: string, min: number, max: number): string => {
|
private onValidate = (input: NumberInput, value: string, min: number, max: number): string => {
|
||||||
const newValue = InputUtils.onValidateValueChange(value, min, max);
|
const newValue = InputUtils.onValidateValueChange(value, min, max);
|
||||||
const dataFieldName = input.dataFieldName;
|
const dataFieldName = input.dataFieldName;
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
@@ -165,13 +192,13 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
return newValue.toString();
|
return newValue.toString();
|
||||||
} else {
|
} else {
|
||||||
const { errors } = this.state;
|
const { errors } = this.state;
|
||||||
errors.set(dataFieldName, `Invalid value ${value}: must be between ${min} and ${max}`);
|
errors.set(dataFieldName, `Invalid value '${value}'. It must be between ${min} and ${max}`);
|
||||||
this.setState({ errors });
|
this.setState({ errors });
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
private onIncrement = (input: AnyInput, value: string, step: number, max: number): string => {
|
private onIncrement = (input: NumberInput, value: string, step: number, max: number): string => {
|
||||||
const newValue = InputUtils.onIncrementValue(value, step, max);
|
const newValue = InputUtils.onIncrementValue(value, step, max);
|
||||||
const dataFieldName = input.dataFieldName;
|
const dataFieldName = input.dataFieldName;
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
@@ -182,7 +209,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
private onDecrement = (input: AnyInput, value: string, step: number, min: number): string => {
|
private onDecrement = (input: NumberInput, value: string, step: number, min: number): string => {
|
||||||
const newValue = InputUtils.onDecrementValue(value, step, min);
|
const newValue = InputUtils.onDecrementValue(value, step, min);
|
||||||
const dataFieldName = input.dataFieldName;
|
const dataFieldName = input.dataFieldName;
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
@@ -194,19 +221,20 @@ 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
const value = this.props.currentValues.get(dataFieldName) as number;
|
const value = this.props.currentValues.get(dataFieldName)?.value as number;
|
||||||
if (input.uiType === UiType.Spinner) {
|
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
|
||||||
|
if (input.uiType === NumberUiType.Spinner) {
|
||||||
return (
|
return (
|
||||||
<>
|
<Stack styles={{ root: { width: 400 } }} tokens={{ childrenGap: 2 }}>
|
||||||
<SpinButton
|
<SpinButton
|
||||||
{...props}
|
{...props}
|
||||||
id={`${input.dataFieldName}-spinner-input`}
|
id={`${input.dataFieldName}-spinner-input`}
|
||||||
@@ -215,6 +243,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
onIncrement={(newValue) => this.onIncrement(input, newValue, props.step, props.max)}
|
onIncrement={(newValue) => this.onIncrement(input, newValue, props.step, props.max)}
|
||||||
onDecrement={(newValue) => this.onDecrement(input, newValue, props.step, props.min)}
|
onDecrement={(newValue) => this.onDecrement(input, newValue, props.step, props.min)}
|
||||||
labelPosition={Position.top}
|
labelPosition={Position.top}
|
||||||
|
disabled={disabled}
|
||||||
styles={{
|
styles={{
|
||||||
label: {
|
label: {
|
||||||
...SmartUiComponent.labelStyle,
|
...SmartUiComponent.labelStyle,
|
||||||
@@ -225,16 +254,18 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
{this.state.errors.has(dataFieldName) && (
|
{this.state.errors.has(dataFieldName) && (
|
||||||
<MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar>
|
<MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar>
|
||||||
)}
|
)}
|
||||||
</>
|
</Stack>
|
||||||
);
|
);
|
||||||
} else if (input.uiType === UiType.Slider) {
|
} else if (input.uiType === NumberUiType.Slider) {
|
||||||
return (
|
return (
|
||||||
<div id={`${input.dataFieldName}-slider-input`}>
|
<div id={`${input.dataFieldName}-slider-input`}>
|
||||||
<Slider
|
<Slider
|
||||||
{...props}
|
{...props}
|
||||||
value={value}
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
onChange={(newValue) => this.props.onInputChange(input, newValue)}
|
onChange={(newValue) => this.props.onInputChange(input, newValue)}
|
||||||
styles={{
|
styles={{
|
||||||
|
root: { width: 400 },
|
||||||
titleLabel: {
|
titleLabel: {
|
||||||
...SmartUiComponent.labelStyle,
|
...SmartUiComponent.labelStyle,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
@@ -250,49 +281,44 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderBooleanInput(input: BooleanInput): JSX.Element {
|
private renderBooleanInput(input: BooleanInput): JSX.Element {
|
||||||
const value = this.props.currentValues.get(input.dataFieldName) as boolean;
|
const value = this.props.currentValues.get(input.dataFieldName)?.value as boolean;
|
||||||
const selectedKey = value || input.defaultValue ? "true" : "false";
|
const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
|
||||||
return (
|
return (
|
||||||
<div id={`${input.dataFieldName}-radioSwitch-input`}>
|
<Toggle
|
||||||
<div className="inputLabelContainer">
|
id={`${input.dataFieldName}-toggle-input`}
|
||||||
<Text variant="small" nowrap className="inputLabel">
|
label={this.props.getTranslation(input.labelTKey)}
|
||||||
{input.label}
|
checked={value || false}
|
||||||
</Text>
|
onText={this.props.getTranslation(input.trueLabelTKey)}
|
||||||
</div>
|
offText={this.props.getTranslation(input.falseLabelTKey)}
|
||||||
<RadioSwitchComponent
|
disabled={disabled}
|
||||||
choices={[
|
onChange={(event, checked: boolean) => this.props.onInputChange(input, checked)}
|
||||||
{
|
styles={{ root: { width: 400 } }}
|
||||||
label: input.falseLabel,
|
|
||||||
key: "false",
|
|
||||||
onSelect: () => this.props.onInputChange(input, false),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: input.trueLabel,
|
|
||||||
key: "true",
|
|
||||||
onSelect: () => this.props.onInputChange(input, true),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
selectedKey={selectedKey}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) as string;
|
const value = this.props.currentValues.get(dataFieldName)?.value as string;
|
||||||
|
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
|
||||||
|
let selectedKey = value ? value : defaultKey;
|
||||||
|
if (!selectedKey) {
|
||||||
|
selectedKey = "";
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
id={`${input.dataFieldName}-dropown-input`}
|
id={`${input.dataFieldName}-dropdown-input`}
|
||||||
label={label}
|
label={this.props.getTranslation(labelTKey)}
|
||||||
selectedKey={value ? value : defaultKey}
|
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}
|
||||||
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 },
|
||||||
label: {
|
label: {
|
||||||
...SmartUiComponent.labelStyle,
|
...SmartUiComponent.labelStyle,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
@@ -303,16 +329,23 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderError(input: AnyInput): JSX.Element {
|
private renderError(input: AnyDisplay): JSX.Element {
|
||||||
return <MessageBar messageBarType={MessageBarType.error}>Error: {input.errorMessage}</MessageBar>;
|
return <MessageBar messageBarType={MessageBarType.error}>Error: {input.errorMessage}</MessageBar>;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderInput(input: AnyInput): JSX.Element {
|
private renderDisplay(input: AnyDisplay): JSX.Element {
|
||||||
if (input.errorMessage) {
|
if (input.errorMessage) {
|
||||||
return this.renderError(input);
|
return this.renderError(input);
|
||||||
}
|
}
|
||||||
|
const inputHidden = this.props.currentValues.get(input.dataFieldName)?.hidden;
|
||||||
|
if (inputHidden) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
switch (input.type) {
|
switch (input.type) {
|
||||||
case "string":
|
case "string":
|
||||||
|
if ("description" in input) {
|
||||||
|
return this.renderDescription(input as DescriptionDisplay);
|
||||||
|
}
|
||||||
return this.renderTextInput(input as StringInput);
|
return this.renderTextInput(input as StringInput);
|
||||||
case "number":
|
case "number":
|
||||||
return this.renderNumberInput(input as NumberInput);
|
return this.renderNumberInput(input as NumberInput);
|
||||||
@@ -326,13 +359,13 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderNode(node: Node): JSX.Element {
|
private renderNode(node: Node): JSX.Element {
|
||||||
const containerStackTokens: IStackTokens = { childrenGap: 15 };
|
const containerStackTokens: IStackTokens = { childrenGap: 10 };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack tokens={containerStackTokens} className="widgetRendererContainer">
|
<Stack tokens={containerStackTokens} className="widgetRendererContainer">
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
{node.info && this.renderInfo(node.info as Info)}
|
{node.info && this.renderInfo(node.info as Info)}
|
||||||
{node.input && this.renderInput(node.input)}
|
{node.input && this.renderDisplay(node.input)}
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
{node.children && node.children.map((child) => <div key={child.id}>{this.renderNode(child)}</div>)}
|
{node.children && node.children.map((child) => <div key={child.id}>{this.renderNode(child)}</div>)}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -340,11 +373,6 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
}
|
}
|
||||||
|
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
const containerStackTokens: IStackTokens = { childrenGap: 20 };
|
return this.renderNode(this.props.descriptor.root);
|
||||||
return (
|
|
||||||
<Stack tokens={containerStackTokens} styles={{ root: { width: 400, padding: 10 } }}>
|
|
||||||
{this.renderNode(this.props.descriptor.root)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,24 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`SmartUiComponent should render 1`] = `
|
exports[`SmartUiComponent disable all inputs 1`] = `
|
||||||
<Stack
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<StyledMessageBarBase
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"padding": 10,
|
|
||||||
"width": 400,
|
"width": 400,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tokens={
|
|
||||||
Object {
|
|
||||||
"childrenGap": 20,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Stack
|
|
||||||
className="widgetRendererContainer"
|
|
||||||
tokens={
|
|
||||||
Object {
|
|
||||||
"childrenGap": 15,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<StackItem>
|
|
||||||
<StyledMessageBarBase>
|
|
||||||
Start at $24/mo per database
|
Start at $24/mo per database
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://aka.ms/azure-cosmos-db-pricing"
|
href="https://aka.ms/azure-cosmos-db-pricing"
|
||||||
@@ -35,6 +28,33 @@ exports[`SmartUiComponent should render 1`] = `
|
|||||||
</StyledLinkBase>
|
</StyledLinkBase>
|
||||||
</StyledMessageBarBase>
|
</StyledMessageBarBase>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
|
<div
|
||||||
|
key="description"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<Text
|
||||||
|
id="description-text-display"
|
||||||
|
>
|
||||||
|
this is an example description text.
|
||||||
|
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Click here for more information.
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
key="throughput"
|
key="throughput"
|
||||||
>
|
>
|
||||||
@@ -42,11 +62,344 @@ exports[`SmartUiComponent should render 1`] = `
|
|||||||
className="widgetRendererContainer"
|
className="widgetRendererContainer"
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 15,
|
"childrenGap": 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
|
<Stack
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CustomizedSpinButton
|
||||||
|
ariaLabel="Throughput (input)"
|
||||||
|
decrementButtonIcon={
|
||||||
|
Object {
|
||||||
|
"iconName": "ChevronDownSmall",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disabled={true}
|
||||||
|
id="throughput-spinner-input"
|
||||||
|
incrementButtonIcon={
|
||||||
|
Object {
|
||||||
|
"iconName": "ChevronUpSmall",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
label="Throughput (input)"
|
||||||
|
labelPosition={0}
|
||||||
|
max={500}
|
||||||
|
min={400}
|
||||||
|
onDecrement={[Function]}
|
||||||
|
onIncrement={[Function]}
|
||||||
|
onValidate={[Function]}
|
||||||
|
step={10}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"label": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 600,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="throughput2"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<div
|
||||||
|
id="throughput2-slider-input"
|
||||||
|
>
|
||||||
|
<StyledSliderBase
|
||||||
|
ariaLabel="Throughput (Slider)"
|
||||||
|
disabled={true}
|
||||||
|
label="Throughput (Slider)"
|
||||||
|
max={500}
|
||||||
|
min={400}
|
||||||
|
onChange={[Function]}
|
||||||
|
step={10}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
"titleLabel": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 600,
|
||||||
|
},
|
||||||
|
"valueLabel": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="throughput3"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<StyledMessageBarBase
|
||||||
|
messageBarType={1}
|
||||||
|
>
|
||||||
|
Error:
|
||||||
|
label, truelabel and falselabel are required for boolean input 'throughput3'
|
||||||
|
</StyledMessageBarBase>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="containerId"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<div
|
||||||
|
className="stringInputContainer"
|
||||||
|
>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
disabled={true}
|
||||||
|
id="containerId-textField-input"
|
||||||
|
label="Container id"
|
||||||
|
onChange={[Function]}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
"subComponentStyles": Object {
|
||||||
|
"label": Object {
|
||||||
|
"root": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="analyticalStore"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<StyledToggleBase
|
||||||
|
checked={false}
|
||||||
|
disabled={true}
|
||||||
|
id="analyticalStore-toggle-input"
|
||||||
|
label="Analytical Store"
|
||||||
|
offText="Disabled"
|
||||||
|
onChange={[Function]}
|
||||||
|
onText="Enabled"
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="database"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<StyledWithResponsiveMode
|
||||||
|
disabled={true}
|
||||||
|
id="database-dropdown-input"
|
||||||
|
label="Database"
|
||||||
|
onChange={[Function]}
|
||||||
|
options={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"key": "db1",
|
||||||
|
"text": "Database 1",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "db2",
|
||||||
|
"text": "Database 2",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "db3",
|
||||||
|
"text": "Database 3",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
selectedKey="db2"
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"dropdown": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
"label": Object {
|
||||||
|
"color": "#393939",
|
||||||
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
|
"fontSize": 12,
|
||||||
|
"fontWeight": 600,
|
||||||
|
},
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`SmartUiComponent should render and honor input's hidden, disabled state 1`] = `
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<StyledMessageBarBase
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Start at $24/mo per database
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://aka.ms/azure-cosmos-db-pricing"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
More Details
|
||||||
|
</StyledLinkBase>
|
||||||
|
</StyledMessageBarBase>
|
||||||
|
</StackItem>
|
||||||
|
<div
|
||||||
|
key="description"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<Text
|
||||||
|
id="description-text-display"
|
||||||
|
>
|
||||||
|
this is an example description text.
|
||||||
|
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://docs.microsoft.com/en-us/azure/cosmos-db/introduction"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Click here for more information.
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="throughput"
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
className="widgetRendererContainer"
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<Stack
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
<CustomizedSpinButton
|
<CustomizedSpinButton
|
||||||
ariaLabel="Throughput (input)"
|
ariaLabel="Throughput (input)"
|
||||||
decrementButtonIcon={
|
decrementButtonIcon={
|
||||||
@@ -80,6 +433,7 @@ exports[`SmartUiComponent should render 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</Stack>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,7 +444,7 @@ exports[`SmartUiComponent should render 1`] = `
|
|||||||
className="widgetRendererContainer"
|
className="widgetRendererContainer"
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 15,
|
"childrenGap": 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -107,6 +461,9 @@ exports[`SmartUiComponent should render 1`] = `
|
|||||||
step={10}
|
step={10}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
"titleLabel": Object {
|
"titleLabel": Object {
|
||||||
"color": "#393939",
|
"color": "#393939",
|
||||||
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||||
@@ -132,7 +489,7 @@ exports[`SmartUiComponent should render 1`] = `
|
|||||||
className="widgetRendererContainer"
|
className="widgetRendererContainer"
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 15,
|
"childrenGap": 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -153,7 +510,7 @@ exports[`SmartUiComponent should render 1`] = `
|
|||||||
className="widgetRendererContainer"
|
className="widgetRendererContainer"
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 15,
|
"childrenGap": 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -162,11 +519,14 @@ exports[`SmartUiComponent should render 1`] = `
|
|||||||
className="stringInputContainer"
|
className="stringInputContainer"
|
||||||
>
|
>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
id="containerId-textBox-input"
|
id="containerId-textField-input"
|
||||||
label="Container id"
|
label="Container id"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
"subComponentStyles": Object {
|
"subComponentStyles": Object {
|
||||||
"label": Object {
|
"label": Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
@@ -180,6 +540,7 @@ exports[`SmartUiComponent should render 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
type="text"
|
type="text"
|
||||||
|
value=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
@@ -192,43 +553,26 @@ exports[`SmartUiComponent should render 1`] = `
|
|||||||
className="widgetRendererContainer"
|
className="widgetRendererContainer"
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 15,
|
"childrenGap": 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<div
|
<StyledToggleBase
|
||||||
id="analyticalStore-radioSwitch-input"
|
checked={false}
|
||||||
>
|
id="analyticalStore-toggle-input"
|
||||||
<div
|
label="Analytical Store"
|
||||||
className="inputLabelContainer"
|
offText="Disabled"
|
||||||
>
|
onChange={[Function]}
|
||||||
<Text
|
onText="Enabled"
|
||||||
className="inputLabel"
|
styles={
|
||||||
nowrap={true}
|
|
||||||
variant="small"
|
|
||||||
>
|
|
||||||
Analytical Store
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<RadioSwitchComponent
|
|
||||||
choices={
|
|
||||||
Array [
|
|
||||||
Object {
|
Object {
|
||||||
"key": "false",
|
"root": Object {
|
||||||
"label": "Disabled",
|
"width": 400,
|
||||||
"onSelect": [Function],
|
|
||||||
},
|
},
|
||||||
Object {
|
|
||||||
"key": "true",
|
|
||||||
"label": "Enabled",
|
|
||||||
"onSelect": [Function],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
selectedKey="true"
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
@@ -239,13 +583,13 @@ exports[`SmartUiComponent should render 1`] = `
|
|||||||
className="widgetRendererContainer"
|
className="widgetRendererContainer"
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 15,
|
"childrenGap": 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<StyledWithResponsiveMode
|
<StyledWithResponsiveMode
|
||||||
id="database-dropown-input"
|
id="database-dropdown-input"
|
||||||
label="Database"
|
label="Database"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
options={
|
options={
|
||||||
@@ -278,12 +622,14 @@ exports[`SmartUiComponent should render 1`] = `
|
|||||||
"fontSize": 12,
|
"fontSize": 12,
|
||||||
"fontWeight": 600,
|
"fontWeight": 600,
|
||||||
},
|
},
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ import { sendMessage, sendCachedDataMessage, handleCachedDataMessage } from "../
|
|||||||
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
|
||||||
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
import { NotebookUtil } from "./Notebook/NotebookUtil";
|
||||||
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
|
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
|
||||||
import { NotificationConsoleComponentAdapter } from "./Menus/NotificationConsole/NotificationConsoleComponentAdapter";
|
|
||||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||||
import { QueriesClient } from "../Common/QueriesClient";
|
import { QueriesClient } from "../Common/QueriesClient";
|
||||||
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
|
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
|
||||||
@@ -107,6 +106,12 @@ interface AdHocAccessData {
|
|||||||
readUrl: string;
|
readUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExplorerParams {
|
||||||
|
setIsNotificationConsoleExpanded: (isExpanded: boolean) => void;
|
||||||
|
setNotificationConsoleData: (consoleData: ConsoleData) => void;
|
||||||
|
setInProgressConsoleDataIdToBeDeleted: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export default class Explorer {
|
export default class Explorer {
|
||||||
public flight: ko.Observable<string> = ko.observable<string>(
|
public flight: ko.Observable<string> = ko.observable<string>(
|
||||||
SharedConstants.CollectionCreation.DefaultAddCollectionDefaultFlight
|
SharedConstants.CollectionCreation.DefaultAddCollectionDefaultFlight
|
||||||
@@ -146,8 +151,9 @@ export default class Explorer {
|
|||||||
public mostRecentActivity: MostRecentActivity.MostRecentActivity;
|
public mostRecentActivity: MostRecentActivity.MostRecentActivity;
|
||||||
|
|
||||||
// Notification Console
|
// Notification Console
|
||||||
public notificationConsoleData: ko.ObservableArray<ConsoleData>;
|
private setIsNotificationConsoleExpanded: (isExpanded: boolean) => void;
|
||||||
public isNotificationConsoleExpanded: ko.Observable<boolean>;
|
private setNotificationConsoleData: (consoleData: ConsoleData) => void;
|
||||||
|
private setInProgressConsoleDataIdToBeDeleted: (id: string) => void;
|
||||||
|
|
||||||
// Panes
|
// Panes
|
||||||
public contextPanes: ContextualPaneBase[];
|
public contextPanes: ContextualPaneBase[];
|
||||||
@@ -260,7 +266,6 @@ export default class Explorer {
|
|||||||
// React adapters
|
// React adapters
|
||||||
private commandBarComponentAdapter: CommandBarComponentAdapter;
|
private commandBarComponentAdapter: CommandBarComponentAdapter;
|
||||||
private splashScreenAdapter: SplashScreenComponentAdapter;
|
private splashScreenAdapter: SplashScreenComponentAdapter;
|
||||||
private notificationConsoleComponentAdapter: NotificationConsoleComponentAdapter;
|
|
||||||
private dialogComponentAdapter: DialogComponentAdapter;
|
private dialogComponentAdapter: DialogComponentAdapter;
|
||||||
private _dialogProps: ko.Observable<DialogProps>;
|
private _dialogProps: ko.Observable<DialogProps>;
|
||||||
private addSynapseLinkDialog: DialogComponentAdapter;
|
private addSynapseLinkDialog: DialogComponentAdapter;
|
||||||
@@ -269,7 +274,11 @@ export default class Explorer {
|
|||||||
|
|
||||||
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
private static readonly MaxNbDatabasesToAutoExpand = 5;
|
||||||
|
|
||||||
constructor() {
|
constructor(params?: ExplorerParams) {
|
||||||
|
this.setIsNotificationConsoleExpanded = params?.setIsNotificationConsoleExpanded;
|
||||||
|
this.setNotificationConsoleData = params?.setNotificationConsoleData;
|
||||||
|
this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted;
|
||||||
|
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
});
|
});
|
||||||
@@ -430,7 +439,6 @@ export default class Explorer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema));
|
this.isSchemaEnabled = ko.computed<boolean>(() => this.isFeatureEnabled(Constants.Features.enableSchema));
|
||||||
this.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
|
||||||
|
|
||||||
this.isAutoscaleDefaultEnabled = ko.observable<boolean>(false);
|
this.isAutoscaleDefaultEnabled = ko.observable<boolean>(false);
|
||||||
|
|
||||||
@@ -478,7 +486,6 @@ export default class Explorer {
|
|||||||
bounds: splitterBounds,
|
bounds: splitterBounds,
|
||||||
direction: SplitterDirection.Vertical,
|
direction: SplitterDirection.Vertical,
|
||||||
});
|
});
|
||||||
this.notificationConsoleData = ko.observableArray<ConsoleData>([]);
|
|
||||||
this.defaultExperience = ko.observable<string>();
|
this.defaultExperience = ko.observable<string>();
|
||||||
this.databaseAccount.subscribe((databaseAccount) => {
|
this.databaseAccount.subscribe((databaseAccount) => {
|
||||||
const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(
|
const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(
|
||||||
@@ -892,7 +899,6 @@ export default class Explorer {
|
|||||||
|
|
||||||
this.commandBarComponentAdapter = new CommandBarComponentAdapter(this);
|
this.commandBarComponentAdapter = new CommandBarComponentAdapter(this);
|
||||||
this.selfServeLoadingComponentAdapter = new SelfServeLoadingComponentAdapter();
|
this.selfServeLoadingComponentAdapter = new SelfServeLoadingComponentAdapter();
|
||||||
this.notificationConsoleComponentAdapter = new NotificationConsoleComponentAdapter(this);
|
|
||||||
|
|
||||||
this._initSettings();
|
this._initSettings();
|
||||||
|
|
||||||
@@ -1349,23 +1355,19 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public logConsoleData(consoleData: ConsoleData): void {
|
public logConsoleData(consoleData: ConsoleData): void {
|
||||||
this.notificationConsoleData.splice(0, 0, consoleData);
|
this.setNotificationConsoleData(consoleData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteInProgressConsoleDataWithId(id: string): void {
|
public deleteInProgressConsoleDataWithId(id: string): void {
|
||||||
const updatedConsoleData = _.reject(
|
this.setInProgressConsoleDataIdToBeDeleted(id);
|
||||||
this.notificationConsoleData(),
|
|
||||||
(data: ConsoleData) => data.type === ConsoleDataType.InProgress && data.id === id
|
|
||||||
);
|
|
||||||
this.notificationConsoleData(updatedConsoleData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public expandConsole(): void {
|
public expandConsole(): void {
|
||||||
this.isNotificationConsoleExpanded(true);
|
this.setIsNotificationConsoleExpanded(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public collapseConsole(): void {
|
public collapseConsole(): void {
|
||||||
this.isNotificationConsoleExpanded(false);
|
this.setIsNotificationConsoleExpanded(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public toggleLeftPaneExpanded() {
|
public toggleLeftPaneExpanded() {
|
||||||
@@ -1718,58 +1720,7 @@ export default class Explorer {
|
|||||||
this._addSynapseLinkDialogProps.valueHasMutated();
|
this._addSynapseLinkDialogProps.valueHasMutated();
|
||||||
};
|
};
|
||||||
|
|
||||||
private _shouldProcessMessage(event: MessageEvent): boolean {
|
public handleMessage(message: any) {
|
||||||
if (typeof event.data !== "object") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (event.data["signature"] !== "pcIframe") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!("data" in event.data)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (typeof event.data["data"] !== "object") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// before initialization completed give exception
|
|
||||||
const message = event.data.data;
|
|
||||||
if (!this._importExplorerConfigComplete && message && message.type) {
|
|
||||||
const messageType = message.type;
|
|
||||||
switch (messageType) {
|
|
||||||
case MessageTypes.SendNotification:
|
|
||||||
case MessageTypes.ClearNotification:
|
|
||||||
case MessageTypes.LoadingStatus:
|
|
||||||
case MessageTypes.InitTestExplorer:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!("inputs" in event.data["data"]) && !this._importExplorerConfigComplete) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleMessage(event: MessageEvent) {
|
|
||||||
if (isInvalidParentFrameOrigin(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._shouldProcessMessage(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message: any = event.data.data;
|
|
||||||
const inputs: ViewModels.DataExplorerInputsFrame = message.inputs;
|
|
||||||
|
|
||||||
const isRunningInPortal = configContext.platform === Platform.Portal;
|
|
||||||
const isRunningInDevMode = process.env.NODE_ENV === "development";
|
|
||||||
if (inputs && configContext.BACKEND_ENDPOINT && isRunningInPortal && isRunningInDevMode) {
|
|
||||||
inputs.extensionEndpoint = configContext.PROXY_PATH;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.initDataExplorerWithFrameInputs(inputs);
|
|
||||||
|
|
||||||
const openAction: ActionContracts.DataExplorerAction = message.openAction;
|
const openAction: ActionContracts.DataExplorerAction = message.openAction;
|
||||||
if (!!openAction) {
|
if (!!openAction) {
|
||||||
if (this.isRefreshingExplorer()) {
|
if (this.isRefreshingExplorer()) {
|
||||||
@@ -1874,7 +1825,7 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): void {
|
public configure(inputs: ViewModels.DataExplorerInputsFrame): void {
|
||||||
if (inputs != null) {
|
if (inputs != null) {
|
||||||
// In development mode, save the iframe message from the portal in session storage.
|
// In development mode, save the iframe message from the portal in session storage.
|
||||||
// This allows webpack hot reload to funciton properly
|
// This allows webpack hot reload to funciton properly
|
||||||
|
|||||||
@@ -36,7 +36,36 @@ export class CommandBarComponentButtonFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newCollectionBtn = CommandBarComponentButtonFactory.createNewCollectionGroup(container);
|
const newCollectionBtn = CommandBarComponentButtonFactory.createNewCollectionGroup(container);
|
||||||
const buttons: CommandButtonComponentProps[] = [newCollectionBtn];
|
const buttons: CommandButtonComponentProps[] = [];
|
||||||
|
|
||||||
|
if (container.isFeatureEnabled && container.isFeatureEnabled("regionselectbutton")) {
|
||||||
|
const regions = [{ name: "West US" }, { name: "East US" }, { name: "North Europe" }];
|
||||||
|
buttons.push({
|
||||||
|
iconSrc: null,
|
||||||
|
onCommandClick: () => {},
|
||||||
|
commandButtonLabel: null,
|
||||||
|
hasPopup: false,
|
||||||
|
isDropdown: true,
|
||||||
|
dropdownPlaceholder: "West US",
|
||||||
|
dropdownSelectedKey: "West US",
|
||||||
|
dropdownWidth: 100,
|
||||||
|
children: regions.map(
|
||||||
|
(region) =>
|
||||||
|
({
|
||||||
|
iconSrc: null,
|
||||||
|
onCommandClick: () => {},
|
||||||
|
commandButtonLabel: region.name,
|
||||||
|
dropdownItemKey: region.name,
|
||||||
|
hasPopup: false,
|
||||||
|
disabled: false,
|
||||||
|
ariaLabel: "",
|
||||||
|
} as CommandButtonComponentProps)
|
||||||
|
),
|
||||||
|
ariaLabel: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buttons.push(newCollectionBtn);
|
||||||
|
|
||||||
const addSynapseLink = CommandBarComponentButtonFactory.createOpenSynapseLinkDialogButton(container);
|
const addSynapseLink = CommandBarComponentButtonFactory.createOpenSynapseLinkDialogButton(container);
|
||||||
if (addSynapseLink) {
|
if (addSynapseLink) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React from "react";
|
|||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import {
|
import {
|
||||||
NotificationConsoleComponentProps,
|
NotificationConsoleComponentProps,
|
||||||
ConsoleData,
|
|
||||||
NotificationConsoleComponent,
|
NotificationConsoleComponent,
|
||||||
ConsoleDataType,
|
ConsoleDataType,
|
||||||
} from "./NotificationConsoleComponent";
|
} from "./NotificationConsoleComponent";
|
||||||
@@ -10,38 +9,40 @@ import {
|
|||||||
describe("NotificationConsoleComponent", () => {
|
describe("NotificationConsoleComponent", () => {
|
||||||
const createBlankProps = (): NotificationConsoleComponentProps => {
|
const createBlankProps = (): NotificationConsoleComponentProps => {
|
||||||
return {
|
return {
|
||||||
consoleData: [],
|
consoleData: undefined,
|
||||||
isConsoleExpanded: true,
|
isConsoleExpanded: false,
|
||||||
onConsoleDataChange: (consoleData: ConsoleData[]) => {},
|
inProgressConsoleDataIdToBeDeleted: "",
|
||||||
onConsoleExpandedChange: (isExpanded: boolean) => {},
|
setIsConsoleExpanded: (isExpanded: boolean): void => {},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
it("renders the console (expanded)", () => {
|
it("renders the console", () => {
|
||||||
const props = createBlankProps();
|
const props = createBlankProps();
|
||||||
props.consoleData.push({
|
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
|
props.consoleData = {
|
||||||
type: ConsoleDataType.Info,
|
type: ConsoleDataType.Info,
|
||||||
date: "date",
|
date: "date",
|
||||||
message: "message",
|
message: "message",
|
||||||
});
|
};
|
||||||
|
wrapper.setProps(props);
|
||||||
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows proper progress count", () => {
|
it("shows proper progress count", () => {
|
||||||
const count = 100;
|
const count = 100;
|
||||||
const props = createBlankProps();
|
const props = createBlankProps();
|
||||||
|
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
props.consoleData.push({
|
props.consoleData = {
|
||||||
type: ConsoleDataType.InProgress,
|
type: ConsoleDataType.InProgress,
|
||||||
date: "date",
|
date: "date" + i,
|
||||||
message: "message",
|
message: "message",
|
||||||
});
|
};
|
||||||
|
wrapper.setProps(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
|
||||||
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual(count.toString());
|
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual(count.toString());
|
||||||
expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual("0");
|
expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual("0");
|
||||||
expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual("0");
|
expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual("0");
|
||||||
@@ -50,16 +51,17 @@ describe("NotificationConsoleComponent", () => {
|
|||||||
it("shows proper error count", () => {
|
it("shows proper error count", () => {
|
||||||
const count = 100;
|
const count = 100;
|
||||||
const props = createBlankProps();
|
const props = createBlankProps();
|
||||||
|
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
props.consoleData.push({
|
props.consoleData = {
|
||||||
type: ConsoleDataType.Error,
|
type: ConsoleDataType.Error,
|
||||||
date: "date",
|
date: "date" + i,
|
||||||
message: "message",
|
message: "message",
|
||||||
});
|
};
|
||||||
|
wrapper.setProps(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
|
||||||
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("0");
|
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("0");
|
||||||
expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual(count.toString());
|
expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual(count.toString());
|
||||||
expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual("0");
|
expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual("0");
|
||||||
@@ -68,31 +70,34 @@ describe("NotificationConsoleComponent", () => {
|
|||||||
it("shows proper info count", () => {
|
it("shows proper info count", () => {
|
||||||
const count = 100;
|
const count = 100;
|
||||||
const props = createBlankProps();
|
const props = createBlankProps();
|
||||||
|
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
props.consoleData.push({
|
props.consoleData = {
|
||||||
type: ConsoleDataType.Info,
|
type: ConsoleDataType.Info,
|
||||||
date: "date",
|
date: "date" + i,
|
||||||
message: "message",
|
message: "message",
|
||||||
});
|
};
|
||||||
|
wrapper.setProps(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
|
||||||
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("0");
|
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("0");
|
||||||
expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual("0");
|
expect(wrapper.find(".notificationConsoleHeader .numErroredItems").text()).toEqual("0");
|
||||||
expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual(count.toString());
|
expect(wrapper.find(".notificationConsoleHeader .numInfoItems").text()).toEqual(count.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
const testRenderNotification = (date: string, msg: string, type: ConsoleDataType, iconClassName: string) => {
|
const testRenderNotification = (date: string, message: string, type: ConsoleDataType, iconClassName: string) => {
|
||||||
const props = createBlankProps();
|
const props = createBlankProps();
|
||||||
props.consoleData.push({
|
|
||||||
date: date,
|
|
||||||
message: msg,
|
|
||||||
type: type,
|
|
||||||
});
|
|
||||||
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
||||||
|
|
||||||
|
props.consoleData = {
|
||||||
|
type,
|
||||||
|
date,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
wrapper.setProps(props);
|
||||||
expect(wrapper.find(".notificationConsoleData .date").text()).toEqual(date);
|
expect(wrapper.find(".notificationConsoleData .date").text()).toEqual(date);
|
||||||
expect(wrapper.find(".notificationConsoleData .message").text()).toEqual(msg);
|
expect(wrapper.find(".notificationConsoleData .message").text()).toEqual(message);
|
||||||
expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`));
|
expect(wrapper.exists(`.notificationConsoleData .${iconClassName}`));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -110,55 +115,78 @@ describe("NotificationConsoleComponent", () => {
|
|||||||
|
|
||||||
it("clears notifications", () => {
|
it("clears notifications", () => {
|
||||||
const props = createBlankProps();
|
const props = createBlankProps();
|
||||||
props.consoleData.push({
|
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
||||||
|
|
||||||
|
props.consoleData = {
|
||||||
type: ConsoleDataType.InProgress,
|
type: ConsoleDataType.InProgress,
|
||||||
date: "date",
|
date: "date",
|
||||||
message: "message1",
|
message: "message1",
|
||||||
});
|
};
|
||||||
props.consoleData.push({
|
wrapper.setProps(props);
|
||||||
|
|
||||||
|
props.consoleData = {
|
||||||
type: ConsoleDataType.Error,
|
type: ConsoleDataType.Error,
|
||||||
date: "date",
|
date: "date",
|
||||||
message: "message2",
|
message: "message2",
|
||||||
});
|
};
|
||||||
props.consoleData.push({
|
wrapper.setProps(props);
|
||||||
|
|
||||||
|
props.consoleData = {
|
||||||
type: ConsoleDataType.Info,
|
type: ConsoleDataType.Info,
|
||||||
date: "date",
|
date: "date",
|
||||||
message: "message3",
|
message: "message3",
|
||||||
});
|
};
|
||||||
|
wrapper.setProps(props);
|
||||||
|
|
||||||
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
|
||||||
wrapper.find(".clearNotificationsButton").simulate("click");
|
wrapper.find(".clearNotificationsButton").simulate("click");
|
||||||
|
|
||||||
expect(!wrapper.exists(".notificationConsoleData"));
|
expect(!wrapper.exists(".notificationConsoleData"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("collapses and hide content", () => {
|
it("collapses and hide content", () => {
|
||||||
const props = createBlankProps();
|
const props = createBlankProps();
|
||||||
props.consoleData.push({
|
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
||||||
|
|
||||||
|
props.consoleData = {
|
||||||
|
type: ConsoleDataType.Info,
|
||||||
date: "date",
|
date: "date",
|
||||||
message: "message",
|
message: "message",
|
||||||
type: ConsoleDataType.Info,
|
};
|
||||||
});
|
|
||||||
props.isConsoleExpanded = true;
|
props.isConsoleExpanded = true;
|
||||||
|
wrapper.setProps(props);
|
||||||
|
|
||||||
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
|
||||||
wrapper.find(".notificationConsoleHeader").simulate("click");
|
wrapper.find(".notificationConsoleHeader").simulate("click");
|
||||||
expect(!wrapper.exists(".notificationConsoleContent"));
|
expect(!wrapper.exists(".notificationConsoleContent"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("display latest data in header", () => {
|
it("display latest data in header", () => {
|
||||||
const latestData = "latest data";
|
const latestData = "latest data";
|
||||||
const props1 = createBlankProps();
|
const props = createBlankProps();
|
||||||
const props2 = createBlankProps();
|
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
||||||
props2.consoleData.push({
|
|
||||||
|
props.consoleData = {
|
||||||
|
type: ConsoleDataType.Info,
|
||||||
date: "date",
|
date: "date",
|
||||||
message: latestData,
|
message: latestData,
|
||||||
type: ConsoleDataType.Info,
|
};
|
||||||
});
|
props.isConsoleExpanded = true;
|
||||||
props2.isConsoleExpanded = true;
|
wrapper.setProps(props);
|
||||||
|
|
||||||
const wrapper = shallow(<NotificationConsoleComponent {...props1} />);
|
|
||||||
wrapper.setProps(props2);
|
|
||||||
expect(wrapper.find(".headerStatusEllipsis").text()).toEqual(latestData);
|
expect(wrapper.find(".headerStatusEllipsis").text()).toEqual(latestData);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("delete in progress message", () => {
|
||||||
|
const props = createBlankProps();
|
||||||
|
props.consoleData = {
|
||||||
|
type: ConsoleDataType.InProgress,
|
||||||
|
date: "date",
|
||||||
|
message: "message",
|
||||||
|
id: "1",
|
||||||
|
};
|
||||||
|
const wrapper = shallow(<NotificationConsoleComponent {...props} />);
|
||||||
|
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("1");
|
||||||
|
|
||||||
|
props.inProgressConsoleDataIdToBeDeleted = "1";
|
||||||
|
wrapper.setProps(props);
|
||||||
|
expect(wrapper.find(".notificationConsoleHeader .numInProgress").text()).toEqual("0");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,15 +37,15 @@ export interface ConsoleData {
|
|||||||
|
|
||||||
export interface NotificationConsoleComponentProps {
|
export interface NotificationConsoleComponentProps {
|
||||||
isConsoleExpanded: boolean;
|
isConsoleExpanded: boolean;
|
||||||
onConsoleExpandedChange: (isExpanded: boolean) => void;
|
consoleData: ConsoleData;
|
||||||
consoleData: ConsoleData[];
|
inProgressConsoleDataIdToBeDeleted: string;
|
||||||
onConsoleDataChange: (consoleData: ConsoleData[]) => void;
|
setIsConsoleExpanded: (isExpanded: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NotificationConsoleComponentState {
|
interface NotificationConsoleComponentState {
|
||||||
headerStatus: string;
|
headerStatus: string;
|
||||||
selectedFilter: string;
|
selectedFilter: string;
|
||||||
isExpanded: boolean;
|
allConsoleData: ConsoleData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotificationConsoleComponent extends React.Component<
|
export class NotificationConsoleComponent extends React.Component<
|
||||||
@@ -60,28 +60,28 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
{ key: "Error", text: "Error" },
|
{ key: "Error", text: "Error" },
|
||||||
];
|
];
|
||||||
private headerTimeoutId?: number;
|
private headerTimeoutId?: number;
|
||||||
private prevHeaderStatus: string | null;
|
private prevHeaderStatus: string;
|
||||||
private consoleHeaderElement?: HTMLElement;
|
private consoleHeaderElement?: HTMLElement;
|
||||||
|
|
||||||
constructor(props: NotificationConsoleComponentProps) {
|
constructor(props: NotificationConsoleComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
headerStatus: "",
|
headerStatus: undefined,
|
||||||
selectedFilter: NotificationConsoleComponent.FilterOptions[0].key || "",
|
selectedFilter: NotificationConsoleComponent.FilterOptions[0].key,
|
||||||
isExpanded: props.isConsoleExpanded,
|
allConsoleData: props.consoleData ? [props.consoleData] : [],
|
||||||
};
|
};
|
||||||
this.prevHeaderStatus = null;
|
this.prevHeaderStatus = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(
|
public componentDidUpdate(
|
||||||
prevProps: NotificationConsoleComponentProps,
|
prevProps: NotificationConsoleComponentProps,
|
||||||
prevState: NotificationConsoleComponentState
|
prevState: NotificationConsoleComponentState
|
||||||
) {
|
) {
|
||||||
const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props);
|
const currentHeaderStatus = NotificationConsoleComponent.extractHeaderStatus(this.props.consoleData);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.prevHeaderStatus !== currentHeaderStatus &&
|
this.prevHeaderStatus !== currentHeaderStatus &&
|
||||||
currentHeaderStatus !== null &&
|
currentHeaderStatus !== undefined &&
|
||||||
prevState.headerStatus !== currentHeaderStatus
|
prevState.headerStatus !== currentHeaderStatus
|
||||||
) {
|
) {
|
||||||
this.setHeaderStatus(currentHeaderStatus);
|
this.setHeaderStatus(currentHeaderStatus);
|
||||||
@@ -92,10 +92,8 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
// updates: currentHeaderStatus -> "" -> currentHeaderStatus -> "" etc.
|
// updates: currentHeaderStatus -> "" -> currentHeaderStatus -> "" etc.
|
||||||
this.prevHeaderStatus = currentHeaderStatus;
|
this.prevHeaderStatus = currentHeaderStatus;
|
||||||
|
|
||||||
if (prevProps.isConsoleExpanded !== this.props.isConsoleExpanded) {
|
if (this.props.consoleData || this.props.inProgressConsoleDataIdToBeDeleted) {
|
||||||
// Sync state and props
|
this.updateConsoleData(prevProps);
|
||||||
// TODO react anti-pattern: remove isExpanded from state which duplicates prop's isConsoleExpanded
|
|
||||||
this.setState({ isExpanded: this.props.isConsoleExpanded });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,12 +102,14 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
};
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const numInProgress = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.InProgress)
|
const numInProgress = this.state.allConsoleData.filter(
|
||||||
|
(data: ConsoleData) => data.type === ConsoleDataType.InProgress
|
||||||
|
).length;
|
||||||
|
const numErroredItems = this.state.allConsoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.Error)
|
||||||
.length;
|
.length;
|
||||||
const numErroredItems = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.Error)
|
const numInfoItems = this.state.allConsoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.Info)
|
||||||
.length;
|
|
||||||
const numInfoItems = this.props.consoleData.filter((data: ConsoleData) => data.type === ConsoleDataType.Info)
|
|
||||||
.length;
|
.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="notificationConsoleContainer">
|
<div className="notificationConsoleContainer">
|
||||||
<div
|
<div
|
||||||
@@ -143,18 +143,18 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
className="expandCollapseButton"
|
className="expandCollapseButton"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
aria-label={"console button" + (this.state.isExpanded ? " collapsed" : " expanded")}
|
aria-label={"console button" + (this.props.isConsoleExpanded ? " collapsed" : " expanded")}
|
||||||
aria-expanded={!this.state.isExpanded}
|
aria-expanded={!this.props.isConsoleExpanded}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={this.state.isExpanded ? ChevronDownIcon : ChevronUpIcon}
|
src={this.props.isConsoleExpanded ? ChevronDownIcon : ChevronUpIcon}
|
||||||
alt={this.state.isExpanded ? "ChevronDownIcon" : "ChevronUpIcon"}
|
alt={this.props.isConsoleExpanded ? "ChevronDownIcon" : "ChevronUpIcon"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AnimateHeight
|
<AnimateHeight
|
||||||
duration={NotificationConsoleComponent.transitionDurationMs}
|
duration={NotificationConsoleComponent.transitionDurationMs}
|
||||||
height={this.state.isExpanded ? "auto" : 0}
|
height={this.props.isConsoleExpanded ? "auto" : 0}
|
||||||
onAnimationEnd={this.onConsoleWasExpanded}
|
onAnimationEnd={this.onConsoleWasExpanded}
|
||||||
>
|
>
|
||||||
<div className="notificationConsoleContents">
|
<div className="notificationConsoleContents">
|
||||||
@@ -189,7 +189,7 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
private expandCollapseConsole() {
|
private expandCollapseConsole() {
|
||||||
this.setState({ isExpanded: !this.state.isExpanded });
|
this.props.setIsConsoleExpanded(!this.props.isConsoleExpanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onExpandCollapseKeyPress = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
private onExpandCollapseKeyPress = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||||
@@ -209,7 +209,7 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
};
|
};
|
||||||
|
|
||||||
private clearNotifications(): void {
|
private clearNotifications(): void {
|
||||||
this.props.onConsoleDataChange([]);
|
this.setState({ allConsoleData: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderAllFilteredConsoleData(rowData: ConsoleData[]): JSX.Element[] {
|
private renderAllFilteredConsoleData(rowData: ConsoleData[]): JSX.Element[] {
|
||||||
@@ -229,12 +229,9 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
};
|
};
|
||||||
|
|
||||||
private getFilteredConsoleData(): ConsoleData[] {
|
private getFilteredConsoleData(): ConsoleData[] {
|
||||||
let filterType: ConsoleDataType | null = null;
|
let filterType: ConsoleDataType;
|
||||||
|
|
||||||
switch (this.state.selectedFilter) {
|
switch (this.state.selectedFilter) {
|
||||||
case "All":
|
|
||||||
filterType = null;
|
|
||||||
break;
|
|
||||||
case "In Progress":
|
case "In Progress":
|
||||||
filterType = ConsoleDataType.InProgress;
|
filterType = ConsoleDataType.InProgress;
|
||||||
break;
|
break;
|
||||||
@@ -245,12 +242,12 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
filterType = ConsoleDataType.Error;
|
filterType = ConsoleDataType.Error;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
filterType = null;
|
filterType = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return filterType == null
|
return filterType
|
||||||
? this.props.consoleData
|
? this.state.allConsoleData.filter((data: ConsoleData) => data.type === filterType)
|
||||||
: this.props.consoleData.filter((data: ConsoleData) => data.type === filterType);
|
: this.state.allConsoleData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setHeaderStatus(statusMessage: string): void {
|
private setHeaderStatus(statusMessage: string): void {
|
||||||
@@ -266,18 +263,43 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static extractHeaderStatus(props: NotificationConsoleComponentProps) {
|
private static extractHeaderStatus(consoleData: ConsoleData) {
|
||||||
if (props.consoleData && props.consoleData.length > 0) {
|
return consoleData?.message.split(":\n")[0];
|
||||||
return props.consoleData[0].message.split(":\n")[0];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onConsoleWasExpanded = (): void => {
|
private onConsoleWasExpanded = (): void => {
|
||||||
this.props.onConsoleExpandedChange(this.state.isExpanded);
|
if (this.props.isConsoleExpanded && this.consoleHeaderElement) {
|
||||||
if (this.state.isExpanded && this.consoleHeaderElement) {
|
|
||||||
this.consoleHeaderElement.focus();
|
this.consoleHeaderElement.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private updateConsoleData = (prevProps: NotificationConsoleComponentProps): void => {
|
||||||
|
if (!this.areConsoleDataEqual(this.props.consoleData, prevProps.consoleData)) {
|
||||||
|
this.setState({ allConsoleData: [this.props.consoleData, ...this.state.allConsoleData] });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.props.inProgressConsoleDataIdToBeDeleted &&
|
||||||
|
prevProps.inProgressConsoleDataIdToBeDeleted !== this.props.inProgressConsoleDataIdToBeDeleted
|
||||||
|
) {
|
||||||
|
const allConsoleData = this.state.allConsoleData.filter(
|
||||||
|
(data: ConsoleData) =>
|
||||||
|
!(data.type === ConsoleDataType.InProgress && data.id === this.props.inProgressConsoleDataIdToBeDeleted)
|
||||||
|
);
|
||||||
|
this.setState({ allConsoleData });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private areConsoleDataEqual = (currentData: ConsoleData, prevData: ConsoleData): boolean => {
|
||||||
|
if (!currentData || !prevData) {
|
||||||
|
return !currentData && !prevData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
currentData.date === prevData.date &&
|
||||||
|
currentData.message === prevData.message &&
|
||||||
|
currentData.type === prevData.type &&
|
||||||
|
currentData.id === prevData.id
|
||||||
|
);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
import * as ko from "knockout";
|
|
||||||
import * as React from "react";
|
|
||||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
|
||||||
import { NotificationConsoleComponent } from "./NotificationConsoleComponent";
|
|
||||||
import { ConsoleData } from "./NotificationConsoleComponent";
|
|
||||||
import Explorer from "../../Explorer";
|
|
||||||
|
|
||||||
export class NotificationConsoleComponentAdapter implements ReactAdapter {
|
|
||||||
public parameters: ko.Observable<number>;
|
|
||||||
public container: Explorer;
|
|
||||||
private consoleData: ko.ObservableArray<ConsoleData>;
|
|
||||||
|
|
||||||
constructor(container: Explorer) {
|
|
||||||
this.container = container;
|
|
||||||
|
|
||||||
this.consoleData = container.notificationConsoleData;
|
|
||||||
this.consoleData.subscribe((newValue: ConsoleData[]) => this.triggerRender());
|
|
||||||
container.isNotificationConsoleExpanded.subscribe(() => this.triggerRender());
|
|
||||||
this.parameters = ko.observable(Date.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
private onConsoleExpandedChange(isExpanded: boolean): void {
|
|
||||||
isExpanded ? this.container.expandConsole() : this.container.collapseConsole();
|
|
||||||
this.triggerRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
private onConsoleDataChange(consoleData: ConsoleData[]): void {
|
|
||||||
this.consoleData(consoleData);
|
|
||||||
this.triggerRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
|
||||||
return (
|
|
||||||
<NotificationConsoleComponent
|
|
||||||
isConsoleExpanded={this.container.isNotificationConsoleExpanded()}
|
|
||||||
onConsoleExpandedChange={this.onConsoleExpandedChange.bind(this)}
|
|
||||||
consoleData={this.consoleData()}
|
|
||||||
onConsoleDataChange={this.onConsoleDataChange.bind(this)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private triggerRender() {
|
|
||||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,169 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
|
exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||||
|
<div
|
||||||
|
className="notificationConsoleContainer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="notificationConsoleHeader"
|
||||||
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="statusBar"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="dataTypeIcons"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="notificationConsoleHeaderIconWithData"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="in progress items"
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="numInProgress"
|
||||||
|
>
|
||||||
|
0
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="notificationConsoleHeaderIconWithData"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="error items"
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="numErroredItems"
|
||||||
|
>
|
||||||
|
0
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="notificationConsoleHeaderIconWithData"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="info items"
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="numInfoItems"
|
||||||
|
>
|
||||||
|
0
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="consoleSplitter"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="headerStatus"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="headerStatusEllipsis"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
aria-expanded={true}
|
||||||
|
aria-label="console button expanded"
|
||||||
|
className="expandCollapseButton"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="ChevronUpIcon"
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AnimateHeight
|
||||||
|
animateOpacity={false}
|
||||||
|
animationStateClasses={
|
||||||
|
Object {
|
||||||
|
"animating": "rah-animating",
|
||||||
|
"animatingDown": "rah-animating--down",
|
||||||
|
"animatingToHeightAuto": "rah-animating--to-height-auto",
|
||||||
|
"animatingToHeightSpecific": "rah-animating--to-height-specific",
|
||||||
|
"animatingToHeightZero": "rah-animating--to-height-zero",
|
||||||
|
"animatingUp": "rah-animating--up",
|
||||||
|
"static": "rah-static",
|
||||||
|
"staticHeightAuto": "rah-static--height-auto",
|
||||||
|
"staticHeightSpecific": "rah-static--height-specific",
|
||||||
|
"staticHeightZero": "rah-static--height-zero",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyInlineTransitions={true}
|
||||||
|
delay={0}
|
||||||
|
duration={200}
|
||||||
|
easing="ease"
|
||||||
|
height={0}
|
||||||
|
onAnimationEnd={[Function]}
|
||||||
|
style={Object {}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="notificationConsoleContents"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="notificationConsoleControls"
|
||||||
|
>
|
||||||
|
<StyledWithResponsiveMode
|
||||||
|
aria-label="All"
|
||||||
|
aria-labelledby="consoleFilterLabel"
|
||||||
|
label="Filter:"
|
||||||
|
onChange={[Function]}
|
||||||
|
options={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"key": "All",
|
||||||
|
"text": "All",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "In Progress",
|
||||||
|
"text": "In progress",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "Info",
|
||||||
|
"text": "Info",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "Error",
|
||||||
|
"text": "Error",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
role="combobox"
|
||||||
|
selectedKey="All"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="consoleSplitter"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="clearNotificationsButton"
|
||||||
|
onClick={[Function]}
|
||||||
|
onKeyDown={[Function]}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="clear notifications image"
|
||||||
|
src=""
|
||||||
|
/>
|
||||||
|
Clear Notifications
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="notificationConsoleData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AnimateHeight>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||||
<div
|
<div
|
||||||
className="notificationConsoleContainer"
|
className="notificationConsoleContainer"
|
||||||
>
|
>
|
||||||
@@ -64,18 +227,20 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="headerStatusEllipsis"
|
className="headerStatusEllipsis"
|
||||||
/>
|
>
|
||||||
|
message
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
aria-expanded={false}
|
aria-expanded={true}
|
||||||
aria-label="console button collapsed"
|
aria-label="console button expanded"
|
||||||
className="expandCollapseButton"
|
className="expandCollapseButton"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="ChevronDownIcon"
|
alt="ChevronUpIcon"
|
||||||
src=""
|
src=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,7 +265,7 @@ exports[`NotificationConsoleComponent renders the console (expanded) 1`] = `
|
|||||||
delay={0}
|
delay={0}
|
||||||
duration={200}
|
duration={200}
|
||||||
easing="ease"
|
easing="ease"
|
||||||
height="auto"
|
height={0}
|
||||||
onAnimationEnd={[Function]}
|
onAnimationEnd={[Function]}
|
||||||
style={Object {}}
|
style={Object {}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -41,16 +41,16 @@ export class NotebookContainerClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
|
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
|
||||||
|
if (this.isResettingWorkspace) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.notebookServerInfo() || !this.notebookServerInfo().notebookServerEndpoint) {
|
if (!this.notebookServerInfo() || !this.notebookServerInfo().notebookServerEndpoint) {
|
||||||
const error = "No server endpoint detected";
|
const error = "No server endpoint detected";
|
||||||
Logger.logError(error, "NotebookContainerClient/getMemoryUsage");
|
Logger.logError(error, "NotebookContainerClient/getMemoryUsage");
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isResettingWorkspace) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
|
const { notebookServerEndpoint, authToken } = this.getNotebookServerConfig();
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${notebookServerEndpoint}/api/metrics/memory`, {
|
const response = await fetch(`${notebookServerEndpoint}/api/metrics/memory`, {
|
||||||
|
|||||||
@@ -45,8 +45,9 @@ type NotebookRendererProps = NotebookRendererBaseProps & NotebookRendererDispatc
|
|||||||
|
|
||||||
const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, children: React.ReactNode) => {
|
const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, children: React.ReactNode) => {
|
||||||
const Cell = () => (
|
const Cell = () => (
|
||||||
<DraggableCell id={id} contentRef={contentRef}>
|
// TODO Draggable and HijackScroll not working anymore. Fix or remove when reworking MarkdownCell.
|
||||||
<HijackScroll id={id} contentRef={contentRef}>
|
// <DraggableCell id={id} contentRef={contentRef}>
|
||||||
|
// <HijackScroll id={id} contentRef={contentRef}>
|
||||||
<CellCreator id={id} contentRef={contentRef}>
|
<CellCreator id={id} contentRef={contentRef}>
|
||||||
<CellLabeler id={id} contentRef={contentRef}>
|
<CellLabeler id={id} contentRef={contentRef}>
|
||||||
<HoverableCell id={id} contentRef={contentRef}>
|
<HoverableCell id={id} contentRef={contentRef}>
|
||||||
@@ -54,8 +55,8 @@ const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, child
|
|||||||
</HoverableCell>
|
</HoverableCell>
|
||||||
</CellLabeler>
|
</CellLabeler>
|
||||||
</CellCreator>
|
</CellCreator>
|
||||||
</HijackScroll>
|
// </HijackScroll>
|
||||||
</DraggableCell>
|
// </DraggableCell>
|
||||||
);
|
);
|
||||||
|
|
||||||
Cell.defaultProps = { cell_type };
|
Cell.defaultProps = { cell_type };
|
||||||
|
|||||||
@@ -818,7 +818,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
|||||||
let indexingPolicy: DataModels.IndexingPolicy;
|
let indexingPolicy: DataModels.IndexingPolicy;
|
||||||
let createMongoWildcardIndex: boolean;
|
let createMongoWildcardIndex: boolean;
|
||||||
// todo - remove mongo indexing policy ticket # 616274
|
// todo - remove mongo indexing policy ticket # 616274
|
||||||
if (this.container.isPreferredApiMongoDB()) {
|
if (this.container.isPreferredApiMongoDB() && this.container.isEnableMongoCapabilityPresent()) {
|
||||||
createMongoWildcardIndex = this.shouldCreateMongoWildcardIndex();
|
createMongoWildcardIndex = this.shouldCreateMongoWildcardIndex();
|
||||||
} else if (this.showIndexingOptionsForSharedThroughput()) {
|
} else if (this.showIndexingOptionsForSharedThroughput()) {
|
||||||
if (this.useIndexingForSharedThroughput()) {
|
if (this.useIndexingForSharedThroughput()) {
|
||||||
|
|||||||
@@ -29,10 +29,6 @@ export abstract class ContextualPaneBase extends WaitsForTemplateViewModel {
|
|||||||
this.title = ko.observable<string>();
|
this.title = ko.observable<string>();
|
||||||
this.formErrorsDetails = ko.observable<string>();
|
this.formErrorsDetails = ko.observable<string>();
|
||||||
this.isExecuting = ko.observable<boolean>(false);
|
this.isExecuting = ko.observable<boolean>(false);
|
||||||
this.container.isNotificationConsoleExpanded.subscribe((isExpanded: boolean) => {
|
|
||||||
this.resizePane();
|
|
||||||
});
|
|
||||||
this.container.isNotificationConsoleExpanded.extend({ rateLimit: 10 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public cancel() {
|
public cancel() {
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ describe("Delete Collection Confirmation Pane", () => {
|
|||||||
describe("shouldRecordFeedback()", () => {
|
describe("shouldRecordFeedback()", () => {
|
||||||
it("should return true if last collection and database does not have shared throughput else false", () => {
|
it("should return true if last collection and database does not have shared throughput else false", () => {
|
||||||
let fakeExplorer = new Explorer();
|
let fakeExplorer = new Explorer();
|
||||||
fakeExplorer.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
|
||||||
fakeExplorer.refreshAllDatabases = () => Q.resolve();
|
fakeExplorer.refreshAllDatabases = () => Q.resolve();
|
||||||
|
|
||||||
let pane = new DeleteCollectionConfirmationPane({
|
let pane = new DeleteCollectionConfirmationPane({
|
||||||
@@ -101,7 +100,6 @@ describe("Delete Collection Confirmation Pane", () => {
|
|||||||
rid: "test",
|
rid: "test",
|
||||||
} as ViewModels.Collection;
|
} as ViewModels.Collection;
|
||||||
};
|
};
|
||||||
fakeExplorer.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
|
||||||
fakeExplorer.selectedCollectionId = ko.computed<string>(() => selectedCollectionId);
|
fakeExplorer.selectedCollectionId = ko.computed<string>(() => selectedCollectionId);
|
||||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||||
const SubscriptionId = "testId";
|
const SubscriptionId = "testId";
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ describe("Delete Database Confirmation Pane", () => {
|
|||||||
describe("shouldRecordFeedback()", () => {
|
describe("shouldRecordFeedback()", () => {
|
||||||
it("should return true if last non empty database or is last database that has shared throughput, else false", () => {
|
it("should return true if last non empty database or is last database that has shared throughput, else false", () => {
|
||||||
let fakeExplorer = {} as Explorer;
|
let fakeExplorer = {} as Explorer;
|
||||||
fakeExplorer.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
|
||||||
|
|
||||||
let pane = new DeleteDatabaseConfirmationPane({
|
let pane = new DeleteDatabaseConfirmationPane({
|
||||||
id: "deletedatabaseconfirmationpane",
|
id: "deletedatabaseconfirmationpane",
|
||||||
@@ -92,7 +91,6 @@ describe("Delete Database Confirmation Pane", () => {
|
|||||||
} as ViewModels.Database;
|
} as ViewModels.Database;
|
||||||
};
|
};
|
||||||
fakeExplorer.refreshAllDatabases = () => Q.resolve();
|
fakeExplorer.refreshAllDatabases = () => Q.resolve();
|
||||||
fakeExplorer.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
|
||||||
fakeExplorer.selectedDatabaseId = ko.computed<string>(() => selectedDatabaseId);
|
fakeExplorer.selectedDatabaseId = ko.computed<string>(() => selectedDatabaseId);
|
||||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||||
const SubscriptionId = "testId";
|
const SubscriptionId = "testId";
|
||||||
|
|||||||
@@ -34,13 +34,6 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount(): void {
|
|
||||||
this.notificationConsoleSubscription = this.props.container.isNotificationConsoleExpanded.subscribe(() => {
|
|
||||||
this.setState({ panelHeight: this.getPanelHeight() });
|
|
||||||
});
|
|
||||||
this.props.container.isNotificationConsoleExpanded.extend({ rateLimit: 10 });
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
public componentWillUnmount(): void {
|
||||||
this.notificationConsoleSubscription && this.notificationConsoleSubscription.dispose();
|
this.notificationConsoleSubscription && this.notificationConsoleSubscription.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,10 +240,7 @@ function updateTableScrollableRegionHeight(): void {
|
|||||||
var dataTablesScrollBodyPosY = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector).offset().top;
|
var dataTablesScrollBodyPosY = $(tabElement).find(Constants.htmlSelectors.dataTableScrollBodySelector).offset().top;
|
||||||
var dataTablesInfoElem = $(tabElement).find(".dataTables_info");
|
var dataTablesInfoElem = $(tabElement).find(".dataTables_info");
|
||||||
var dataTablesPaginateElem = $(tabElement).find(".dataTables_paginate");
|
var dataTablesPaginateElem = $(tabElement).find(".dataTables_paginate");
|
||||||
const explorer = window.dataExplorer;
|
const notificationConsoleHeight = 32; /** Header height **/
|
||||||
const notificationConsoleHeight = explorer.isNotificationConsoleExpanded()
|
|
||||||
? 252 /** 32px(header) + 220px(content height) **/
|
|
||||||
: 32; /** Header height **/
|
|
||||||
|
|
||||||
var scrollHeight =
|
var scrollHeight =
|
||||||
bodyHeight -
|
bodyHeight -
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import Q from "q";
|
|
||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import UploadWorker from "worker-loader!../../workers/upload";
|
import UploadWorker from "worker-loader!../../workers/upload";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
@@ -254,9 +253,9 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public expandCollection(): Q.Promise<any> {
|
public expandCollection(): void {
|
||||||
if (this.isCollectionExpanded()) {
|
if (this.isCollectionExpanded()) {
|
||||||
return Q();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isCollectionExpanded(true);
|
this.isCollectionExpanded(true);
|
||||||
@@ -268,8 +267,6 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
defaultExperience: this.container.defaultExperience(),
|
defaultExperience: this.container.defaultExperience(),
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
});
|
});
|
||||||
|
|
||||||
return Q.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDocumentDBDocumentsClick() {
|
public onDocumentDBDocumentsClick() {
|
||||||
@@ -547,7 +544,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
|
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
|
||||||
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
|
const pendingNotificationsPromise: Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
|
||||||
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.SettingsV2, (tab) => {
|
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.SettingsV2, (tab) => {
|
||||||
return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id();
|
return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id();
|
||||||
});
|
});
|
||||||
@@ -580,7 +577,7 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
settingsTabV2: SettingsTabV2,
|
settingsTabV2: SettingsTabV2,
|
||||||
traceStartData: any,
|
traceStartData: any,
|
||||||
settingsTabOptions: ViewModels.TabOptions,
|
settingsTabOptions: ViewModels.TabOptions,
|
||||||
getPendingNotification: Q.Promise<DataModels.Notification>
|
getPendingNotification: Promise<DataModels.Notification>
|
||||||
): void => {
|
): void => {
|
||||||
const settingsTabV2Options: ViewModels.SettingsTabV2Options = {
|
const settingsTabV2Options: ViewModels.SettingsTabV2Options = {
|
||||||
...settingsTabOptions,
|
...settingsTabOptions,
|
||||||
@@ -980,19 +977,19 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
this.container.deleteCollectionConfirmationPane.open();
|
this.container.deleteCollectionConfirmationPane.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
public uploadFiles = (fileList: FileList): Q.Promise<UploadDetails> => {
|
public uploadFiles = (fileList: FileList): Promise<UploadDetails> => {
|
||||||
// TODO: right now web worker is not working with AAD flow. Use main thread for upload for now until we have backend upload capability
|
// TODO: right now web worker is not working with AAD flow. Use main thread for upload for now until we have backend upload capability
|
||||||
if (configContext.platform === Platform.Hosted && window.authType === AuthType.AAD) {
|
if (configContext.platform === Platform.Hosted && window.authType === AuthType.AAD) {
|
||||||
return this._uploadFilesCors(fileList);
|
return this._uploadFilesCors(fileList);
|
||||||
}
|
}
|
||||||
const documentUploader: Worker = new UploadWorker();
|
const documentUploader: Worker = new UploadWorker();
|
||||||
const deferred: Q.Deferred<UploadDetails> = Q.defer<UploadDetails>();
|
|
||||||
let inProgressNotificationId: string = "";
|
let inProgressNotificationId: string = "";
|
||||||
|
|
||||||
if (!fileList || fileList.length === 0) {
|
if (!fileList || fileList.length === 0) {
|
||||||
return Q.reject("No files specified");
|
return Promise.reject("No files specified");
|
||||||
}
|
}
|
||||||
documentUploader.onmessage = (event: MessageEvent) => {
|
|
||||||
|
const onmessage = (resolve: (value: UploadDetails) => void, reject: (reason: any) => void, event: MessageEvent) => {
|
||||||
const numSuccessful: number = event.data.numUploadsSuccessful;
|
const numSuccessful: number = event.data.numUploadsSuccessful;
|
||||||
const numFailed: number = event.data.numUploadsFailed;
|
const numFailed: number = event.data.numUploadsFailed;
|
||||||
const runtimeError: string = event.data.runtimeError;
|
const runtimeError: string = event.data.runtimeError;
|
||||||
@@ -1001,31 +998,26 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressNotificationId);
|
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressNotificationId);
|
||||||
documentUploader.terminate();
|
documentUploader.terminate();
|
||||||
if (!!runtimeError) {
|
if (!!runtimeError) {
|
||||||
deferred.reject(runtimeError);
|
reject(runtimeError);
|
||||||
} else if (numSuccessful === 0) {
|
} else if (numSuccessful === 0) {
|
||||||
// all uploads failed
|
// all uploads failed
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
NotificationConsoleUtils.logConsoleError(`Failed to upload all documents to container ${this.id()}`);
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Failed to upload all documents to container ${this.id()}`
|
|
||||||
);
|
|
||||||
} else if (numFailed > 0) {
|
} else if (numFailed > 0) {
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
NotificationConsoleUtils.logConsoleError(
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Failed to upload ${numFailed} of ${numSuccessful + numFailed} documents to container ${this.id()}`
|
`Failed to upload ${numFailed} of ${numSuccessful + numFailed} documents to container ${this.id()}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
NotificationConsoleUtils.logConsoleInfo(
|
||||||
ConsoleDataType.Info,
|
|
||||||
`Successfully uploaded all ${numSuccessful} documents to container ${this.id()}`
|
`Successfully uploaded all ${numSuccessful} documents to container ${this.id()}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this._logUploadDetailsInConsole(uploadDetails);
|
this._logUploadDetailsInConsole(uploadDetails);
|
||||||
deferred.resolve(uploadDetails);
|
resolve(uploadDetails);
|
||||||
};
|
};
|
||||||
documentUploader.onerror = (event: ErrorEvent): void => {
|
function onerror(reject: (reason: any) => void, event: ErrorEvent) {
|
||||||
documentUploader.terminate();
|
documentUploader.terminate();
|
||||||
deferred.reject(event.error);
|
reject(event.error);
|
||||||
};
|
}
|
||||||
|
|
||||||
const uploaderMessage: StartUploadMessageParams = {
|
const uploaderMessage: StartUploadMessageParams = {
|
||||||
files: fileList,
|
files: fileList,
|
||||||
@@ -1040,42 +1032,33 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return new Promise<UploadDetails>((resolve, reject) => {
|
||||||
|
documentUploader.onmessage = onmessage.bind(null, resolve, reject);
|
||||||
|
documentUploader.onerror = onerror.bind(null, reject);
|
||||||
|
|
||||||
documentUploader.postMessage(uploaderMessage);
|
documentUploader.postMessage(uploaderMessage);
|
||||||
inProgressNotificationId = NotificationConsoleUtils.logConsoleMessage(
|
inProgressNotificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||||
ConsoleDataType.InProgress,
|
ConsoleDataType.InProgress,
|
||||||
`Uploading and creating documents in container ${this.id()}`
|
`Uploading and creating documents in container ${this.id()}`
|
||||||
);
|
);
|
||||||
|
});
|
||||||
return deferred.promise;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private _uploadFilesCors(files: FileList): Q.Promise<UploadDetails> {
|
private async _uploadFilesCors(files: FileList): Promise<UploadDetails> {
|
||||||
const deferred: Q.Deferred<UploadDetails> = Q.defer<UploadDetails>();
|
const data = await Promise.all(Array.from(files).map((file) => this._uploadFile(file)));
|
||||||
const promises: Array<Q.Promise<UploadDetailsRecord>> = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
return { data };
|
||||||
promises.push(this._uploadFile(files[i]));
|
|
||||||
}
|
|
||||||
Q.all(promises).then((uploadDetails: Array<UploadDetailsRecord>) => {
|
|
||||||
deferred.resolve({ data: uploadDetails });
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _uploadFile(file: File): Q.Promise<UploadDetailsRecord> {
|
private _uploadFile(file: File): Promise<UploadDetailsRecord> {
|
||||||
const deferred: Q.Deferred<UploadDetailsRecord> = Q.defer();
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (evt: any): void => {
|
const onload = (resolve: (value: UploadDetailsRecord) => void, evt: any): void => {
|
||||||
const fileData: string = evt.target.result;
|
const fileData: string = evt.target.result;
|
||||||
this._createDocumentsFromFile(file.name, fileData).then((record) => {
|
this._createDocumentsFromFile(file.name, fileData).then((record) => resolve(record));
|
||||||
deferred.resolve(record);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.onerror = (evt: ProgressEvent): void => {
|
const onerror = (resolve: (value: UploadDetailsRecord) => void, evt: ProgressEvent): void => {
|
||||||
deferred.resolve({
|
resolve({
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
numSucceeded: 0,
|
numSucceeded: 0,
|
||||||
numFailed: 1,
|
numFailed: 1,
|
||||||
@@ -1083,9 +1066,11 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return new Promise<UploadDetailsRecord>((resolve) => {
|
||||||
|
reader.onload = onload.bind(this, resolve);
|
||||||
|
reader.onerror = onerror.bind(this, resolve);
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
return deferred.promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> {
|
private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> {
|
||||||
@@ -1119,32 +1104,23 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
|
private async _getPendingThroughputSplitNotification(): Promise<DataModels.Notification> {
|
||||||
if (!this.container) {
|
if (!this.container) {
|
||||||
return Q.resolve(undefined);
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deferred: Q.Deferred<DataModels.Notification> = Q.defer<DataModels.Notification>();
|
const throughputUpdateRegExp = new RegExp("Throughput update (.*) in progress");
|
||||||
fetchPortalNotifications().then(
|
try {
|
||||||
(notifications: DataModels.Notification[]) => {
|
const notifications = await fetchPortalNotifications();
|
||||||
if (!notifications || notifications.length === 0) {
|
if (!notifications) {
|
||||||
deferred.resolve(undefined);
|
return undefined;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingNotification = _.find(notifications, (notification: DataModels.Notification) => {
|
return notifications.find(
|
||||||
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress");
|
({ kind, collectionName, description = "" }) =>
|
||||||
return (
|
kind === "message" && collectionName === this.id() && throughputUpdateRegExp.test(description)
|
||||||
notification.kind === "message" &&
|
|
||||||
notification.collectionName === this.id() &&
|
|
||||||
notification.description &&
|
|
||||||
throughputUpdateRegExp.test(notification.description)
|
|
||||||
);
|
);
|
||||||
});
|
} catch (error) {
|
||||||
|
|
||||||
deferred.resolve(pendingNotification);
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
Logger.logError(
|
Logger.logError(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
@@ -1154,11 +1130,9 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
}),
|
}),
|
||||||
"Settings tree node"
|
"Settings tree node"
|
||||||
);
|
);
|
||||||
deferred.resolve(undefined);
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
return deferred.promise;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _logUploadDetailsInConsole(uploadDetails: UploadDetails): void {
|
private _logUploadDetailsInConsole(uploadDetails: UploadDetails): void {
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
|||||||
this.isCollectionExpanded = ko.observable<boolean>(true);
|
this.isCollectionExpanded = ko.observable<boolean>(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public expandCollection(): Q.Promise<void> {
|
public expandCollection(): void {
|
||||||
if (this.isCollectionExpanded()) {
|
if (this.isCollectionExpanded()) {
|
||||||
return Q();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isCollectionExpanded(true);
|
this.isCollectionExpanded(true);
|
||||||
@@ -55,8 +55,6 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
|
|||||||
defaultExperience: this.container.defaultExperience(),
|
defaultExperience: this.container.defaultExperience(),
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
});
|
});
|
||||||
|
|
||||||
return Q.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public collapseCollection() {
|
public collapseCollection() {
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { AuthType } from "./AuthType";
|
import { AuthType } from "./AuthType";
|
||||||
import { AccessInputMetadata, DatabaseAccount } from "./Contracts/DataModels";
|
import { AccessInputMetadata, DatabaseAccount } from "./Contracts/DataModels";
|
||||||
|
|
||||||
|
type HostedConfig = AAD | ConnectionString | EncryptedToken | ResourceToken;
|
||||||
export interface HostedExplorerChildFrame extends Window {
|
export interface HostedExplorerChildFrame extends Window {
|
||||||
hostedConfig: AAD | ConnectionString | EncryptedToken | ResourceToken;
|
hostedConfig: HostedConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AAD {
|
export interface AAD {
|
||||||
authType: AuthType.AAD;
|
authType: AuthType.AAD;
|
||||||
databaseAccount: DatabaseAccount;
|
databaseAccount: DatabaseAccount;
|
||||||
authorizationToken: string;
|
authorizationToken: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConnectionString {
|
export interface ConnectionString {
|
||||||
authType: AuthType.ConnectionString;
|
authType: AuthType.ConnectionString;
|
||||||
// Connection string uses still use encrypted token for Cassandra/Mongo APIs as they us the portal backend proxy
|
// Connection string uses still use encrypted token for Cassandra/Mongo APIs as they us the portal backend proxy
|
||||||
encryptedToken: string;
|
encryptedToken: string;
|
||||||
@@ -20,13 +21,13 @@ interface ConnectionString {
|
|||||||
masterKey?: string;
|
masterKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EncryptedToken {
|
export interface EncryptedToken {
|
||||||
authType: AuthType.EncryptedToken;
|
authType: AuthType.EncryptedToken;
|
||||||
encryptedToken: string;
|
encryptedToken: string;
|
||||||
encryptedTokenMetadata: AccessInputMetadata;
|
encryptedTokenMetadata: AccessInputMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResourceToken {
|
export interface ResourceToken {
|
||||||
authType: AuthType.ResourceToken;
|
authType: AuthType.ResourceToken;
|
||||||
resourceToken: string;
|
resourceToken: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ describe("Gallery", () => {
|
|||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(
|
expect(window.fetch).toBeCalledWith(
|
||||||
`${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleDatabaseAccount.name}/gallery/${id}/favorite`,
|
`${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleSubscriptionId}/${sampleDatabaseAccount.name}/gallery/${id}/favorite`,
|
||||||
{
|
{
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export class JunoClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getPinnedRepos(scope: string): Promise<IJunoResponse<IPinnedRepo[]>> {
|
public async getPinnedRepos(scope: string): Promise<IJunoResponse<IPinnedRepo[]>> {
|
||||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/pinnedrepos`, {
|
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/github/pinnedrepos`, {
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ export class JunoClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async updatePinnedRepos(repos: IPinnedRepo[]): Promise<IJunoResponse<undefined>> {
|
public async updatePinnedRepos(repos: IPinnedRepo[]): Promise<IJunoResponse<undefined>> {
|
||||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/pinnedrepos`, {
|
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/github/pinnedrepos`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify(repos),
|
body: JSON.stringify(repos),
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
@@ -120,7 +120,7 @@ export class JunoClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async deleteGitHubInfo(): Promise<IJunoResponse<undefined>> {
|
public async deleteGitHubInfo(): Promise<IJunoResponse<undefined>> {
|
||||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github`, {
|
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/github`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
});
|
});
|
||||||
@@ -135,9 +135,12 @@ export class JunoClient {
|
|||||||
const githubParams = JunoClient.getGitHubClientParams();
|
const githubParams = JunoClient.getGitHubClientParams();
|
||||||
githubParams.append("code", code);
|
githubParams.append("code", code);
|
||||||
|
|
||||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/token?${githubParams.toString()}`, {
|
const response = await window.fetch(
|
||||||
|
`${this.getNotebooksSubscriptionIdAccountUrl()}/github/token?${githubParams.toString()}`,
|
||||||
|
{
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
let data: IGitHubOAuthToken;
|
let data: IGitHubOAuthToken;
|
||||||
const body = await response.text();
|
const body = await response.text();
|
||||||
@@ -159,10 +162,13 @@ export class JunoClient {
|
|||||||
const githubParams = JunoClient.getGitHubClientParams();
|
const githubParams = JunoClient.getGitHubClientParams();
|
||||||
githubParams.append("access_token", token);
|
githubParams.append("access_token", token);
|
||||||
|
|
||||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/github/token?${githubParams.toString()}`, {
|
const response = await window.fetch(
|
||||||
|
`${this.getNotebooksSubscriptionIdAccountUrl()}/github/token?${githubParams.toString()}`,
|
||||||
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
@@ -179,7 +185,7 @@ export class JunoClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getPublicGalleryData(): Promise<IJunoResponse<IPublicGalleryData>> {
|
public async getPublicGalleryData(): Promise<IJunoResponse<IPublicGalleryData>> {
|
||||||
const url = `${this.getNotebooksAccountUrl()}/gallery/public`;
|
const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/public`;
|
||||||
const response = await window.fetch(url, {
|
const response = await window.fetch(url, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
@@ -197,7 +203,7 @@ export class JunoClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async acceptCodeOfConduct(): Promise<IJunoResponse<boolean>> {
|
public async acceptCodeOfConduct(): Promise<IJunoResponse<boolean>> {
|
||||||
const url = `${this.getNotebooksAccountUrl()}/gallery/acceptCodeOfConduct`;
|
const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/acceptCodeOfConduct`;
|
||||||
const response = await window.fetch(url, {
|
const response = await window.fetch(url, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
@@ -215,7 +221,7 @@ export class JunoClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async isCodeOfConductAccepted(): Promise<IJunoResponse<boolean>> {
|
public async isCodeOfConductAccepted(): Promise<IJunoResponse<boolean>> {
|
||||||
const url = `${this.getNotebooksAccountUrl()}/gallery/isCodeOfConductAccepted`;
|
const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/isCodeOfConductAccepted`;
|
||||||
const response = await window.fetch(url, {
|
const response = await window.fetch(url, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
@@ -294,7 +300,7 @@ export class JunoClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async favoriteNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
public async favoriteNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
||||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery/${id}/favorite`, {
|
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/${id}/favorite`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
});
|
});
|
||||||
@@ -365,9 +371,7 @@ export class JunoClient {
|
|||||||
content: string,
|
content: string,
|
||||||
isLinkInjectionEnabled: boolean
|
isLinkInjectionEnabled: boolean
|
||||||
): Promise<IJunoResponse<IGalleryItem>> {
|
): Promise<IJunoResponse<IGalleryItem>> {
|
||||||
const response = await window.fetch(
|
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery`, {
|
||||||
`${this.getNotebooksUrl()}/${this.getSubscriptionId()}/${this.getAccount()}/gallery`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -379,8 +383,7 @@ export class JunoClient {
|
|||||||
content: JSON.parse(content),
|
content: JSON.parse(content),
|
||||||
addLinkToNotebookViewer: isLinkInjectionEnabled,
|
addLinkToNotebookViewer: isLinkInjectionEnabled,
|
||||||
} as IPublishNotebookRequest),
|
} as IPublishNotebookRequest),
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
let data: IGalleryItem;
|
let data: IGalleryItem;
|
||||||
if (response.status === HttpStatusCodes.OK) {
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
@@ -502,6 +505,10 @@ export class JunoClient {
|
|||||||
return `${this.getNotebooksUrl()}/${this.getAccount()}`;
|
return `${this.getNotebooksUrl()}/${this.getAccount()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getNotebooksSubscriptionIdAccountUrl(): string {
|
||||||
|
return `${this.getNotebooksUrl()}/${this.getSubscriptionId()}/${this.getAccount()}`;
|
||||||
|
}
|
||||||
|
|
||||||
private getAnalyticsUrl(): string {
|
private getAnalyticsUrl(): string {
|
||||||
return `${configContext.JUNO_ENDPOINT}/api/analytics`;
|
return `${configContext.JUNO_ENDPOINT}/api/analytics`;
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/Localization/en/translations.json
Normal file
33
src/Localization/en/translations.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"translations": {
|
||||||
|
"Common": {
|
||||||
|
"Save": "Save",
|
||||||
|
"Discard": "Discard",
|
||||||
|
"Refresh": "Refesh"
|
||||||
|
},
|
||||||
|
"SelfServeExample": {
|
||||||
|
"North Central US": "North Central US",
|
||||||
|
"West US": "West US",
|
||||||
|
"East US 2": "East US 2",
|
||||||
|
"ClassInfo": "This is a self serve class",
|
||||||
|
"RegionDropdownInfo": "More regions can be added in the future.",
|
||||||
|
"ValidationError": "Regions and AccountName should not be empty.",
|
||||||
|
"DescriptionText": "This class sets collection and database throughput.",
|
||||||
|
"DecriptionLinkText": "Click here for more information",
|
||||||
|
"Regions": "Regions",
|
||||||
|
"RegionsPlaceholder": "Select a region",
|
||||||
|
"Enable Logging": "Enable Logging",
|
||||||
|
"Enable": "Enable",
|
||||||
|
"Disable": "Disable",
|
||||||
|
"Account Name": "Account Name",
|
||||||
|
"AccountNamePlaceHolder": "Enter the account name",
|
||||||
|
"Collection Throughput": "Collection Throughput",
|
||||||
|
"Enable DB level throughput": "Enable DB level throughput",
|
||||||
|
"Database Throughput": "Database Throughput",
|
||||||
|
"RefreshMessage": "Self Serve Example successfully refreshing",
|
||||||
|
"SubmissionMessage": "Submitted successfully"
|
||||||
|
},
|
||||||
|
"SqlX": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
226
src/Main.tsx
226
src/Main.tsx
@@ -53,215 +53,33 @@ import "object.entries/auto";
|
|||||||
import "./Libs/is-integer-polyfill";
|
import "./Libs/is-integer-polyfill";
|
||||||
import "url-polyfill/url-polyfill.min";
|
import "url-polyfill/url-polyfill.min";
|
||||||
|
|
||||||
initializeIcons();
|
|
||||||
|
|
||||||
import { AuthType } from "./AuthType";
|
|
||||||
|
|
||||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||||
import { applyExplorerBindings } from "./applyExplorerBindings";
|
import { ExplorerParams } from "./Explorer/Explorer";
|
||||||
import { configContext, initializeConfiguration, Platform } from "./ConfigContext";
|
import React, { useState } from "react";
|
||||||
import Explorer from "./Explorer/Explorer";
|
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import copyImage from "../images/Copy.svg";
|
import copyImage from "../images/Copy.svg";
|
||||||
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
|
||||||
import refreshImg from "../images/refresh-cosmos.svg";
|
import refreshImg from "../images/refresh-cosmos.svg";
|
||||||
import arrowLeftImg from "../images/imgarrowlefticon.svg";
|
import arrowLeftImg from "../images/imgarrowlefticon.svg";
|
||||||
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
|
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
|
||||||
import { updateUserContext } from "./UserContext";
|
import { useConfig } from "./hooks/useConfig";
|
||||||
import { CollectionCreation } from "./Shared/Constants";
|
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
||||||
import { extractFeatures } from "./Platform/Hosted/extractFeatures";
|
import { NotificationConsoleComponent } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { emulatorAccount } from "./Platform/Emulator/emulatorAccount";
|
|
||||||
import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame";
|
initializeIcons();
|
||||||
import {
|
|
||||||
getDatabaseAccountKindFromExperience,
|
|
||||||
getDatabaseAccountPropertiesFromMetadata,
|
|
||||||
} from "./Platform/Hosted/HostedUtils";
|
|
||||||
import { DefaultExperienceUtility } from "./Shared/DefaultExperienceUtility";
|
|
||||||
import { parseResourceTokenConnectionString } from "./Platform/Hosted/Helpers/ResourceTokenUtils";
|
|
||||||
import { AccountKind, DefaultAccountExperience, ServerIds } from "./Common/Constants";
|
|
||||||
import { listKeys } from "./Utils/arm/generatedClients/2020-04-01/databaseAccounts";
|
|
||||||
import { SelfServeType } from "./SelfServe/SelfServeUtils";
|
|
||||||
|
|
||||||
const App: React.FunctionComponent = () => {
|
const App: React.FunctionComponent = () => {
|
||||||
useEffect(() => {
|
const [isNotificationConsoleExpanded, setIsNotificationConsoleExpanded] = useState(false);
|
||||||
initializeConfiguration().then(async (config) => {
|
const [notificationConsoleData, setNotificationConsoleData] = useState(undefined);
|
||||||
let explorer: Explorer;
|
//TODO: Refactor so we don't need to pass the id to remove a console data
|
||||||
if (config.platform === Platform.Hosted) {
|
const [inProgressConsoleDataIdToBeDeleted, setInProgressConsoleDataIdToBeDeleted] = useState("");
|
||||||
const win = (window as unknown) as HostedExplorerChildFrame;
|
const explorerParams: ExplorerParams = {
|
||||||
explorer = new Explorer();
|
setIsNotificationConsoleExpanded,
|
||||||
explorer.selfServeType(SelfServeType.none);
|
setNotificationConsoleData,
|
||||||
if (win.hostedConfig.authType === AuthType.EncryptedToken) {
|
setInProgressConsoleDataIdToBeDeleted,
|
||||||
// TODO: Remove window.authType
|
};
|
||||||
window.authType = AuthType.EncryptedToken;
|
const config = useConfig();
|
||||||
// Impossible to tell if this is a try cosmos sub using an encrypted token
|
useKnockoutExplorer(config, explorerParams);
|
||||||
explorer.isTryCosmosDBSubscription(false);
|
|
||||||
updateUserContext({
|
|
||||||
accessToken: encodeURIComponent(win.hostedConfig.encryptedToken),
|
|
||||||
});
|
|
||||||
|
|
||||||
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
|
|
||||||
win.hostedConfig.encryptedTokenMetadata.apiKind
|
|
||||||
);
|
|
||||||
explorer.initDataExplorerWithFrameInputs({
|
|
||||||
databaseAccount: {
|
|
||||||
id: "",
|
|
||||||
// id: Main._databaseAccountId,
|
|
||||||
name: win.hostedConfig.encryptedTokenMetadata.accountName,
|
|
||||||
kind: getDatabaseAccountKindFromExperience(apiExperience),
|
|
||||||
properties: getDatabaseAccountPropertiesFromMetadata(win.hostedConfig.encryptedTokenMetadata),
|
|
||||||
tags: [],
|
|
||||||
},
|
|
||||||
subscriptionId: undefined,
|
|
||||||
resourceGroup: undefined,
|
|
||||||
masterKey: undefined,
|
|
||||||
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
|
|
||||||
authorizationToken: undefined,
|
|
||||||
features: extractFeatures(),
|
|
||||||
csmEndpoint: undefined,
|
|
||||||
dnsSuffix: undefined,
|
|
||||||
serverId: ServerIds.productionPortal,
|
|
||||||
extensionEndpoint: configContext.BACKEND_ENDPOINT,
|
|
||||||
subscriptionType: CollectionCreation.DefaultSubscriptionType,
|
|
||||||
quotaId: undefined,
|
|
||||||
addCollectionDefaultFlight: explorer.flight(),
|
|
||||||
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(),
|
|
||||||
});
|
|
||||||
explorer.isAccountReady(true);
|
|
||||||
} else if (win.hostedConfig.authType === AuthType.ResourceToken) {
|
|
||||||
window.authType = AuthType.ResourceToken;
|
|
||||||
// Resource tokens can only be used with SQL API
|
|
||||||
const apiExperience: string = DefaultAccountExperience.DocumentDB;
|
|
||||||
const parsedResourceToken = parseResourceTokenConnectionString(win.hostedConfig.resourceToken);
|
|
||||||
updateUserContext({
|
|
||||||
resourceToken: parsedResourceToken.resourceToken,
|
|
||||||
endpoint: parsedResourceToken.accountEndpoint,
|
|
||||||
});
|
|
||||||
explorer.resourceTokenDatabaseId(parsedResourceToken.databaseId);
|
|
||||||
explorer.resourceTokenCollectionId(parsedResourceToken.collectionId);
|
|
||||||
if (parsedResourceToken.partitionKey) {
|
|
||||||
explorer.resourceTokenPartitionKey(parsedResourceToken.partitionKey);
|
|
||||||
}
|
|
||||||
explorer.initDataExplorerWithFrameInputs({
|
|
||||||
databaseAccount: {
|
|
||||||
id: "",
|
|
||||||
name: parsedResourceToken.accountEndpoint,
|
|
||||||
kind: AccountKind.GlobalDocumentDB,
|
|
||||||
properties: { documentEndpoint: parsedResourceToken.accountEndpoint },
|
|
||||||
tags: { defaultExperience: apiExperience },
|
|
||||||
},
|
|
||||||
subscriptionId: undefined,
|
|
||||||
resourceGroup: undefined,
|
|
||||||
masterKey: undefined,
|
|
||||||
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
|
|
||||||
authorizationToken: undefined,
|
|
||||||
features: extractFeatures(),
|
|
||||||
csmEndpoint: undefined,
|
|
||||||
dnsSuffix: undefined,
|
|
||||||
serverId: ServerIds.productionPortal,
|
|
||||||
extensionEndpoint: configContext.BACKEND_ENDPOINT,
|
|
||||||
subscriptionType: CollectionCreation.DefaultSubscriptionType,
|
|
||||||
quotaId: undefined,
|
|
||||||
addCollectionDefaultFlight: explorer.flight(),
|
|
||||||
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(),
|
|
||||||
isAuthWithresourceToken: true,
|
|
||||||
});
|
|
||||||
explorer.isAccountReady(true);
|
|
||||||
explorer.isRefreshingExplorer(false);
|
|
||||||
} else if (win.hostedConfig.authType === AuthType.ConnectionString) {
|
|
||||||
// For legacy reasons lots of code expects a connection string login to look and act like an encrypted token login
|
|
||||||
window.authType = AuthType.EncryptedToken;
|
|
||||||
// Impossible to tell if this is a try cosmos sub using an encrypted token
|
|
||||||
explorer.isTryCosmosDBSubscription(false);
|
|
||||||
updateUserContext({
|
|
||||||
accessToken: encodeURIComponent(win.hostedConfig.encryptedToken),
|
|
||||||
});
|
|
||||||
|
|
||||||
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
|
|
||||||
win.hostedConfig.encryptedTokenMetadata.apiKind
|
|
||||||
);
|
|
||||||
explorer.initDataExplorerWithFrameInputs({
|
|
||||||
databaseAccount: {
|
|
||||||
id: "",
|
|
||||||
// id: Main._databaseAccountId,
|
|
||||||
name: win.hostedConfig.encryptedTokenMetadata.accountName,
|
|
||||||
kind: getDatabaseAccountKindFromExperience(apiExperience),
|
|
||||||
properties: getDatabaseAccountPropertiesFromMetadata(win.hostedConfig.encryptedTokenMetadata),
|
|
||||||
tags: [],
|
|
||||||
},
|
|
||||||
subscriptionId: undefined,
|
|
||||||
resourceGroup: undefined,
|
|
||||||
masterKey: win.hostedConfig.masterKey,
|
|
||||||
hasWriteAccess: true, // TODO: we should embed this information in the token ideally
|
|
||||||
authorizationToken: undefined,
|
|
||||||
features: extractFeatures(),
|
|
||||||
csmEndpoint: undefined,
|
|
||||||
dnsSuffix: undefined,
|
|
||||||
serverId: ServerIds.productionPortal,
|
|
||||||
extensionEndpoint: configContext.BACKEND_ENDPOINT,
|
|
||||||
subscriptionType: CollectionCreation.DefaultSubscriptionType,
|
|
||||||
quotaId: undefined,
|
|
||||||
addCollectionDefaultFlight: explorer.flight(),
|
|
||||||
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(),
|
|
||||||
});
|
|
||||||
explorer.isAccountReady(true);
|
|
||||||
} else if (win.hostedConfig.authType === AuthType.AAD) {
|
|
||||||
window.authType = AuthType.AAD;
|
|
||||||
const account = win.hostedConfig.databaseAccount;
|
|
||||||
const accountResourceId = account.id;
|
|
||||||
const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0];
|
|
||||||
const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0];
|
|
||||||
updateUserContext({
|
|
||||||
authorizationToken: `Bearer ${win.hostedConfig.authorizationToken}`,
|
|
||||||
databaseAccount: win.hostedConfig.databaseAccount,
|
|
||||||
});
|
|
||||||
const keys = await listKeys(subscriptionId, resourceGroup, account.name);
|
|
||||||
explorer.initDataExplorerWithFrameInputs({
|
|
||||||
databaseAccount: account,
|
|
||||||
subscriptionId,
|
|
||||||
resourceGroup,
|
|
||||||
masterKey: keys.primaryMasterKey,
|
|
||||||
hasWriteAccess: true, //TODO: 425017 - support read access
|
|
||||||
authorizationToken: `Bearer ${win.hostedConfig.authorizationToken}`,
|
|
||||||
features: extractFeatures(),
|
|
||||||
csmEndpoint: undefined,
|
|
||||||
dnsSuffix: undefined,
|
|
||||||
serverId: ServerIds.productionPortal,
|
|
||||||
extensionEndpoint: configContext.BACKEND_ENDPOINT,
|
|
||||||
subscriptionType: CollectionCreation.DefaultSubscriptionType,
|
|
||||||
quotaId: undefined,
|
|
||||||
addCollectionDefaultFlight: explorer.flight(),
|
|
||||||
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(),
|
|
||||||
});
|
|
||||||
explorer.isAccountReady(true);
|
|
||||||
}
|
|
||||||
} else if (config.platform === Platform.Emulator) {
|
|
||||||
window.authType = AuthType.MasterKey;
|
|
||||||
explorer = new Explorer();
|
|
||||||
explorer.selfServeType(SelfServeType.none);
|
|
||||||
explorer.databaseAccount(emulatorAccount);
|
|
||||||
explorer.isAccountReady(true);
|
|
||||||
} else if (config.platform === Platform.Portal) {
|
|
||||||
window.authType = AuthType.AAD;
|
|
||||||
explorer = new Explorer();
|
|
||||||
|
|
||||||
// In development mode, try to load the iframe message from session storage.
|
|
||||||
// This allows webpack hot reload to funciton properly
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage");
|
|
||||||
if (initMessage) {
|
|
||||||
const message = JSON.parse(initMessage);
|
|
||||||
console.warn("Loaded cached portal iframe message from session storage");
|
|
||||||
console.dir(message);
|
|
||||||
explorer.initDataExplorerWithFrameInputs(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener("message", explorer.handleMessage.bind(explorer), false);
|
|
||||||
}
|
|
||||||
applyExplorerBindings(explorer);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flexContainer">
|
<div className="flexContainer">
|
||||||
@@ -463,9 +281,15 @@ const App: React.FunctionComponent = () => {
|
|||||||
role="contentinfo"
|
role="contentinfo"
|
||||||
aria-label="Notification console"
|
aria-label="Notification console"
|
||||||
id="explorerNotificationConsole"
|
id="explorerNotificationConsole"
|
||||||
data-bind="react: notificationConsoleComponentAdapter"
|
>
|
||||||
|
<NotificationConsoleComponent
|
||||||
|
isConsoleExpanded={isNotificationConsoleExpanded}
|
||||||
|
consoleData={notificationConsoleData}
|
||||||
|
inProgressConsoleDataIdToBeDeleted={inProgressConsoleDataIdToBeDeleted}
|
||||||
|
setIsConsoleExpanded={setIsNotificationConsoleExpanded}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/* Global loader - Start */}
|
{/* Global loader - Start */}
|
||||||
|
|
||||||
<div className="splashLoaderContainer" data-bind="visible: isRefreshingExplorer">
|
<div className="splashLoaderContainer" data-bind="visible: isRefreshingExplorer">
|
||||||
|
|||||||
@@ -288,12 +288,10 @@ export class TabRouteHandler {
|
|||||||
private _openSprocTabForResource(databaseId: string, collectionId: string, sprocId: string): void {
|
private _openSprocTabForResource(databaseId: string, collectionId: string, sprocId: string): void {
|
||||||
this._executeActionHelper(() => {
|
this._executeActionHelper(() => {
|
||||||
const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId);
|
const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId);
|
||||||
collection &&
|
collection && collection.expandCollection();
|
||||||
collection.expandCollection().then(() => {
|
|
||||||
const storedProcedure = collection && collection.findStoredProcedureWithId(sprocId);
|
const storedProcedure = collection && collection.findStoredProcedureWithId(sprocId);
|
||||||
storedProcedure && storedProcedure.open();
|
storedProcedure && storedProcedure.open();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _openNewTriggerTabForResource(databaseId: string, collectionId: string): void {
|
private _openNewTriggerTabForResource(databaseId: string, collectionId: string): void {
|
||||||
@@ -319,12 +317,10 @@ export class TabRouteHandler {
|
|||||||
private _openTriggerTabForResource(databaseId: string, collectionId: string, triggerId: string): void {
|
private _openTriggerTabForResource(databaseId: string, collectionId: string, triggerId: string): void {
|
||||||
this._executeActionHelper(() => {
|
this._executeActionHelper(() => {
|
||||||
const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId);
|
const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId);
|
||||||
collection &&
|
collection && collection.expandCollection();
|
||||||
collection.expandCollection().then(() => {
|
|
||||||
const trigger = collection && collection.findTriggerWithId(triggerId);
|
const trigger = collection && collection.findTriggerWithId(triggerId);
|
||||||
trigger && trigger.open();
|
trigger && trigger.open();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _openNewUserDefinedFunctionTabForResource(databaseId: string, collectionId: string): void {
|
private _openNewUserDefinedFunctionTabForResource(databaseId: string, collectionId: string): void {
|
||||||
@@ -350,12 +346,10 @@ export class TabRouteHandler {
|
|||||||
private _openUserDefinedFunctionTabForResource(databaseId: string, collectionId: string, udfId: string): void {
|
private _openUserDefinedFunctionTabForResource(databaseId: string, collectionId: string, udfId: string): void {
|
||||||
this._executeActionHelper(() => {
|
this._executeActionHelper(() => {
|
||||||
const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId);
|
const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId);
|
||||||
collection &&
|
collection && collection.expandCollection();
|
||||||
collection.expandCollection().then(() => {
|
|
||||||
const userDefinedFunction = collection && collection.findUserDefinedFunctionWithId(udfId);
|
const userDefinedFunction = collection && collection.findUserDefinedFunctionWithId(udfId);
|
||||||
userDefinedFunction && userDefinedFunction.open();
|
userDefinedFunction && userDefinedFunction.open();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _openConflictsTabForResource(databaseId: string, collectionId: string): void {
|
private _openConflictsTabForResource(databaseId: string, collectionId: string): void {
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Info } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
|
||||||
import { addPropertyToMap, buildSmartUiDescriptor } from "./SelfServeUtils";
|
|
||||||
|
|
||||||
export const IsDisplayable = (): ClassDecorator => {
|
|
||||||
return (target) => {
|
|
||||||
buildSmartUiDescriptor(target.name, target.prototype);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ClassInfo = (info: (() => Promise<Info>) | Info): ClassDecorator => {
|
|
||||||
return (target) => {
|
|
||||||
addPropertyToMap(target.prototype, "root", target.name, "info", info);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,50 +1,64 @@
|
|||||||
import { ChoiceItem, Info, InputType, UiType } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
import { ChoiceItem, Description, Info, InputType, NumberUiType, SmartUiInput } from "./SelfServeTypes";
|
||||||
import { addPropertyToMap, CommonInputTypes } from "./SelfServeUtils";
|
import { addPropertyToMap, DecoratorProperties, buildSmartUiDescriptor } from "./SelfServeUtils";
|
||||||
|
|
||||||
type ValueOf<T> = T[keyof T];
|
type ValueOf<T> = T[keyof T];
|
||||||
interface Decorator {
|
interface Decorator {
|
||||||
name: keyof CommonInputTypes;
|
name: keyof DecoratorProperties;
|
||||||
value: ValueOf<CommonInputTypes>;
|
value: ValueOf<DecoratorProperties>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InputOptionsBase {
|
interface InputOptionsBase {
|
||||||
label: string;
|
labelTKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NumberInputOptions extends InputOptionsBase {
|
export interface NumberInputOptions extends InputOptionsBase {
|
||||||
min: (() => Promise<number>) | number;
|
min: (() => Promise<number>) | number;
|
||||||
max: (() => Promise<number>) | number;
|
max: (() => Promise<number>) | number;
|
||||||
step: (() => Promise<number>) | number;
|
step: (() => Promise<number>) | number;
|
||||||
uiType: UiType;
|
uiType: NumberUiType;
|
||||||
}
|
}
|
||||||
|
|
||||||
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[];
|
||||||
|
placeholderTKey?: (() => Promise<string>) | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type InputOptions = NumberInputOptions | StringInputOptions | BooleanInputOptions | ChoiceInputOptions;
|
export interface DescriptionDisplayOptions {
|
||||||
|
description?: (() => Promise<Description>) | Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
type InputOptions =
|
||||||
|
| NumberInputOptions
|
||||||
|
| StringInputOptions
|
||||||
|
| BooleanInputOptions
|
||||||
|
| ChoiceInputOptions
|
||||||
|
| DescriptionDisplayOptions;
|
||||||
|
|
||||||
const isNumberInputOptions = (inputOptions: InputOptions): inputOptions is NumberInputOptions => {
|
const isNumberInputOptions = (inputOptions: InputOptions): inputOptions is NumberInputOptions => {
|
||||||
return "min" in inputOptions;
|
return "min" in inputOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
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 => {
|
||||||
return "choices" in inputOptions;
|
return "choices" in inputOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isDescriptionDisplayOptions = (inputOptions: InputOptions): inputOptions is DescriptionDisplayOptions => {
|
||||||
|
return "description" in inputOptions;
|
||||||
|
};
|
||||||
|
|
||||||
const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
|
const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
|
||||||
return (target, property) => {
|
return (target, property) => {
|
||||||
let className = target.constructor.name;
|
let className = target.constructor.name;
|
||||||
@@ -66,7 +80,7 @@ const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const OnChange = (
|
export const OnChange = (
|
||||||
onChange: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>
|
onChange: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>
|
||||||
): PropertyDecorator => {
|
): PropertyDecorator => {
|
||||||
return addToMap({ name: "onChange", value: onChange });
|
return addToMap({ name: "onChange", value: onChange });
|
||||||
};
|
};
|
||||||
@@ -78,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 },
|
||||||
@@ -86,16 +100,34 @@ 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({ name: "label", value: inputOptions.label }, { name: "choices", value: inputOptions.choices });
|
return addToMap(
|
||||||
|
{ name: "labelTKey", value: inputOptions.labelTKey },
|
||||||
|
{ name: "placeholderTKey", value: inputOptions.placeholderTKey },
|
||||||
|
{ name: "choices", value: inputOptions.choices }
|
||||||
|
);
|
||||||
|
} else if (isDescriptionDisplayOptions(inputOptions)) {
|
||||||
|
return addToMap({ name: "description", value: inputOptions.description });
|
||||||
} else {
|
} 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 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const IsDisplayable = (): ClassDecorator => {
|
||||||
|
return (target) => {
|
||||||
|
buildSmartUiDescriptor(target.name, target.prototype);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClassInfo = (info: (() => Promise<Info>) | Info): ClassDecorator => {
|
||||||
|
return (target) => {
|
||||||
|
addPropertyToMap(target.prototype, "root", target.name, "info", info);
|
||||||
|
};
|
||||||
|
};
|
||||||
76
src/SelfServe/Example/SelfServeExample.rp.ts
Normal file
76
src/SelfServe/Example/SelfServeExample.rp.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { get } from "../../Utils/arm/generatedClients/2020-04-01/databaseAccounts";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
import { SessionStorageUtility } from "../../Shared/StorageUtility";
|
||||||
|
import { RefreshResult } from "../SelfServeTypes";
|
||||||
|
export enum Regions {
|
||||||
|
NorthCentralUS = "NorthCentralUS",
|
||||||
|
WestUS = "WestUS",
|
||||||
|
EastUS2 = "EastUS2",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitializeResponse {
|
||||||
|
regions: Regions;
|
||||||
|
enableLogging: boolean;
|
||||||
|
accountName: string;
|
||||||
|
collectionThroughput: number;
|
||||||
|
dbThroughput: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMaxCollectionThroughput = async (): Promise<number> => {
|
||||||
|
return 10000;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMinCollectionThroughput = async (): Promise<number> => {
|
||||||
|
return 400;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMaxDatabaseThroughput = async (): Promise<number> => {
|
||||||
|
return 10000;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMinDatabaseThroughput = async (): Promise<number> => {
|
||||||
|
return 400;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const update = async (
|
||||||
|
regions: Regions,
|
||||||
|
enableLogging: boolean,
|
||||||
|
accountName: string,
|
||||||
|
collectionThroughput: number,
|
||||||
|
dbThoughput: number
|
||||||
|
): Promise<void> => {
|
||||||
|
SessionStorageUtility.setEntry("regions", regions);
|
||||||
|
SessionStorageUtility.setEntry("enableLogging", enableLogging?.toString());
|
||||||
|
SessionStorageUtility.setEntry("accountName", accountName);
|
||||||
|
SessionStorageUtility.setEntry("collectionThroughput", collectionThroughput?.toString());
|
||||||
|
SessionStorageUtility.setEntry("dbThroughput", dbThoughput?.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initialize = async (): Promise<InitializeResponse> => {
|
||||||
|
const regions = Regions[SessionStorageUtility.getEntry("regions") as keyof typeof Regions];
|
||||||
|
const enableLogging = SessionStorageUtility.getEntry("enableLogging") === "true";
|
||||||
|
const accountName = SessionStorageUtility.getEntry("accountName");
|
||||||
|
let collectionThroughput = parseInt(SessionStorageUtility.getEntry("collectionThroughput"));
|
||||||
|
collectionThroughput = isNaN(collectionThroughput) ? undefined : collectionThroughput;
|
||||||
|
let dbThroughput = parseInt(SessionStorageUtility.getEntry("dbThroughput"));
|
||||||
|
dbThroughput = isNaN(dbThroughput) ? undefined : dbThroughput;
|
||||||
|
return {
|
||||||
|
regions: regions,
|
||||||
|
enableLogging: enableLogging,
|
||||||
|
accountName: accountName,
|
||||||
|
collectionThroughput: collectionThroughput,
|
||||||
|
dbThroughput: dbThroughput,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onRefreshSelfServeExample = async (): Promise<RefreshResult> => {
|
||||||
|
const subscriptionId = userContext.subscriptionId;
|
||||||
|
const resourceGroup = userContext.resourceGroup;
|
||||||
|
const databaseAccountName = userContext.databaseAccount.name;
|
||||||
|
const databaseAccountGetResults = await get(subscriptionId, resourceGroup, databaseAccountName);
|
||||||
|
const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded";
|
||||||
|
return {
|
||||||
|
isUpdateInProgress: isUpdateInProgress,
|
||||||
|
notificationMessage: "RefreshMessage",
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,37 +1,66 @@
|
|||||||
import { PropertyInfo, OnChange, Values } from "../PropertyDecorators";
|
import { PropertyInfo, OnChange, Values, IsDisplayable, ClassInfo } from "../Decorators";
|
||||||
import { ClassInfo, IsDisplayable } from "../ClassDecorators";
|
import {
|
||||||
import { SelfServeBaseClass } from "../SelfServeUtils";
|
ChoiceItem,
|
||||||
import { ChoiceItem, Info, InputType, UiType } from "../../Explorer/Controls/SmartUi/SmartUiComponent";
|
Info,
|
||||||
import { SessionStorageUtility } from "../../Shared/StorageUtility";
|
InputType,
|
||||||
|
NumberUiType,
|
||||||
|
RefreshResult,
|
||||||
|
SelfServeBaseClass,
|
||||||
|
SelfServeNotification,
|
||||||
|
SelfServeNotificationType,
|
||||||
|
SmartUiInput,
|
||||||
|
} from "../SelfServeTypes";
|
||||||
|
import {
|
||||||
|
onRefreshSelfServeExample,
|
||||||
|
Regions,
|
||||||
|
update,
|
||||||
|
initialize,
|
||||||
|
getMinDatabaseThroughput,
|
||||||
|
getMaxDatabaseThroughput,
|
||||||
|
getMinCollectionThroughput,
|
||||||
|
getMaxCollectionThroughput,
|
||||||
|
} from "./SelfServeExample.rp";
|
||||||
|
|
||||||
export enum Regions {
|
const regionDropdownItems: ChoiceItem[] = [
|
||||||
NorthCentralUS = "NCUS",
|
|
||||||
WestUS = "WUS",
|
|
||||||
EastUS2 = "EUS2",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const regionDropdownItems: ChoiceItem[] = [
|
|
||||||
{ label: "North Central US", key: Regions.NorthCentralUS },
|
{ label: "North Central US", key: Regions.NorthCentralUS },
|
||||||
{ label: "West US", key: Regions.WestUS },
|
{ label: "West US", key: Regions.WestUS },
|
||||||
{ label: "East US 2", key: Regions.EastUS2 },
|
{ label: "East US 2", key: Regions.EastUS2 },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const selfServeExampleInfo: Info = {
|
const selfServeExampleInfo: Info = {
|
||||||
message: "This is a self serve class",
|
messageTKey: "ClassInfo",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const regionDropdownInfo: Info = {
|
const regionDropdownInfo: Info = {
|
||||||
message: "More regions can be added in the future.",
|
messageTKey: "RegionDropdownInfo",
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDbThroughputChange = (currentState: Map<string, InputType>, newValue: InputType): Map<string, InputType> => {
|
const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: InputType): Map<string, SmartUiInput> => {
|
||||||
currentState.set("dbThroughput", newValue);
|
currentState.set("regions", { value: newValue });
|
||||||
currentState.set("collectionThroughput", newValue);
|
const currentEnableLogging = currentState.get("enableLogging");
|
||||||
|
if (newValue === Regions.NorthCentralUS) {
|
||||||
|
currentState.set("enableLogging", { value: false, disabled: true });
|
||||||
|
} else {
|
||||||
|
currentState.set("enableLogging", { value: currentEnableLogging.value, disabled: false });
|
||||||
|
}
|
||||||
return currentState;
|
return currentState;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initializeMaxThroughput = async (): Promise<number> => {
|
const onEnableDbLevelThroughputChange = (
|
||||||
return 10000;
|
currentState: Map<string, SmartUiInput>,
|
||||||
|
newValue: InputType
|
||||||
|
): Map<string, SmartUiInput> => {
|
||||||
|
currentState.set("enableDbLevelThroughput", { value: newValue });
|
||||||
|
const currentDbThroughput = currentState.get("dbThroughput");
|
||||||
|
const isDbThroughputHidden = newValue === undefined || !(newValue as boolean);
|
||||||
|
currentState.set("dbThroughput", { value: currentDbThroughput.value, hidden: isDbThroughputHidden });
|
||||||
|
return currentState;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = (currentvalues: Map<string, SmartUiInput>): void => {
|
||||||
|
if (!currentvalues.get("regions").value || !currentvalues.get("accountName").value) {
|
||||||
|
throw new Error("ValidationError");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -40,11 +69,15 @@ const initializeMaxThroughput = async (): Promise<number> => {
|
|||||||
Each self serve class
|
Each self serve class
|
||||||
- Needs to extends the SelfServeBase class.
|
- Needs to extends the SelfServeBase class.
|
||||||
- Needs to have the @IsDisplayable() decorator to tell the compiler that UI needs to be generated from this class.
|
- Needs to have the @IsDisplayable() decorator to tell the compiler that UI needs to be generated from this class.
|
||||||
- Needs to define an onSubmit() function, a callback for when the submit button is clicked.
|
- Needs to define an onSave() function, a callback for when the submit button is clicked.
|
||||||
- Needs to define an initialize() function, to set default values for the inputs.
|
- Needs to define an initialize() function, to set default values for the inputs.
|
||||||
|
- Needs to define an onRefresh() function, a callback for when the refresh button is clicked.
|
||||||
|
|
||||||
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.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -61,25 +94,46 @@ const initializeMaxThroughput = async (): Promise<number> => {
|
|||||||
@ClassInfo(selfServeExampleInfo)
|
@ClassInfo(selfServeExampleInfo)
|
||||||
export default class SelfServeExample extends SelfServeBaseClass {
|
export default class SelfServeExample extends SelfServeBaseClass {
|
||||||
/*
|
/*
|
||||||
onSubmit()
|
onRefresh()
|
||||||
|
- role : Callback that is triggerrd when the refresh button is clicked. You should perform the your rest API
|
||||||
|
call to check if the update action is completed.
|
||||||
|
- returns:
|
||||||
|
RefreshResult -
|
||||||
|
isComponentUpdating: Indicated if the state is still being updated
|
||||||
|
notificationMessage: Notification message to be shown in case the component is still being updated
|
||||||
|
i.e, isComponentUpdating is true
|
||||||
|
*/
|
||||||
|
public onRefresh = async (): Promise<RefreshResult> => {
|
||||||
|
return onRefreshSelfServeExample();
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
onSave()
|
||||||
- input: (currentValues: Map<string, InputType>) => Promise<void>
|
- input: (currentValues: Map<string, InputType>) => Promise<void>
|
||||||
- role: Callback that is triggerred when the submit button is clicked. You should perform your rest API
|
- role: Callback that is triggerred when the submit button is clicked. You should perform your rest API
|
||||||
calls here using the data from the different inputs passed as a Map to this callback function.
|
calls here using the data from the different inputs passed as a Map to this callback function.
|
||||||
|
|
||||||
In this example, the onSubmit callback simply sets the value for keys corresponding to the field name
|
In this example, the onSave callback simply sets the value for keys corresponding to the field name
|
||||||
in the SessionStorage.
|
in the SessionStorage.
|
||||||
|
- returns: SelfServeNotification -
|
||||||
|
message: The message to be displayed in the message bar after the onSave is completed
|
||||||
|
type: The type of message bar to be used (info, warning, error)
|
||||||
*/
|
*/
|
||||||
public onSubmit = async (currentValues: Map<string, InputType>): Promise<void> => {
|
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<SelfServeNotification> => {
|
||||||
SessionStorageUtility.setEntry("regions", currentValues.get("regions")?.toString());
|
validate(currentValues);
|
||||||
SessionStorageUtility.setEntry("enableLogging", currentValues.get("enableLogging")?.toString());
|
const regions = Regions[currentValues.get("regions")?.value as keyof typeof Regions];
|
||||||
SessionStorageUtility.setEntry("accountName", currentValues.get("accountName")?.toString());
|
const enableLogging = currentValues.get("enableLogging")?.value as boolean;
|
||||||
SessionStorageUtility.setEntry("dbThroughput", currentValues.get("dbThroughput")?.toString());
|
const accountName = currentValues.get("accountName")?.value as string;
|
||||||
SessionStorageUtility.setEntry("collectionThroughput", currentValues.get("collectionThroughput")?.toString());
|
const collectionThroughput = currentValues.get("collectionThroughput")?.value as number;
|
||||||
|
const enableDbLevelThroughput = currentValues.get("enableDbLevelThroughput")?.value as boolean;
|
||||||
|
let dbThroughput = currentValues.get("dbThroughput")?.value as number;
|
||||||
|
dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined;
|
||||||
|
await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput);
|
||||||
|
return { message: "SubmissionMessage", type: SelfServeNotificationType.info };
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
initialize()
|
initialize()
|
||||||
- input: () => Promise<Map<string, InputType>>
|
|
||||||
- role: Set default values for the properties of this class.
|
- role: Set default values for the properties of this class.
|
||||||
|
|
||||||
The properties of this class (namely regions, enableLogging, accountName, dbThroughput, collectionThroughput),
|
The properties of this class (namely regions, enableLogging, accountName, dbThroughput, collectionThroughput),
|
||||||
@@ -87,24 +141,46 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
|||||||
defaults can be set by setting values in a Map corresponding to the field's name.
|
defaults can be set by setting values in a Map corresponding to the field's name.
|
||||||
|
|
||||||
Typically, you can make rest calls in the async initialize function, to fetch the initial values for
|
Typically, you can make rest calls in the async initialize function, to fetch the initial values for
|
||||||
these fields. This is called after the onSubmit callback, to reinitialize the defaults.
|
these fields. This is called after the onSave callback, to reinitialize the defaults.
|
||||||
|
|
||||||
In this example, the initialize function simply reads the SessionStorage to fetch the default values
|
In this example, the initialize function simply reads the SessionStorage to fetch the default values
|
||||||
for these fields. These are then set when the changes are submitted.
|
for these fields. These are then set when the changes are submitted.
|
||||||
|
- returns: () => Promise<Map<string, InputType>>
|
||||||
*/
|
*/
|
||||||
public initialize = async (): Promise<Map<string, InputType>> => {
|
public initialize = async (): Promise<Map<string, SmartUiInput>> => {
|
||||||
const defaults = new Map<string, InputType>();
|
const initializeResponse = await initialize();
|
||||||
defaults.set("regions", SessionStorageUtility.getEntry("regions"));
|
const defaults = new Map<string, SmartUiInput>();
|
||||||
defaults.set("enableLogging", SessionStorageUtility.getEntry("enableLogging") === "true");
|
defaults.set("regions", { value: initializeResponse.regions });
|
||||||
const stringInput = SessionStorageUtility.getEntry("accountName");
|
defaults.set("enableLogging", { value: initializeResponse.enableLogging });
|
||||||
defaults.set("accountName", stringInput ? stringInput : "");
|
const accountName = initializeResponse.accountName;
|
||||||
const numberSliderInput = parseInt(SessionStorageUtility.getEntry("dbThroughput"));
|
defaults.set("accountName", { value: accountName ? accountName : "" });
|
||||||
defaults.set("dbThroughput", isNaN(numberSliderInput) ? 1 : numberSliderInput);
|
defaults.set("collectionThroughput", { value: initializeResponse.collectionThroughput });
|
||||||
const numberSpinnerInput = parseInt(SessionStorageUtility.getEntry("collectionThroughput"));
|
const enableDbLevelThroughput = !!initializeResponse.dbThroughput;
|
||||||
defaults.set("collectionThroughput", isNaN(numberSpinnerInput) ? 1 : numberSpinnerInput);
|
defaults.set("enableDbLevelThroughput", { value: enableDbLevelThroughput });
|
||||||
|
defaults.set("dbThroughput", { value: initializeResponse.dbThroughput, hidden: !enableDbLevelThroughput });
|
||||||
return defaults;
|
return defaults;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
@Values() :
|
||||||
|
- input: NumberInputOptions | StringInputOptions | BooleanInputOptions | ChoiceInputOptions | DescriptionDisplay
|
||||||
|
- role: Specifies the required options to display the property as
|
||||||
|
a) TextBox for text input
|
||||||
|
b) Spinner/Slider for number input
|
||||||
|
c) Radio buton/Toggle for boolean input
|
||||||
|
d) Dropdown for choice input
|
||||||
|
e) Text (with optional hyperlink) for descriptions
|
||||||
|
*/
|
||||||
|
@Values({
|
||||||
|
description: {
|
||||||
|
textTKey: "DescriptionText",
|
||||||
|
link: {
|
||||||
|
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
||||||
|
textTKey: "DecriptionLinkText",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
description: string;
|
||||||
/*
|
/*
|
||||||
@PropertyInfo()
|
@PropertyInfo()
|
||||||
- optional
|
- optional
|
||||||
@@ -114,54 +190,64 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
|||||||
@PropertyInfo(regionDropdownInfo)
|
@PropertyInfo(regionDropdownInfo)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@Values() :
|
@OnChange()
|
||||||
- input: NumberInputOptions | StringInputOptions | BooleanInputOptions | ChoiceInputOptions
|
- optional
|
||||||
- role: Specifies the required options to display the property as TextBox, Number Spinner/Slider, Radio buton or Dropdown.
|
- input: (currentValues: Map<string, InputType>, newValue: InputType) => Map<string, InputType>
|
||||||
|
- role: Takes a Map of current values and the newValue for this property as inputs. This is called when a property,
|
||||||
|
say prop1, changes its value in the UI. This can be used to
|
||||||
|
a) Change the value (and reflect it in the UI) for prop2 based on prop1.
|
||||||
|
b) Change the visibility for prop2 in the UI, based on prop1
|
||||||
|
|
||||||
|
The new Map of propertyName -> value is returned.
|
||||||
|
|
||||||
|
In this example, the onRegionsChange function sets the enableLogging property to false (and disables
|
||||||
|
the corresponsing toggle UI) when "regions" is set to "North Central US", and enables the toggle for
|
||||||
|
any other value of "regions"
|
||||||
*/
|
*/
|
||||||
@Values({ label: "Regions", choices: regionDropdownItems })
|
@OnChange(onRegionsChange)
|
||||||
|
@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;
|
||||||
|
|
||||||
/*
|
|
||||||
@OnChange()
|
|
||||||
- optional
|
|
||||||
- input: (currentValues: Map<string, InputType>, newValue: InputType) => Map<string, InputType>
|
|
||||||
- role: Takes a Map of current values and the newValue for this property as inputs. This is called when a property
|
|
||||||
changes its value in the UI. This can be used to change other input values based on some other input.
|
|
||||||
|
|
||||||
The new Map of propertyName -> value is returned.
|
|
||||||
|
|
||||||
In this example, the onDbThroughputChange function sets the collectionThroughput to the same value as the dbThroughput
|
|
||||||
when the slider in moved in the UI.
|
|
||||||
*/
|
|
||||||
@OnChange(onDbThroughputChange)
|
|
||||||
@Values({
|
@Values({
|
||||||
label: "Database Throughput",
|
labelTKey: "Collection Throughput",
|
||||||
min: 400,
|
min: getMinCollectionThroughput,
|
||||||
max: initializeMaxThroughput,
|
max: getMaxCollectionThroughput,
|
||||||
step: 100,
|
step: 100,
|
||||||
uiType: UiType.Slider,
|
uiType: NumberUiType.Spinner,
|
||||||
})
|
|
||||||
dbThroughput: number;
|
|
||||||
|
|
||||||
@Values({
|
|
||||||
label: "Collection Throughput",
|
|
||||||
min: 400,
|
|
||||||
max: initializeMaxThroughput,
|
|
||||||
step: 100,
|
|
||||||
uiType: UiType.Spinner,
|
|
||||||
})
|
})
|
||||||
collectionThroughput: number;
|
collectionThroughput: number;
|
||||||
|
|
||||||
|
/*
|
||||||
|
In this example, the onEnableDbLevelThroughputChange function makes the dbThroughput property visible when
|
||||||
|
enableDbLevelThroughput, a boolean, is set to true and hides dbThroughput property when it is set to false.
|
||||||
|
*/
|
||||||
|
@OnChange(onEnableDbLevelThroughputChange)
|
||||||
|
@Values({
|
||||||
|
labelTKey: "Enable DB level throughput",
|
||||||
|
trueLabelTKey: "Enable",
|
||||||
|
falseLabelTKey: "Disable",
|
||||||
|
})
|
||||||
|
enableDbLevelThroughput: boolean;
|
||||||
|
|
||||||
|
@Values({
|
||||||
|
labelTKey: "Database Throughput",
|
||||||
|
min: getMinDatabaseThroughput,
|
||||||
|
max: getMaxDatabaseThroughput,
|
||||||
|
step: 100,
|
||||||
|
uiType: NumberUiType.Slider,
|
||||||
|
})
|
||||||
|
dbThroughput: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,63 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { shallow } from "enzyme";
|
import { shallow } from "enzyme";
|
||||||
import { SelfServeDescriptor, SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent";
|
import { SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent";
|
||||||
import { InputType, UiType } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
import { NumberUiType, SelfServeDescriptor, SelfServeNotificationType, SmartUiInput } from "./SelfServeTypes";
|
||||||
|
|
||||||
describe("SelfServeComponent", () => {
|
describe("SelfServeComponent", () => {
|
||||||
const defaultValues = new Map<string, InputType>([
|
const defaultValues = new Map<string, SmartUiInput>([
|
||||||
["throughput", "450"],
|
["throughput", { value: 450 }],
|
||||||
["analyticalStore", "false"],
|
["analyticalStore", { value: false }],
|
||||||
["database", "db2"],
|
["database", { value: "db2" }],
|
||||||
]);
|
]);
|
||||||
const initializeMock = jest.fn(async () => defaultValues);
|
const updatedValues = new Map<string, SmartUiInput>([
|
||||||
const onSubmitMock = jest.fn(async () => {
|
["throughput", { value: 460 }],
|
||||||
return;
|
["analyticalStore", { value: true }],
|
||||||
|
["database", { value: "db2" }],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const initializeMock = jest.fn(async () => new Map(defaultValues));
|
||||||
|
const onSaveMock = jest.fn(async () => {
|
||||||
|
return { message: "submitted successfully", type: SelfServeNotificationType.info };
|
||||||
|
});
|
||||||
|
const onRefreshMock = jest.fn(async () => {
|
||||||
|
return { isUpdateInProgress: false, notificationMessage: "refresh performed successfully" };
|
||||||
|
});
|
||||||
|
const onRefreshIsUpdatingMock = jest.fn(async () => {
|
||||||
|
return { isUpdateInProgress: true, notificationMessage: "refresh performed successfully" };
|
||||||
});
|
});
|
||||||
|
|
||||||
const exampleData: SelfServeDescriptor = {
|
const exampleData: SelfServeDescriptor = {
|
||||||
initialize: initializeMock,
|
initialize: initializeMock,
|
||||||
onSubmit: onSubmitMock,
|
onSave: onSaveMock,
|
||||||
inputNames: ["throughput", "containerId", "analyticalStore", "database"],
|
onRefresh: onRefreshMock,
|
||||||
|
inputNames: ["throughput", "analyticalStore", "database"],
|
||||||
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,
|
||||||
max: 500,
|
max: 500,
|
||||||
step: 10,
|
step: 10,
|
||||||
defaultValue: 400,
|
defaultValue: 400,
|
||||||
uiType: UiType.Spinner,
|
uiType: NumberUiType.Spinner,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "containerId",
|
id: "containerId",
|
||||||
input: {
|
input: {
|
||||||
label: "Container id",
|
labelTKey: "Container id",
|
||||||
dataFieldName: "containerId",
|
dataFieldName: "containerId",
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
@@ -52,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",
|
||||||
@@ -63,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: [
|
||||||
@@ -78,27 +91,109 @@ describe("SelfServeComponent", () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const verifyDefaultsSet = (currentValues: Map<string, InputType>): void => {
|
const isEqual = (source: Map<string, SmartUiInput>, target: Map<string, SmartUiInput>): void => {
|
||||||
for (const key of currentValues.keys()) {
|
expect(target.size).toEqual(source.size);
|
||||||
if (defaultValues.has(key)) {
|
for (const key of source.keys()) {
|
||||||
expect(defaultValues.get(key)).toEqual(currentValues.get(key));
|
expect(target.get(key)).toEqual(source.get(key));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should render", async () => {
|
it("should render and honor save, discard, refresh actions", async () => {
|
||||||
const wrapper = shallow(<SelfServeComponent descriptor={exampleData} />);
|
const wrapper = shallow(<SelfServeComponent descriptor={exampleData} />);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
// initialize() should be called and defaults should be set when component is mounted
|
// initialize() and onRefresh() should be called and defaults should be set when component is mounted
|
||||||
expect(initializeMock).toHaveBeenCalled();
|
expect(initializeMock).toHaveBeenCalledTimes(1);
|
||||||
const state = wrapper.state() as SelfServeComponentState;
|
expect(onRefreshMock).toHaveBeenCalledTimes(1);
|
||||||
verifyDefaultsSet(state.currentValues);
|
let state = wrapper.state() as SelfServeComponentState;
|
||||||
|
isEqual(state.currentValues, defaultValues);
|
||||||
|
|
||||||
// onSubmit() must be called when submit button is clicked
|
// when currentValues and baselineValues differ, save and discard should not be disabled
|
||||||
const submitButton = wrapper.find("#submitButton");
|
wrapper.setState({ currentValues: updatedValues });
|
||||||
submitButton.simulate("click");
|
wrapper.update();
|
||||||
expect(onSubmitMock).toHaveBeenCalled();
|
state = wrapper.state() as SelfServeComponentState;
|
||||||
|
isEqual(state.currentValues, updatedValues);
|
||||||
|
const selfServeComponent = wrapper.instance() as SelfServeComponent;
|
||||||
|
expect(selfServeComponent.isSaveButtonDisabled()).toBeFalsy();
|
||||||
|
expect(selfServeComponent.isDiscardButtonDisabled()).toBeFalsy();
|
||||||
|
|
||||||
|
// when errors exist, save is disabled but discard is enabled
|
||||||
|
wrapper.setState({ hasErrors: true });
|
||||||
|
wrapper.update();
|
||||||
|
state = wrapper.state() as SelfServeComponentState;
|
||||||
|
expect(selfServeComponent.isSaveButtonDisabled()).toBeTruthy();
|
||||||
|
expect(selfServeComponent.isDiscardButtonDisabled()).toBeFalsy();
|
||||||
|
|
||||||
|
// discard resets currentValues to baselineValues
|
||||||
|
selfServeComponent.discard();
|
||||||
|
state = wrapper.state() as SelfServeComponentState;
|
||||||
|
isEqual(state.currentValues, defaultValues);
|
||||||
|
isEqual(state.currentValues, state.baselineValues);
|
||||||
|
|
||||||
|
// resetBaselineValues sets baselineValues to currentValues
|
||||||
|
wrapper.setState({ baselineValues: updatedValues });
|
||||||
|
wrapper.update();
|
||||||
|
state = wrapper.state() as SelfServeComponentState;
|
||||||
|
isEqual(state.baselineValues, updatedValues);
|
||||||
|
selfServeComponent.resetBaselineValues();
|
||||||
|
state = wrapper.state() as SelfServeComponentState;
|
||||||
|
isEqual(state.baselineValues, defaultValues);
|
||||||
|
isEqual(state.currentValues, state.baselineValues);
|
||||||
|
|
||||||
|
// clicking refresh calls onRefresh. If component is not updating, it calls initialize() as well
|
||||||
|
selfServeComponent.onRefreshClicked();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
expect(onRefreshMock).toHaveBeenCalledTimes(2);
|
||||||
|
expect(initializeMock).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
selfServeComponent.onSaveButtonClick();
|
||||||
|
expect(onSaveMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getResolvedValue", async () => {
|
||||||
|
const wrapper = shallow(<SelfServeComponent descriptor={exampleData} />);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
const selfServeComponent = wrapper.instance() as SelfServeComponent;
|
||||||
|
|
||||||
|
const numberResult = 1;
|
||||||
|
const numberPromise = async (): Promise<number> => {
|
||||||
|
return numberResult;
|
||||||
|
};
|
||||||
|
expect(await selfServeComponent.getResolvedValue(numberResult)).toEqual(numberResult);
|
||||||
|
expect(await selfServeComponent.getResolvedValue(numberPromise)).toEqual(numberResult);
|
||||||
|
|
||||||
|
const stringResult = "result";
|
||||||
|
const stringPromise = async (): Promise<string> => {
|
||||||
|
return stringResult;
|
||||||
|
};
|
||||||
|
expect(await selfServeComponent.getResolvedValue(stringResult)).toEqual(stringResult);
|
||||||
|
expect(await selfServeComponent.getResolvedValue(stringPromise)).toEqual(stringResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("message bar and spinner snapshots", async () => {
|
||||||
|
const newDescriptor = { ...exampleData, onRefresh: onRefreshIsUpdatingMock };
|
||||||
|
let wrapper = shallow(<SelfServeComponent descriptor={newDescriptor} />);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
let selfServeComponent = wrapper.instance() as SelfServeComponent;
|
||||||
|
selfServeComponent.onSaveButtonClick();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
|
newDescriptor.onRefresh = onRefreshMock;
|
||||||
|
wrapper = shallow(<SelfServeComponent descriptor={newDescriptor} />);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
selfServeComponent = wrapper.instance() as SelfServeComponent;
|
||||||
|
selfServeComponent.onSaveButtonClick();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
|
wrapper.setState({ isInitializing: true });
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
|
wrapper.setState({ compileErrorMessage: "sample error message" });
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,62 +1,34 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { IStackTokens, PrimaryButton, Spinner, SpinnerSize, Stack } from "office-ui-fabric-react";
|
|
||||||
import {
|
import {
|
||||||
ChoiceItem,
|
CommandBar,
|
||||||
|
ICommandBarItemProps,
|
||||||
|
IStackTokens,
|
||||||
|
MessageBar,
|
||||||
|
MessageBarType,
|
||||||
|
Spinner,
|
||||||
|
SpinnerSize,
|
||||||
|
Stack,
|
||||||
|
} from "office-ui-fabric-react";
|
||||||
|
import {
|
||||||
|
AnyDisplay,
|
||||||
|
Node,
|
||||||
InputType,
|
InputType,
|
||||||
InputTypeValue,
|
RefreshResult,
|
||||||
SmartUiComponent,
|
SelfServeDescriptor,
|
||||||
UiType,
|
SelfServeNotification,
|
||||||
SmartUiDescriptor,
|
SmartUiInput,
|
||||||
Info,
|
DescriptionDisplay,
|
||||||
} from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
StringInput,
|
||||||
|
NumberInput,
|
||||||
export interface BaseInput {
|
BooleanInput,
|
||||||
label: (() => Promise<string>) | string;
|
ChoiceInput,
|
||||||
dataFieldName: string;
|
SelfServeNotificationType,
|
||||||
type: InputTypeValue;
|
} from "./SelfServeTypes";
|
||||||
onChange?: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>;
|
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
||||||
placeholder?: (() => Promise<string>) | string;
|
import { getMessageBarType } from "./SelfServeUtils";
|
||||||
errorMessage?: string;
|
import { Translation } from "react-i18next";
|
||||||
}
|
import { TFunction } from "i18next";
|
||||||
|
import "../i18n";
|
||||||
export interface NumberInput extends BaseInput {
|
|
||||||
min: (() => Promise<number>) | number;
|
|
||||||
max: (() => Promise<number>) | number;
|
|
||||||
step: (() => Promise<number>) | number;
|
|
||||||
defaultValue?: number;
|
|
||||||
uiType: UiType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BooleanInput extends BaseInput {
|
|
||||||
trueLabel: (() => Promise<string>) | string;
|
|
||||||
falseLabel: (() => Promise<string>) | string;
|
|
||||||
defaultValue?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StringInput extends BaseInput {
|
|
||||||
defaultValue?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChoiceInput extends BaseInput {
|
|
||||||
choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
|
|
||||||
defaultKey?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Node {
|
|
||||||
id: string;
|
|
||||||
info?: (() => Promise<Info>) | Info;
|
|
||||||
input?: AnyInput;
|
|
||||||
children?: Node[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SelfServeDescriptor {
|
|
||||||
root: Node;
|
|
||||||
initialize?: () => Promise<Map<string, InputType>>;
|
|
||||||
onSubmit?: (currentValues: Map<string, InputType>) => Promise<void>;
|
|
||||||
inputNames?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AnyInput = NumberInput | BooleanInput | StringInput | ChoiceInput;
|
|
||||||
|
|
||||||
export interface SelfServeComponentProps {
|
export interface SelfServeComponentProps {
|
||||||
descriptor: SelfServeDescriptor;
|
descriptor: SelfServeDescriptor;
|
||||||
@@ -64,13 +36,20 @@ export interface SelfServeComponentProps {
|
|||||||
|
|
||||||
export interface SelfServeComponentState {
|
export interface SelfServeComponentState {
|
||||||
root: SelfServeDescriptor;
|
root: SelfServeDescriptor;
|
||||||
currentValues: Map<string, InputType>;
|
currentValues: Map<string, SmartUiInput>;
|
||||||
baselineValues: Map<string, InputType>;
|
baselineValues: Map<string, SmartUiInput>;
|
||||||
isRefreshing: boolean;
|
isInitializing: boolean;
|
||||||
|
hasErrors: boolean;
|
||||||
|
compileErrorMessage: string;
|
||||||
|
notification: SelfServeNotification;
|
||||||
|
refreshResult: RefreshResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
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.initializeSmartUiComponent();
|
this.initializeSmartUiComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,62 +59,109 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
root: this.props.descriptor,
|
root: this.props.descriptor,
|
||||||
currentValues: new Map(),
|
currentValues: new Map(),
|
||||||
baselineValues: new Map(),
|
baselineValues: new Map(),
|
||||||
isRefreshing: false,
|
isInitializing: true,
|
||||||
|
hasErrors: false,
|
||||||
|
compileErrorMessage: undefined,
|
||||||
|
notification: undefined,
|
||||||
|
refreshResult: undefined,
|
||||||
};
|
};
|
||||||
|
this.smartUiGeneratorClassName = this.props.descriptor.root.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onError = (hasErrors: boolean): void => {
|
||||||
|
this.setState({ hasErrors });
|
||||||
|
};
|
||||||
|
|
||||||
private initializeSmartUiComponent = async (): Promise<void> => {
|
private initializeSmartUiComponent = async (): Promise<void> => {
|
||||||
this.setState({ isRefreshing: true });
|
this.setState({ isInitializing: true });
|
||||||
await this.initializeSmartUiNode(this.props.descriptor.root);
|
|
||||||
await this.setDefaults();
|
await this.setDefaults();
|
||||||
this.setState({ isRefreshing: false });
|
const { currentValues, baselineValues } = this.state;
|
||||||
|
await this.initializeSmartUiNode(this.props.descriptor.root, currentValues, baselineValues);
|
||||||
|
this.setState({ isInitializing: false, currentValues, baselineValues });
|
||||||
};
|
};
|
||||||
|
|
||||||
private setDefaults = async (): Promise<void> => {
|
private setDefaults = async (): Promise<void> => {
|
||||||
this.setState({ isRefreshing: true });
|
|
||||||
let { currentValues, baselineValues } = this.state;
|
let { currentValues, baselineValues } = this.state;
|
||||||
|
|
||||||
const initialValues = await this.props.descriptor.initialize();
|
const initialValues = await this.props.descriptor.initialize();
|
||||||
|
this.props.descriptor.inputNames.map((inputName) => {
|
||||||
|
let initialValue = initialValues.get(inputName);
|
||||||
|
if (!initialValue) {
|
||||||
|
initialValue = { value: undefined, hidden: false };
|
||||||
|
}
|
||||||
|
currentValues = currentValues.set(inputName, initialValue);
|
||||||
|
baselineValues = baselineValues.set(inputName, initialValue);
|
||||||
|
initialValues.delete(inputName);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (initialValues.size > 0) {
|
||||||
|
const keys = [];
|
||||||
for (const key of initialValues.keys()) {
|
for (const key of initialValues.keys()) {
|
||||||
if (this.props.descriptor.inputNames.indexOf(key) === -1) {
|
keys.push(key);
|
||||||
this.setState({ isRefreshing: false });
|
|
||||||
throw new Error(`${key} is not an input property of this class.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentValues = currentValues.set(key, initialValues.get(key));
|
this.setState({
|
||||||
baselineValues = baselineValues.set(key, initialValues.get(key));
|
compileErrorMessage: `The following fields have default values set but are not input properties of this class: ${keys.join(
|
||||||
|
", "
|
||||||
|
)}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.setState({ currentValues, baselineValues, isRefreshing: false });
|
this.setState({ currentValues, baselineValues });
|
||||||
|
};
|
||||||
|
|
||||||
|
public resetBaselineValues = (): void => {
|
||||||
|
const currentValues = this.state.currentValues;
|
||||||
|
let baselineValues = this.state.baselineValues;
|
||||||
|
for (const key of currentValues.keys()) {
|
||||||
|
const currentValue = currentValues.get(key);
|
||||||
|
baselineValues = baselineValues.set(key, { ...currentValue });
|
||||||
|
}
|
||||||
|
this.setState({ baselineValues });
|
||||||
};
|
};
|
||||||
|
|
||||||
public discard = (): void => {
|
public discard = (): void => {
|
||||||
let { currentValues } = this.state;
|
let { currentValues } = this.state;
|
||||||
const { baselineValues } = this.state;
|
const { baselineValues } = this.state;
|
||||||
for (const key of baselineValues.keys()) {
|
for (const key of currentValues.keys()) {
|
||||||
currentValues = currentValues.set(key, baselineValues.get(key));
|
const baselineValue = baselineValues.get(key);
|
||||||
|
currentValues = currentValues.set(key, { ...baselineValue });
|
||||||
}
|
}
|
||||||
this.setState({ currentValues });
|
this.setState({ currentValues });
|
||||||
};
|
};
|
||||||
|
|
||||||
private initializeSmartUiNode = async (currentNode: Node): Promise<void> => {
|
private initializeSmartUiNode = async (
|
||||||
|
currentNode: Node,
|
||||||
|
currentValues: Map<string, SmartUiInput>,
|
||||||
|
baselineValues: Map<string, SmartUiInput>
|
||||||
|
): Promise<void> => {
|
||||||
currentNode.info = await this.getResolvedValue(currentNode.info);
|
currentNode.info = await this.getResolvedValue(currentNode.info);
|
||||||
|
|
||||||
if (currentNode.input) {
|
if (currentNode.input) {
|
||||||
currentNode.input = await this.getResolvedInput(currentNode.input);
|
currentNode.input = await this.getResolvedInput(currentNode.input, currentValues, baselineValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises = currentNode.children?.map(async (child: Node) => await this.initializeSmartUiNode(child));
|
const promises = currentNode.children?.map(
|
||||||
|
async (child: Node) => await this.initializeSmartUiNode(child, currentValues, baselineValues)
|
||||||
|
);
|
||||||
if (promises) {
|
if (promises) {
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private getResolvedInput = async (input: AnyInput): Promise<AnyInput> => {
|
private getResolvedInput = async (
|
||||||
input.label = await this.getResolvedValue(input.label);
|
input: AnyDisplay,
|
||||||
input.placeholder = await this.getResolvedValue(input.placeholder);
|
currentValues: Map<string, SmartUiInput>,
|
||||||
|
baselineValues: Map<string, SmartUiInput>
|
||||||
|
): Promise<AnyDisplay> => {
|
||||||
|
input.labelTKey = await this.getResolvedValue(input.labelTKey);
|
||||||
|
input.placeholderTKey = await this.getResolvedValue(input.placeholderTKey);
|
||||||
|
|
||||||
switch (input.type) {
|
switch (input.type) {
|
||||||
case "string": {
|
case "string": {
|
||||||
|
if ("description" in input) {
|
||||||
|
const descriptionDisplay = input as DescriptionDisplay;
|
||||||
|
descriptionDisplay.description = await this.getResolvedValue(descriptionDisplay.description);
|
||||||
|
}
|
||||||
return input as StringInput;
|
return input as StringInput;
|
||||||
}
|
}
|
||||||
case "number": {
|
case "number": {
|
||||||
@@ -143,12 +169,22 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
numberInput.min = await this.getResolvedValue(numberInput.min);
|
numberInput.min = await this.getResolvedValue(numberInput.min);
|
||||||
numberInput.max = await this.getResolvedValue(numberInput.max);
|
numberInput.max = await this.getResolvedValue(numberInput.max);
|
||||||
numberInput.step = await this.getResolvedValue(numberInput.step);
|
numberInput.step = await this.getResolvedValue(numberInput.step);
|
||||||
|
|
||||||
|
const dataFieldName = numberInput.dataFieldName;
|
||||||
|
const defaultValue = currentValues.get(dataFieldName)?.value;
|
||||||
|
|
||||||
|
if (!defaultValue) {
|
||||||
|
const newDefaultValue = { value: numberInput.min, hidden: currentValues.get(dataFieldName)?.hidden };
|
||||||
|
currentValues.set(dataFieldName, newDefaultValue);
|
||||||
|
baselineValues.set(dataFieldName, newDefaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
return numberInput;
|
return numberInput;
|
||||||
}
|
}
|
||||||
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: {
|
||||||
@@ -166,53 +202,180 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onInputChange = (input: AnyInput, newValue: InputType) => {
|
private onInputChange = (input: AnyDisplay, newValue: InputType) => {
|
||||||
if (input.onChange) {
|
if (input.onChange) {
|
||||||
const newValues = input.onChange(this.state.currentValues, newValue);
|
const newValues = input.onChange(this.state.currentValues, newValue);
|
||||||
this.setState({ currentValues: newValues });
|
this.setState({ currentValues: newValues });
|
||||||
} else {
|
} else {
|
||||||
const dataFieldName = input.dataFieldName;
|
const dataFieldName = input.dataFieldName;
|
||||||
const { currentValues } = this.state;
|
const { currentValues } = this.state;
|
||||||
currentValues.set(dataFieldName, newValue);
|
const currentInputValue = currentValues.get(dataFieldName);
|
||||||
|
currentValues.set(dataFieldName, { value: newValue, hidden: currentInputValue?.hidden });
|
||||||
this.setState({ currentValues });
|
this.setState({ currentValues });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public onSaveButtonClick = (): void => {
|
||||||
const containerStackTokens: IStackTokens = { childrenGap: 20 };
|
const onSavePromise = this.props.descriptor.onSave(this.state.currentValues);
|
||||||
return !this.state.isRefreshing ? (
|
onSavePromise.catch((error) => {
|
||||||
<div style={{ overflowX: "auto" }}>
|
this.setState({
|
||||||
<Stack tokens={containerStackTokens} styles={{ root: { width: 400, padding: 10 } }}>
|
notification: {
|
||||||
<SmartUiComponent
|
message: `${error.message}`,
|
||||||
descriptor={this.state.root as SmartUiDescriptor}
|
type: SelfServeNotificationType.error,
|
||||||
currentValues={this.state.currentValues}
|
},
|
||||||
onInputChange={this.onInputChange}
|
});
|
||||||
/>
|
});
|
||||||
|
onSavePromise.then((notification: SelfServeNotification) => {
|
||||||
|
this.setState({
|
||||||
|
notification: {
|
||||||
|
message: notification.message,
|
||||||
|
type: notification.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.resetBaselineValues();
|
||||||
|
this.onRefreshClicked();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
public isDiscardButtonDisabled = (): boolean => {
|
||||||
<PrimaryButton
|
for (const key of this.state.currentValues.keys()) {
|
||||||
id="submitButton"
|
const currentValue = JSON.stringify(this.state.currentValues.get(key));
|
||||||
styles={{ root: { width: 100 } }}
|
const baselineValue = JSON.stringify(this.state.baselineValues.get(key));
|
||||||
text="submit"
|
|
||||||
onClick={async () => {
|
if (currentValue !== baselineValue) {
|
||||||
await this.props.descriptor.onSubmit(this.state.currentValues);
|
return false;
|
||||||
this.setDefaults();
|
}
|
||||||
}}
|
}
|
||||||
/>
|
return true;
|
||||||
<PrimaryButton
|
};
|
||||||
id="discardButton"
|
|
||||||
styles={{ root: { width: 100 } }}
|
public isSaveButtonDisabled = (): boolean => {
|
||||||
text="discard"
|
if (this.state.hasErrors) {
|
||||||
onClick={() => this.discard()}
|
return true;
|
||||||
/>
|
}
|
||||||
</Stack>
|
for (const key of this.state.currentValues.keys()) {
|
||||||
</Stack>
|
const currentValue = JSON.stringify(this.state.currentValues.get(key));
|
||||||
</div>
|
const baselineValue = JSON.stringify(this.state.baselineValues.get(key));
|
||||||
) : (
|
|
||||||
|
if (currentValue !== baselineValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
private performRefresh = async (): Promise<RefreshResult> => {
|
||||||
|
const refreshResult = await this.props.descriptor.onRefresh();
|
||||||
|
this.setState({ refreshResult: { ...refreshResult } });
|
||||||
|
return refreshResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
public onRefreshClicked = async (): Promise<void> => {
|
||||||
|
this.setState({ isInitializing: true });
|
||||||
|
const refreshResult = await this.performRefresh();
|
||||||
|
if (!refreshResult.isUpdateInProgress) {
|
||||||
|
this.initializeSmartUiComponent();
|
||||||
|
}
|
||||||
|
this.setState({ isInitializing: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
public getCommonTranslation = (translationFunction: TFunction, key: string): string => {
|
||||||
|
return translationFunction(`Common.${key}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
private getCommandBarItems = (translate: TFunction): ICommandBarItemProps[] => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: "save",
|
||||||
|
text: this.getCommonTranslation(translate, "Save"),
|
||||||
|
iconProps: { iconName: "Save" },
|
||||||
|
split: true,
|
||||||
|
disabled: this.isSaveButtonDisabled(),
|
||||||
|
onClick: this.onSaveButtonClick,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "discard",
|
||||||
|
text: this.getCommonTranslation(translate, "Discard"),
|
||||||
|
iconProps: { iconName: "Undo" },
|
||||||
|
split: true,
|
||||||
|
disabled: this.isDiscardButtonDisabled(),
|
||||||
|
onClick: () => {
|
||||||
|
this.discard();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "refresh",
|
||||||
|
text: this.getCommonTranslation(translate, "Refresh"),
|
||||||
|
disabled: this.state.isInitializing,
|
||||||
|
iconProps: { iconName: "Refresh" },
|
||||||
|
split: true,
|
||||||
|
onClick: () => {
|
||||||
|
this.onRefreshClicked();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
private getNotificationMessageTranslation = (translationFunction: TFunction, messageKey: string): string => {
|
||||||
|
const translation = translationFunction(messageKey);
|
||||||
|
if (translation === `${this.smartUiGeneratorClassName}.${messageKey}`) {
|
||||||
|
return messageKey;
|
||||||
|
}
|
||||||
|
return translation;
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
const containerStackTokens: IStackTokens = { childrenGap: 5 };
|
||||||
|
if (this.state.compileErrorMessage) {
|
||||||
|
return <MessageBar messageBarType={MessageBarType.error}>{this.state.compileErrorMessage}</MessageBar>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Translation>
|
||||||
|
{(translate) => {
|
||||||
|
const getTranslation = (key: string): string => {
|
||||||
|
return translate(`${this.smartUiGeneratorClassName}.${key}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ overflowX: "auto" }}>
|
||||||
|
<Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}>
|
||||||
|
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems(translate)} />
|
||||||
|
{this.state.isInitializing ? (
|
||||||
<Spinner
|
<Spinner
|
||||||
size={SpinnerSize.large}
|
size={SpinnerSize.large}
|
||||||
styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }}
|
styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{this.state.refreshResult?.isUpdateInProgress && (
|
||||||
|
<MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}>
|
||||||
|
{getTranslation(this.state.refreshResult.notificationMessage)}
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
|
{this.state.notification && (
|
||||||
|
<MessageBar
|
||||||
|
messageBarType={getMessageBarType(this.state.notification.type)}
|
||||||
|
styles={{ root: { width: 400 } }}
|
||||||
|
onDismiss={() => this.setState({ notification: undefined })}
|
||||||
|
>
|
||||||
|
{this.getNotificationMessageTranslation(getTranslation, this.state.notification.message)}
|
||||||
|
</MessageBar>
|
||||||
|
)}
|
||||||
|
<SmartUiComponent
|
||||||
|
disabled={this.state.refreshResult?.isUpdateInProgress}
|
||||||
|
descriptor={this.state.root as SmartUiDescriptor}
|
||||||
|
currentValues={this.state.currentValues}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onError={this.onError}
|
||||||
|
getTranslation={getTranslation}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Translation>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import * as ko from "knockout";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
||||||
import Explorer from "../Explorer/Explorer";
|
import Explorer from "../Explorer/Explorer";
|
||||||
import { SelfServeDescriptor, SelfServeComponent } from "./SelfServeComponent";
|
import { SelfServeComponent } from "./SelfServeComponent";
|
||||||
|
import { SelfServeDescriptor } from "./SelfServeTypes";
|
||||||
import { SelfServeType } from "./SelfServeUtils";
|
import { SelfServeType } from "./SelfServeUtils";
|
||||||
|
|
||||||
export class SelfServeComponentAdapter implements ReactAdapter {
|
export class SelfServeComponentAdapter implements ReactAdapter {
|
||||||
@@ -28,6 +29,10 @@ export class SelfServeComponentAdapter implements ReactAdapter {
|
|||||||
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
|
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
|
||||||
return new SelfServeExample.default().toSelfServeDescriptor();
|
return new SelfServeExample.default().toSelfServeDescriptor();
|
||||||
}
|
}
|
||||||
|
case SelfServeType.sqlx: {
|
||||||
|
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
|
||||||
|
return new SqlX.default().toSelfServeDescriptor();
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
130
src/SelfServe/SelfServeTypes.ts
Normal file
130
src/SelfServe/SelfServeTypes.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
interface BaseInput {
|
||||||
|
dataFieldName: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
type: InputTypeValue;
|
||||||
|
labelTKey?: (() => Promise<string>) | string;
|
||||||
|
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>;
|
||||||
|
placeholderTKey?: (() => Promise<string>) | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NumberInput extends BaseInput {
|
||||||
|
min: (() => Promise<number>) | number;
|
||||||
|
max: (() => Promise<number>) | number;
|
||||||
|
step: (() => Promise<number>) | number;
|
||||||
|
defaultValue?: number;
|
||||||
|
uiType: NumberUiType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BooleanInput extends BaseInput {
|
||||||
|
trueLabelTKey: (() => Promise<string>) | string;
|
||||||
|
falseLabelTKey: (() => Promise<string>) | string;
|
||||||
|
defaultValue?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StringInput extends BaseInput {
|
||||||
|
defaultValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChoiceInput extends BaseInput {
|
||||||
|
choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
|
||||||
|
defaultKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DescriptionDisplay extends BaseInput {
|
||||||
|
description: (() => Promise<Description>) | Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Node {
|
||||||
|
id: string;
|
||||||
|
info?: (() => Promise<Info>) | Info;
|
||||||
|
input?: AnyDisplay;
|
||||||
|
children?: Node[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelfServeDescriptor {
|
||||||
|
root: Node;
|
||||||
|
initialize?: () => Promise<Map<string, SmartUiInput>>;
|
||||||
|
onSave?: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>;
|
||||||
|
inputNames?: string[];
|
||||||
|
onRefresh?: () => Promise<RefreshResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
|
||||||
|
|
||||||
|
export abstract class SelfServeBaseClass {
|
||||||
|
public abstract initialize: () => Promise<Map<string, SmartUiInput>>;
|
||||||
|
public abstract onSave: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>;
|
||||||
|
public abstract onRefresh: () => Promise<RefreshResult>;
|
||||||
|
|
||||||
|
public toSelfServeDescriptor(): SelfServeDescriptor {
|
||||||
|
const className = this.constructor.name;
|
||||||
|
const selfServeDescriptor = Reflect.getMetadata(className, this) as SelfServeDescriptor;
|
||||||
|
|
||||||
|
if (!this.initialize) {
|
||||||
|
throw new Error(`initialize() was not declared for the class '${className}'`);
|
||||||
|
}
|
||||||
|
if (!this.onSave) {
|
||||||
|
throw new Error(`onSave() was not declared for the class '${className}'`);
|
||||||
|
}
|
||||||
|
if (!this.onRefresh) {
|
||||||
|
throw new Error(`onRefresh() was not declared for the class '${className}'`);
|
||||||
|
}
|
||||||
|
if (!selfServeDescriptor?.root) {
|
||||||
|
throw new Error(`@SmartUi decorator was not declared for the class '${className}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
selfServeDescriptor.initialize = this.initialize;
|
||||||
|
selfServeDescriptor.onSave = this.onSave;
|
||||||
|
selfServeDescriptor.onRefresh = this.onRefresh;
|
||||||
|
return selfServeDescriptor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InputTypeValue = "number" | "string" | "boolean" | "object";
|
||||||
|
|
||||||
|
export enum NumberUiType {
|
||||||
|
Spinner = "Spinner",
|
||||||
|
Slider = "Slider",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChoiceItem = { label: string; key: string };
|
||||||
|
|
||||||
|
export type InputType = number | string | boolean | ChoiceItem;
|
||||||
|
|
||||||
|
export interface Info {
|
||||||
|
messageTKey: string;
|
||||||
|
link?: {
|
||||||
|
href: string;
|
||||||
|
textTKey: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Description {
|
||||||
|
textTKey: string;
|
||||||
|
link?: {
|
||||||
|
href: string;
|
||||||
|
textTKey: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SmartUiInput {
|
||||||
|
value: InputType;
|
||||||
|
hidden?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SelfServeNotificationType {
|
||||||
|
info = "info",
|
||||||
|
warning = "warning",
|
||||||
|
error = "error",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelfServeNotification {
|
||||||
|
message: string;
|
||||||
|
type: SelfServeNotificationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RefreshResult {
|
||||||
|
isUpdateInProgress: boolean;
|
||||||
|
notificationMessage: string;
|
||||||
|
}
|
||||||
@@ -1,40 +1,39 @@
|
|||||||
import {
|
import { NumberUiType, RefreshResult, SelfServeBaseClass, SelfServeNotification, SmartUiInput } from "./SelfServeTypes";
|
||||||
CommonInputTypes,
|
import { DecoratorProperties, mapToSmartUiDescriptor, updateContextWithDecorator } from "./SelfServeUtils";
|
||||||
mapToSmartUiDescriptor,
|
|
||||||
SelfServeBaseClass,
|
|
||||||
updateContextWithDecorator,
|
|
||||||
} from "./SelfServeUtils";
|
|
||||||
import { InputType, UiType } from "./../Explorer/Controls/SmartUi/SmartUiComponent";
|
|
||||||
|
|
||||||
describe("SelfServeUtils", () => {
|
describe("SelfServeUtils", () => {
|
||||||
it("initialize should be declared for self serve classes", () => {
|
it("initialize should be declared for self serve classes", () => {
|
||||||
class Test extends SelfServeBaseClass {
|
class Test extends SelfServeBaseClass {
|
||||||
public onSubmit = async (): Promise<void> => {
|
public initialize: () => Promise<Map<string, SmartUiInput>>;
|
||||||
return;
|
public onSave: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>;
|
||||||
};
|
public onRefresh: () => Promise<RefreshResult>;
|
||||||
public initialize: () => Promise<Map<string, InputType>>;
|
|
||||||
}
|
}
|
||||||
expect(() => new Test().toSelfServeDescriptor()).toThrow("initialize() was not declared for the class 'Test'");
|
expect(() => new Test().toSelfServeDescriptor()).toThrow("initialize() was not declared for the class 'Test'");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("onSubmit should be declared for self serve classes", () => {
|
it("onSave should be declared for self serve classes", () => {
|
||||||
class Test extends SelfServeBaseClass {
|
class Test extends SelfServeBaseClass {
|
||||||
public onSubmit: () => Promise<void>;
|
public initialize = jest.fn();
|
||||||
public initialize = async (): Promise<Map<string, InputType>> => {
|
public onSave: () => Promise<SelfServeNotification>;
|
||||||
return undefined;
|
public onRefresh: () => Promise<RefreshResult>;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
expect(() => new Test().toSelfServeDescriptor()).toThrow("onSubmit() was not declared for the class 'Test'");
|
expect(() => new Test().toSelfServeDescriptor()).toThrow("onSave() was not declared for the class 'Test'");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("onRefresh should be declared for self serve classes", () => {
|
||||||
|
class Test extends SelfServeBaseClass {
|
||||||
|
public initialize = jest.fn();
|
||||||
|
public onSave = jest.fn();
|
||||||
|
public onRefresh: () => Promise<RefreshResult>;
|
||||||
|
}
|
||||||
|
expect(() => new Test().toSelfServeDescriptor()).toThrow("onRefresh() was not declared for the class 'Test'");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("@SmartUi decorator must be present for self serve classes", () => {
|
it("@SmartUi decorator must be present for self serve classes", () => {
|
||||||
class Test extends SelfServeBaseClass {
|
class Test extends SelfServeBaseClass {
|
||||||
public onSubmit = async (): Promise<void> => {
|
public initialize = jest.fn();
|
||||||
return;
|
public onSave = jest.fn();
|
||||||
};
|
public onRefresh = jest.fn();
|
||||||
public initialize = async (): Promise<Map<string, InputType>> => {
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
expect(() => new Test().toSelfServeDescriptor()).toThrow(
|
expect(() => new Test().toSelfServeDescriptor()).toThrow(
|
||||||
"@SmartUi decorator was not declared for the class 'Test'"
|
"@SmartUi decorator was not declared for the class 'Test'"
|
||||||
@@ -42,7 +41,7 @@ describe("SelfServeUtils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("updateContextWithDecorator", () => {
|
it("updateContextWithDecorator", () => {
|
||||||
const context = new Map<string, CommonInputTypes>();
|
const context = new Map<string, DecoratorProperties>();
|
||||||
updateContextWithDecorator(context, "dbThroughput", "testClass", "max", 1);
|
updateContextWithDecorator(context, "dbThroughput", "testClass", "max", 1);
|
||||||
updateContextWithDecorator(context, "dbThroughput", "testClass", "min", 2);
|
updateContextWithDecorator(context, "dbThroughput", "testClass", "min", 2);
|
||||||
updateContextWithDecorator(context, "collThroughput", "testClass", "max", 5);
|
updateContextWithDecorator(context, "collThroughput", "testClass", "max", 5);
|
||||||
@@ -52,18 +51,18 @@ describe("SelfServeUtils", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("mapToSmartUiDescriptor", () => {
|
it("mapToSmartUiDescriptor", () => {
|
||||||
const context: Map<string, CommonInputTypes> = new Map([
|
const context: Map<string, DecoratorProperties> = new Map([
|
||||||
[
|
[
|
||||||
"dbThroughput",
|
"dbThroughput",
|
||||||
{
|
{
|
||||||
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,
|
||||||
uiType: UiType.Slider,
|
uiType: NumberUiType.Slider,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -72,11 +71,11 @@ 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,
|
||||||
uiType: UiType.Spinner,
|
uiType: NumberUiType.Spinner,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -85,11 +84,11 @@ 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,
|
||||||
uiType: UiType.Spinner,
|
uiType: NumberUiType.Spinner,
|
||||||
errorMessage: "label, truelabel and falselabel are required for boolean input",
|
errorMessage: "label, truelabel and falselabel are required for boolean input",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -99,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",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -109,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",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -120,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",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -130,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" },
|
||||||
@@ -144,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",
|
||||||
@@ -159,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,
|
||||||
@@ -173,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,
|
||||||
@@ -187,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,
|
||||||
@@ -202,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[],
|
||||||
},
|
},
|
||||||
@@ -213,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[],
|
||||||
},
|
},
|
||||||
@@ -225,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[],
|
||||||
@@ -237,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" },
|
||||||
@@ -252,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[],
|
||||||
@@ -271,7 +270,7 @@ describe("SelfServeUtils", () => {
|
|||||||
"invalidRegions",
|
"invalidRegions",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const descriptor = mapToSmartUiDescriptor(context);
|
const descriptor = mapToSmartUiDescriptor("TestClass", context);
|
||||||
expect(descriptor).toEqual(expectedDescriptor);
|
expect(descriptor).toEqual(expectedDescriptor);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
|
import { MessageBarType } from "office-ui-fabric-react";
|
||||||
import "reflect-metadata";
|
import "reflect-metadata";
|
||||||
import { ChoiceItem, Info, InputTypeValue, InputType } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
|
||||||
import {
|
import {
|
||||||
|
Node,
|
||||||
|
AnyDisplay,
|
||||||
BooleanInput,
|
BooleanInput,
|
||||||
ChoiceInput,
|
ChoiceInput,
|
||||||
SelfServeDescriptor,
|
ChoiceItem,
|
||||||
|
Description,
|
||||||
|
DescriptionDisplay,
|
||||||
|
Info,
|
||||||
|
InputType,
|
||||||
|
InputTypeValue,
|
||||||
NumberInput,
|
NumberInput,
|
||||||
|
SelfServeDescriptor,
|
||||||
|
SmartUiInput,
|
||||||
StringInput,
|
StringInput,
|
||||||
Node,
|
SelfServeNotificationType,
|
||||||
AnyInput,
|
} from "./SelfServeTypes";
|
||||||
} from "./SelfServeComponent";
|
|
||||||
|
|
||||||
export enum SelfServeType {
|
export enum SelfServeType {
|
||||||
// No self serve type passed, launch explorer
|
// No self serve type passed, launch explorer
|
||||||
@@ -17,82 +25,61 @@ export enum SelfServeType {
|
|||||||
invalid = "invalid",
|
invalid = "invalid",
|
||||||
// Add your self serve types here
|
// Add your self serve types here
|
||||||
example = "example",
|
example = "example",
|
||||||
|
sqlx = "sqlx",
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class SelfServeBaseClass {
|
export interface DecoratorProperties {
|
||||||
public abstract onSubmit: (currentValues: Map<string, InputType>) => Promise<void>;
|
|
||||||
public abstract initialize: () => Promise<Map<string, InputType>>;
|
|
||||||
|
|
||||||
public toSelfServeDescriptor(): SelfServeDescriptor {
|
|
||||||
const className = this.constructor.name;
|
|
||||||
const smartUiDescriptor = Reflect.getMetadata(className, this) as SelfServeDescriptor;
|
|
||||||
|
|
||||||
if (!this.initialize) {
|
|
||||||
throw new Error(`initialize() was not declared for the class '${className}'`);
|
|
||||||
}
|
|
||||||
if (!this.onSubmit) {
|
|
||||||
throw new Error(`onSubmit() was not declared for the class '${className}'`);
|
|
||||||
}
|
|
||||||
if (!smartUiDescriptor?.root) {
|
|
||||||
throw new Error(`@SmartUi decorator was not declared for the class '${className}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
smartUiDescriptor.initialize = this.initialize;
|
|
||||||
smartUiDescriptor.onSubmit = this.onSubmit;
|
|
||||||
return smartUiDescriptor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CommonInputTypes {
|
|
||||||
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;
|
||||||
onChange?: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>;
|
description?: (() => Promise<Description>) | Description;
|
||||||
onSubmit?: (currentValues: Map<string, InputType>) => Promise<void>;
|
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>;
|
||||||
initialize?: () => Promise<Map<string, InputType>>;
|
onSave?: (currentValues: Map<string, SmartUiInput>) => Promise<void>;
|
||||||
|
initialize?: () => Promise<Map<string, SmartUiInput>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const setValue = <T extends keyof CommonInputTypes, K extends CommonInputTypes[T]>(
|
const setValue = <T extends keyof DecoratorProperties, K extends DecoratorProperties[T]>(
|
||||||
name: T,
|
name: T,
|
||||||
value: K,
|
value: K,
|
||||||
fieldObject: CommonInputTypes
|
fieldObject: DecoratorProperties
|
||||||
): void => {
|
): void => {
|
||||||
fieldObject[name] = value;
|
fieldObject[name] = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getValue = <T extends keyof CommonInputTypes>(name: T, fieldObject: CommonInputTypes): unknown => {
|
const getValue = <T extends keyof DecoratorProperties>(name: T, fieldObject: DecoratorProperties): unknown => {
|
||||||
return fieldObject[name];
|
return fieldObject[name];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const addPropertyToMap = <T extends keyof CommonInputTypes, K extends CommonInputTypes[T]>(
|
export const addPropertyToMap = <T extends keyof DecoratorProperties, K extends DecoratorProperties[T]>(
|
||||||
target: unknown,
|
target: unknown,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
className: string,
|
className: string,
|
||||||
descriptorName: keyof CommonInputTypes,
|
descriptorName: keyof DecoratorProperties,
|
||||||
descriptorValue: K
|
descriptorValue: K
|
||||||
): void => {
|
): void => {
|
||||||
const context =
|
const context =
|
||||||
(Reflect.getMetadata(className, target) as Map<string, CommonInputTypes>) ?? new Map<string, CommonInputTypes>();
|
(Reflect.getMetadata(className, target) as Map<string, DecoratorProperties>) ??
|
||||||
|
new Map<string, DecoratorProperties>();
|
||||||
updateContextWithDecorator(context, propertyName, className, descriptorName, descriptorValue);
|
updateContextWithDecorator(context, propertyName, className, descriptorName, descriptorValue);
|
||||||
Reflect.defineMetadata(className, context, target);
|
Reflect.defineMetadata(className, context, target);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateContextWithDecorator = <T extends keyof CommonInputTypes, K extends CommonInputTypes[T]>(
|
export const updateContextWithDecorator = <T extends keyof DecoratorProperties, K extends DecoratorProperties[T]>(
|
||||||
context: Map<string, CommonInputTypes>,
|
context: Map<string, DecoratorProperties>,
|
||||||
propertyName: string,
|
propertyName: string,
|
||||||
className: string,
|
className: string,
|
||||||
descriptorName: keyof CommonInputTypes,
|
descriptorName: keyof DecoratorProperties,
|
||||||
descriptorValue: K
|
descriptorValue: K
|
||||||
): void => {
|
): void => {
|
||||||
if (!(context instanceof Map)) {
|
if (!(context instanceof Map)) {
|
||||||
@@ -112,19 +99,22 @@ export const updateContextWithDecorator = <T extends keyof CommonInputTypes, K e
|
|||||||
};
|
};
|
||||||
|
|
||||||
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, CommonInputTypes>;
|
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, CommonInputTypes>): 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: [],
|
||||||
},
|
},
|
||||||
@@ -140,7 +130,7 @@ export const mapToSmartUiDescriptor = (context: Map<string, CommonInputTypes>):
|
|||||||
};
|
};
|
||||||
|
|
||||||
const addToDescriptor = (
|
const addToDescriptor = (
|
||||||
context: Map<string, CommonInputTypes>,
|
context: Map<string, DecoratorProperties>,
|
||||||
root: Node,
|
root: Node,
|
||||||
key: string,
|
key: string,
|
||||||
inputNames: string[]
|
inputNames: string[]
|
||||||
@@ -157,27 +147,41 @@ const addToDescriptor = (
|
|||||||
root.children.push(element);
|
root.children.push(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInput = (value: CommonInputTypes): AnyInput => {
|
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;
|
||||||
case "string":
|
case "string":
|
||||||
if (!value.label) {
|
if (value.description) {
|
||||||
|
return value as DescriptionDisplay;
|
||||||
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMessageBarType = (type: SelfServeNotificationType): MessageBarType => {
|
||||||
|
switch (type) {
|
||||||
|
case SelfServeNotificationType.info:
|
||||||
|
return MessageBarType.info;
|
||||||
|
case SelfServeNotificationType.warning:
|
||||||
|
return MessageBarType.warning;
|
||||||
|
case SelfServeNotificationType.error:
|
||||||
|
return MessageBarType.error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
33
src/SelfServe/SqlX/SqlX.rp.ts
Normal file
33
src/SelfServe/SqlX/SqlX.rp.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { RefreshResult } from "../SelfServeTypes";
|
||||||
|
|
||||||
|
export interface DedicatedGatewayResponse {
|
||||||
|
sku: string;
|
||||||
|
instances: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRegionSpecificMinInstances = async (): Promise<number> => {
|
||||||
|
// TODO: write RP call to get min number of instances needed for this region
|
||||||
|
throw new Error("getRegionSpecificMinInstances not implemented");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRegionSpecificMaxInstances = async (): Promise<number> => {
|
||||||
|
// TODO: write RP call to get max number of instances needed for this region
|
||||||
|
throw new Error("getRegionSpecificMaxInstances not implemented");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateDedicatedGatewayProvisioning = async (sku: string, instances: number): Promise<void> => {
|
||||||
|
// TODO: write RP call to update dedicated gateway provisioning
|
||||||
|
throw new Error(
|
||||||
|
`updateDedicatedGatewayProvisioning not implemented. Parameters- sku: ${sku}, instances:${instances}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initializeDedicatedGatewayProvisioning = async (): Promise<DedicatedGatewayResponse> => {
|
||||||
|
// TODO: write RP call to initialize UI for dedicated gateway provisioning
|
||||||
|
throw new Error("initializeDedicatedGatewayProvisioning not implemented");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refreshDedicatedGatewayProvisioning = async (): Promise<RefreshResult> => {
|
||||||
|
// TODO: write RP call to check if dedicated gateway update has gone through
|
||||||
|
throw new Error("refreshDedicatedGatewayProvisioning not implemented");
|
||||||
|
};
|
||||||
97
src/SelfServe/SqlX/SqlX.tsx
Normal file
97
src/SelfServe/SqlX/SqlX.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { IsDisplayable, OnChange, Values } from "../Decorators";
|
||||||
|
import {
|
||||||
|
ChoiceItem,
|
||||||
|
InputType,
|
||||||
|
NumberUiType,
|
||||||
|
RefreshResult,
|
||||||
|
SelfServeBaseClass,
|
||||||
|
SelfServeNotification,
|
||||||
|
SmartUiInput,
|
||||||
|
} from "../SelfServeTypes";
|
||||||
|
import { refreshDedicatedGatewayProvisioning } from "./SqlX.rp";
|
||||||
|
|
||||||
|
const onEnableDedicatedGatewayChange = (
|
||||||
|
currentState: Map<string, SmartUiInput>,
|
||||||
|
newValue: InputType
|
||||||
|
): Map<string, SmartUiInput> => {
|
||||||
|
const sku = currentState.get("sku");
|
||||||
|
const instances = currentState.get("instances");
|
||||||
|
const isSkuHidden = newValue === undefined || !(newValue as boolean);
|
||||||
|
currentState.set("enableDedicatedGateway", { value: newValue });
|
||||||
|
currentState.set("sku", { value: sku.value, hidden: isSkuHidden });
|
||||||
|
currentState.set("instances", { value: instances.value, hidden: isSkuHidden });
|
||||||
|
return currentState;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSkus = async (): Promise<ChoiceItem[]> => {
|
||||||
|
// TODO: get SKUs from getRegionSpecificSkus() RP call and return array of {label:..., key:...}.
|
||||||
|
throw new Error("getSkus not implemented.");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInstancesMin = async (): Promise<number> => {
|
||||||
|
// TODO: get SKUs from getRegionSpecificSkus() RP call and return array of {label:..., key:...}.
|
||||||
|
throw new Error("getInstancesMin not implemented.");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInstancesMax = async (): Promise<number> => {
|
||||||
|
// TODO: get SKUs from getRegionSpecificSkus() RP call and return array of {label:..., key:...}.
|
||||||
|
throw new Error("getInstancesMax not implemented.");
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = (currentValues: Map<string, SmartUiInput>): void => {
|
||||||
|
// TODO: add cusom validation logic to be called before Saving the data.
|
||||||
|
throw new Error(`validate not implemented. No. of properties to validate: ${currentValues.size}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
@IsDisplayable()
|
||||||
|
export default class SqlX extends SelfServeBaseClass {
|
||||||
|
public onRefresh = async (): Promise<RefreshResult> => {
|
||||||
|
return refreshDedicatedGatewayProvisioning();
|
||||||
|
};
|
||||||
|
|
||||||
|
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<SelfServeNotification> => {
|
||||||
|
validate(currentValues);
|
||||||
|
// TODO: add pre processing logic before calling the updateDedicatedGatewayProvisioning() RP call.
|
||||||
|
throw new Error(`onSave not implemented. No. of properties to save: ${currentValues.size}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
public initialize = async (): Promise<Map<string, SmartUiInput>> => {
|
||||||
|
// TODO: get initialization data from initializeDedicatedGatewayProvisioning() RP call.
|
||||||
|
throw new Error("onSave not implemented");
|
||||||
|
};
|
||||||
|
|
||||||
|
@Values({
|
||||||
|
description: {
|
||||||
|
textTKey: "Provisioning dedicated gateways for SqlX accounts.",
|
||||||
|
link: {
|
||||||
|
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
||||||
|
textTKey: "Learn more about dedicated gateway.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@OnChange(onEnableDedicatedGatewayChange)
|
||||||
|
@Values({
|
||||||
|
labelTKey: "Dedicated Gateway",
|
||||||
|
trueLabelTKey: "Enable",
|
||||||
|
falseLabelTKey: "Disable",
|
||||||
|
})
|
||||||
|
enableDedicatedGateway: boolean;
|
||||||
|
|
||||||
|
@Values({
|
||||||
|
labelTKey: "SKUs",
|
||||||
|
choices: getSkus,
|
||||||
|
placeholderTKey: "Select SKUs",
|
||||||
|
})
|
||||||
|
sku: ChoiceItem;
|
||||||
|
|
||||||
|
@Values({
|
||||||
|
labelTKey: "Number of instances",
|
||||||
|
min: getInstancesMin,
|
||||||
|
max: getInstancesMax,
|
||||||
|
step: 1,
|
||||||
|
uiType: NumberUiType.Spinner,
|
||||||
|
})
|
||||||
|
instances: number;
|
||||||
|
}
|
||||||
@@ -1,168 +1,33 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`SelfServeComponent should render 1`] = `
|
exports[`SelfServeComponent message bar and spinner snapshots 1`] = `
|
||||||
<div
|
<Translation>
|
||||||
style={
|
<Component />
|
||||||
Object {
|
</Translation>
|
||||||
"overflowX": "auto",
|
`;
|
||||||
}
|
|
||||||
}
|
exports[`SelfServeComponent message bar and spinner snapshots 2`] = `
|
||||||
>
|
<Translation>
|
||||||
<Stack
|
<Component />
|
||||||
styles={
|
</Translation>
|
||||||
Object {
|
`;
|
||||||
"root": Object {
|
|
||||||
"padding": 10,
|
exports[`SelfServeComponent message bar and spinner snapshots 3`] = `
|
||||||
"width": 400,
|
<Translation>
|
||||||
},
|
<Component />
|
||||||
}
|
</Translation>
|
||||||
}
|
`;
|
||||||
tokens={
|
|
||||||
Object {
|
exports[`SelfServeComponent message bar and spinner snapshots 4`] = `
|
||||||
"childrenGap": 20,
|
<StyledMessageBarBase
|
||||||
}
|
messageBarType={1}
|
||||||
}
|
>
|
||||||
>
|
sample error message
|
||||||
<SmartUiComponent
|
</StyledMessageBarBase>
|
||||||
currentValues={
|
`;
|
||||||
Map {
|
|
||||||
"throughput" => "450",
|
exports[`SelfServeComponent should render and honor save, discard, refresh actions 1`] = `
|
||||||
"analyticalStore" => "false",
|
<Translation>
|
||||||
"database" => "db2",
|
<Component />
|
||||||
}
|
</Translation>
|
||||||
}
|
|
||||||
descriptor={
|
|
||||||
Object {
|
|
||||||
"initialize": [MockFunction] {
|
|
||||||
"calls": Array [
|
|
||||||
Array [],
|
|
||||||
],
|
|
||||||
"results": Array [
|
|
||||||
Object {
|
|
||||||
"type": "return",
|
|
||||||
"value": Promise {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"inputNames": Array [
|
|
||||||
"throughput",
|
|
||||||
"containerId",
|
|
||||||
"analyticalStore",
|
|
||||||
"database",
|
|
||||||
],
|
|
||||||
"onSubmit": [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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onInputChange={[Function]}
|
|
||||||
/>
|
|
||||||
<Stack
|
|
||||||
horizontal={true}
|
|
||||||
tokens={
|
|
||||||
Object {
|
|
||||||
"childrenGap": 10,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CustomizedPrimaryButton
|
|
||||||
id="submitButton"
|
|
||||||
onClick={[Function]}
|
|
||||||
styles={
|
|
||||||
Object {
|
|
||||||
"root": Object {
|
|
||||||
"width": 100,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
text="submit"
|
|
||||||
/>
|
|
||||||
<CustomizedPrimaryButton
|
|
||||||
id="discardButton"
|
|
||||||
onClick={[Function]}
|
|
||||||
styles={
|
|
||||||
Object {
|
|
||||||
"root": Object {
|
|
||||||
"width": 100,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
text="discard"
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -19,14 +19,13 @@ export function logConsoleMessage(type: ConsoleDataType, message: string, id?: s
|
|||||||
if (!id) {
|
if (!id) {
|
||||||
id = _.uniqueId();
|
id = _.uniqueId();
|
||||||
}
|
}
|
||||||
dataExplorer.logConsoleData({ type: type, date: formattedDate, message: message, id: id });
|
dataExplorer.logConsoleData({ type, date: formattedDate, message, id });
|
||||||
}
|
}
|
||||||
return id || "";
|
return id || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearInProgressMessageWithId(id: string): void {
|
export function clearInProgressMessageWithId(id: string): void {
|
||||||
const dataExplorer = _global.dataExplorer;
|
_global.dataExplorer?.deleteInProgressConsoleDataWithId(id);
|
||||||
dataExplorer && dataExplorer.deleteInProgressConsoleDataWithId(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logConsoleProgress(message: string): () => void {
|
export function logConsoleProgress(message: string): () => void {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer";
|
import { BindingHandlersRegisterer } from "./Bindings/BindingHandlersRegisterer";
|
||||||
import { sendMessage } from "./Common/MessageHandler";
|
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import Explorer from "./Explorer/Explorer";
|
import Explorer from "./Explorer/Explorer";
|
||||||
|
|
||||||
@@ -10,7 +9,6 @@ export const applyExplorerBindings = (explorer: Explorer) => {
|
|||||||
ko.applyBindings(explorer);
|
ko.applyBindings(explorer);
|
||||||
// This message should ideally be sent immediately after explorer has been initialized for optimal data explorer load times.
|
// This message should ideally be sent immediately after explorer has been initialized for optimal data explorer load times.
|
||||||
// TODO: Send another message to describe that the bindings have been applied, and handle message transfers accordingly in the portal
|
// TODO: Send another message to describe that the bindings have been applied, and handle message transfers accordingly in the portal
|
||||||
sendMessage("ready");
|
|
||||||
$("#divExplorer").show();
|
$("#divExplorer").show();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
14
src/hooks/useConfig.ts
Normal file
14
src/hooks/useConfig.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { ConfigContext, initializeConfiguration } from "../ConfigContext";
|
||||||
|
|
||||||
|
// This hook initializes global configuration from a config.json file that is injected at deploy time
|
||||||
|
// This allows the same main Data Explorer build to be exactly the same in all clouds/platforms,
|
||||||
|
// but override some of the configuration as nesssary
|
||||||
|
export function useConfig(): Readonly<ConfigContext> {
|
||||||
|
const [state, setState] = useState<ConfigContext>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initializeConfiguration().then((response) => setState(response));
|
||||||
|
}, []);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
301
src/hooks/useKnockoutExplorer.ts
Normal file
301
src/hooks/useKnockoutExplorer.ts
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { applyExplorerBindings } from "../applyExplorerBindings";
|
||||||
|
import { AuthType } from "../AuthType";
|
||||||
|
import { AccountKind, DefaultAccountExperience, ServerIds } from "../Common/Constants";
|
||||||
|
import { sendMessage } from "../Common/MessageHandler";
|
||||||
|
import { configContext, ConfigContext, Platform } from "../ConfigContext";
|
||||||
|
import { ActionType, DataExplorerAction } from "../Contracts/ActionContracts";
|
||||||
|
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||||
|
import { DataExplorerInputsFrame } from "../Contracts/ViewModels";
|
||||||
|
import Explorer, { ExplorerParams } from "../Explorer/Explorer";
|
||||||
|
import {
|
||||||
|
AAD,
|
||||||
|
ConnectionString,
|
||||||
|
EncryptedToken,
|
||||||
|
HostedExplorerChildFrame,
|
||||||
|
ResourceToken,
|
||||||
|
} from "../HostedExplorerChildFrame";
|
||||||
|
import { emulatorAccount } from "../Platform/Emulator/emulatorAccount";
|
||||||
|
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||||
|
import { parseResourceTokenConnectionString } from "../Platform/Hosted/Helpers/ResourceTokenUtils";
|
||||||
|
import {
|
||||||
|
getDatabaseAccountKindFromExperience,
|
||||||
|
getDatabaseAccountPropertiesFromMetadata,
|
||||||
|
} from "../Platform/Hosted/HostedUtils";
|
||||||
|
import { SelfServeType } from "../SelfServe/SelfServeUtils";
|
||||||
|
import { CollectionCreation } from "../Shared/Constants";
|
||||||
|
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
|
||||||
|
import { updateUserContext } from "../UserContext";
|
||||||
|
import { listKeys } from "../Utils/arm/generatedClients/2020-04-01/databaseAccounts";
|
||||||
|
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
||||||
|
|
||||||
|
// This hook will create a new instance of Explorer.ts and bind it to the DOM
|
||||||
|
// This hook has a LOT of magic, but ideally we can delete it once we have removed KO and switched entirely to React
|
||||||
|
// Pleas tread carefully :)
|
||||||
|
let explorer: Explorer;
|
||||||
|
|
||||||
|
export function useKnockoutExplorer(config: ConfigContext, explorerParams: ExplorerParams): Explorer {
|
||||||
|
explorer = explorer || new Explorer(explorerParams);
|
||||||
|
useEffect(() => {
|
||||||
|
const effect = async () => {
|
||||||
|
if (config) {
|
||||||
|
if (config.platform === Platform.Hosted) {
|
||||||
|
await configureHosted(config);
|
||||||
|
applyExplorerBindings(explorer);
|
||||||
|
} else if (config.platform === Platform.Emulator) {
|
||||||
|
configureEmulator();
|
||||||
|
applyExplorerBindings(explorer);
|
||||||
|
} else if (config.platform === Platform.Portal) {
|
||||||
|
configurePortal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
effect();
|
||||||
|
}, [config]);
|
||||||
|
return explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function configureHosted(config: ConfigContext) {
|
||||||
|
const win = (window as unknown) as HostedExplorerChildFrame;
|
||||||
|
explorer.selfServeType(SelfServeType.none);
|
||||||
|
if (win.hostedConfig.authType === AuthType.EncryptedToken) {
|
||||||
|
configureHostedWithEncryptedToken(win.hostedConfig, config);
|
||||||
|
} else if (win.hostedConfig.authType === AuthType.ResourceToken) {
|
||||||
|
configureHostedWithResourceToken(win.hostedConfig);
|
||||||
|
} else if (win.hostedConfig.authType === AuthType.ConnectionString) {
|
||||||
|
configureHostedWithConnectionString(win.hostedConfig);
|
||||||
|
} else if (win.hostedConfig.authType === AuthType.AAD) {
|
||||||
|
await configureHostedWithAAD(win.hostedConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function configureHostedWithAAD(config: AAD) {
|
||||||
|
window.authType = AuthType.AAD;
|
||||||
|
const account = config.databaseAccount;
|
||||||
|
const accountResourceId = account.id;
|
||||||
|
const subscriptionId = accountResourceId && accountResourceId.split("subscriptions/")[1].split("/")[0];
|
||||||
|
const resourceGroup = accountResourceId && accountResourceId.split("resourceGroups/")[1].split("/")[0];
|
||||||
|
updateUserContext({
|
||||||
|
authorizationToken: `Bearer ${config.authorizationToken}`,
|
||||||
|
databaseAccount: config.databaseAccount,
|
||||||
|
});
|
||||||
|
const keys = await listKeys(subscriptionId, resourceGroup, account.name);
|
||||||
|
explorer.configure({
|
||||||
|
databaseAccount: account,
|
||||||
|
subscriptionId,
|
||||||
|
resourceGroup,
|
||||||
|
masterKey: keys.primaryMasterKey,
|
||||||
|
hasWriteAccess: true,
|
||||||
|
authorizationToken: `Bearer ${config.authorizationToken}`,
|
||||||
|
features: extractFeatures(),
|
||||||
|
csmEndpoint: undefined,
|
||||||
|
dnsSuffix: undefined,
|
||||||
|
serverId: ServerIds.productionPortal,
|
||||||
|
extensionEndpoint: configContext.BACKEND_ENDPOINT,
|
||||||
|
subscriptionType: CollectionCreation.DefaultSubscriptionType,
|
||||||
|
quotaId: undefined,
|
||||||
|
addCollectionDefaultFlight: explorer.flight(),
|
||||||
|
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(),
|
||||||
|
});
|
||||||
|
explorer.isAccountReady(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureHostedWithConnectionString(config: ConnectionString) {
|
||||||
|
// For legacy reasons lots of code expects a connection string login to look and act like an encrypted token login
|
||||||
|
window.authType = AuthType.EncryptedToken;
|
||||||
|
// Impossible to tell if this is a try cosmos sub using an encrypted token
|
||||||
|
explorer.isTryCosmosDBSubscription(false);
|
||||||
|
updateUserContext({
|
||||||
|
accessToken: encodeURIComponent(config.encryptedToken),
|
||||||
|
});
|
||||||
|
|
||||||
|
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
|
||||||
|
config.encryptedTokenMetadata.apiKind
|
||||||
|
);
|
||||||
|
explorer.configure({
|
||||||
|
databaseAccount: {
|
||||||
|
id: "",
|
||||||
|
// id: Main._databaseAccountId,
|
||||||
|
name: config.encryptedTokenMetadata.accountName,
|
||||||
|
kind: getDatabaseAccountKindFromExperience(apiExperience),
|
||||||
|
properties: getDatabaseAccountPropertiesFromMetadata(config.encryptedTokenMetadata),
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
subscriptionId: undefined,
|
||||||
|
resourceGroup: undefined,
|
||||||
|
masterKey: config.masterKey,
|
||||||
|
hasWriteAccess: true,
|
||||||
|
authorizationToken: undefined,
|
||||||
|
features: extractFeatures(),
|
||||||
|
csmEndpoint: undefined,
|
||||||
|
dnsSuffix: undefined,
|
||||||
|
serverId: ServerIds.productionPortal,
|
||||||
|
extensionEndpoint: configContext.BACKEND_ENDPOINT,
|
||||||
|
subscriptionType: CollectionCreation.DefaultSubscriptionType,
|
||||||
|
quotaId: undefined,
|
||||||
|
addCollectionDefaultFlight: explorer.flight(),
|
||||||
|
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(),
|
||||||
|
});
|
||||||
|
explorer.isAccountReady(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureHostedWithResourceToken(config: ResourceToken) {
|
||||||
|
window.authType = AuthType.ResourceToken;
|
||||||
|
// Resource tokens can only be used with SQL API
|
||||||
|
const apiExperience: string = DefaultAccountExperience.DocumentDB;
|
||||||
|
const parsedResourceToken = parseResourceTokenConnectionString(config.resourceToken);
|
||||||
|
updateUserContext({
|
||||||
|
resourceToken: parsedResourceToken.resourceToken,
|
||||||
|
endpoint: parsedResourceToken.accountEndpoint,
|
||||||
|
});
|
||||||
|
explorer.resourceTokenDatabaseId(parsedResourceToken.databaseId);
|
||||||
|
explorer.resourceTokenCollectionId(parsedResourceToken.collectionId);
|
||||||
|
if (parsedResourceToken.partitionKey) {
|
||||||
|
explorer.resourceTokenPartitionKey(parsedResourceToken.partitionKey);
|
||||||
|
}
|
||||||
|
explorer.configure({
|
||||||
|
databaseAccount: {
|
||||||
|
id: "",
|
||||||
|
name: parsedResourceToken.accountEndpoint,
|
||||||
|
kind: AccountKind.GlobalDocumentDB,
|
||||||
|
properties: { documentEndpoint: parsedResourceToken.accountEndpoint },
|
||||||
|
tags: { defaultExperience: apiExperience },
|
||||||
|
},
|
||||||
|
subscriptionId: undefined,
|
||||||
|
resourceGroup: undefined,
|
||||||
|
masterKey: undefined,
|
||||||
|
hasWriteAccess: true,
|
||||||
|
authorizationToken: undefined,
|
||||||
|
features: extractFeatures(),
|
||||||
|
csmEndpoint: undefined,
|
||||||
|
dnsSuffix: undefined,
|
||||||
|
serverId: ServerIds.productionPortal,
|
||||||
|
extensionEndpoint: configContext.BACKEND_ENDPOINT,
|
||||||
|
subscriptionType: CollectionCreation.DefaultSubscriptionType,
|
||||||
|
quotaId: undefined,
|
||||||
|
addCollectionDefaultFlight: explorer.flight(),
|
||||||
|
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(),
|
||||||
|
isAuthWithresourceToken: true,
|
||||||
|
});
|
||||||
|
explorer.isAccountReady(true);
|
||||||
|
explorer.isRefreshingExplorer(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureHostedWithEncryptedToken(config: EncryptedToken, configContext: ConfigContext) {
|
||||||
|
window.authType = AuthType.EncryptedToken;
|
||||||
|
// Impossible to tell if this is a try cosmos sub using an encrypted token
|
||||||
|
explorer.isTryCosmosDBSubscription(false);
|
||||||
|
updateUserContext({
|
||||||
|
accessToken: encodeURIComponent(config.encryptedToken),
|
||||||
|
});
|
||||||
|
|
||||||
|
const apiExperience: string = DefaultExperienceUtility.getDefaultExperienceFromApiKind(
|
||||||
|
config.encryptedTokenMetadata.apiKind
|
||||||
|
);
|
||||||
|
explorer.configure({
|
||||||
|
databaseAccount: {
|
||||||
|
id: "",
|
||||||
|
name: config.encryptedTokenMetadata.accountName,
|
||||||
|
kind: getDatabaseAccountKindFromExperience(apiExperience),
|
||||||
|
properties: getDatabaseAccountPropertiesFromMetadata(config.encryptedTokenMetadata),
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
subscriptionId: undefined,
|
||||||
|
resourceGroup: undefined,
|
||||||
|
masterKey: undefined,
|
||||||
|
hasWriteAccess: true,
|
||||||
|
authorizationToken: undefined,
|
||||||
|
features: extractFeatures(),
|
||||||
|
csmEndpoint: undefined,
|
||||||
|
dnsSuffix: undefined,
|
||||||
|
serverId: ServerIds.productionPortal,
|
||||||
|
extensionEndpoint: configContext.BACKEND_ENDPOINT,
|
||||||
|
subscriptionType: CollectionCreation.DefaultSubscriptionType,
|
||||||
|
quotaId: undefined,
|
||||||
|
addCollectionDefaultFlight: explorer.flight(),
|
||||||
|
isTryCosmosDBSubscription: explorer.isTryCosmosDBSubscription(),
|
||||||
|
});
|
||||||
|
explorer.isAccountReady(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureEmulator() {
|
||||||
|
window.authType = AuthType.MasterKey;
|
||||||
|
explorer.selfServeType(SelfServeType.none);
|
||||||
|
explorer.databaseAccount(emulatorAccount);
|
||||||
|
explorer.isAccountReady(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function configurePortal() {
|
||||||
|
window.authType = AuthType.AAD;
|
||||||
|
// In development mode, try to load the iframe message from session storage.
|
||||||
|
// This allows webpack hot reload to function properly in the portal
|
||||||
|
if (process.env.NODE_ENV === "development" && !window.location.search.includes("disablePortalInitCache")) {
|
||||||
|
const initMessage = sessionStorage.getItem("portalDataExplorerInitMessage");
|
||||||
|
if (initMessage) {
|
||||||
|
const message = JSON.parse(initMessage);
|
||||||
|
console.warn(
|
||||||
|
"Loaded cached portal iframe message from session storage. Do a full page refresh to get a new message"
|
||||||
|
);
|
||||||
|
console.dir(message);
|
||||||
|
explorer.configure(message);
|
||||||
|
applyExplorerBindings(explorer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the Portal, configuration of Explorer happens via iframe message
|
||||||
|
window.addEventListener(
|
||||||
|
"message",
|
||||||
|
(event) => {
|
||||||
|
if (isInvalidParentFrameOrigin(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldProcessMessage(event)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for init message
|
||||||
|
const message: PortalMessage = event.data?.data;
|
||||||
|
const inputs = message?.inputs;
|
||||||
|
if (inputs) {
|
||||||
|
if (
|
||||||
|
configContext.BACKEND_ENDPOINT &&
|
||||||
|
configContext.platform === Platform.Portal &&
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
) {
|
||||||
|
inputs.extensionEndpoint = configContext.PROXY_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
explorer.configure(inputs);
|
||||||
|
applyExplorerBindings(explorer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
sendMessage("ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldProcessMessage(event: MessageEvent): boolean {
|
||||||
|
if (typeof event.data !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (event.data["signature"] !== "pcIframe") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!("data" in event.data)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof event.data["data"] !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PortalMessage {
|
||||||
|
openAction?: DataExplorerAction;
|
||||||
|
actionType?: ActionType;
|
||||||
|
type?: MessageTypes;
|
||||||
|
inputs?: DataExplorerInputsFrame;
|
||||||
|
}
|
||||||
31
src/i18n.ts
Normal file
31
src/i18n.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import i18n from "i18next";
|
||||||
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
|
import { initReactI18next } from "react-i18next";
|
||||||
|
import XHR from "i18next-http-backend";
|
||||||
|
import EnglishTranslations from "./Localization/en/translations.json";
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(XHR)
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
resources: {
|
||||||
|
en: EnglishTranslations,
|
||||||
|
},
|
||||||
|
fallbackLng: "en",
|
||||||
|
detection: { order: ["navigator", "cookie", "localStorage", "sessionStorage", "querystring", "htmlTag"] },
|
||||||
|
debug: process.env.NODE_ENV === "development",
|
||||||
|
ns: ["translations"],
|
||||||
|
defaultNS: "translations",
|
||||||
|
keySeparator: ".",
|
||||||
|
interpolation: {
|
||||||
|
formatSeparator: ",",
|
||||||
|
},
|
||||||
|
react: {
|
||||||
|
wait: true,
|
||||||
|
bindI18n: "languageChanged loaded",
|
||||||
|
bindI18nStore: "added removed",
|
||||||
|
nsMode: "default",
|
||||||
|
useSuspense: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -37,6 +37,7 @@ export const uploadNotebookIfNotExist = async (frame: Frame, notebookName: strin
|
|||||||
|
|
||||||
export const getNotebookNode = async (frame: Frame, uploadNotebookName: string): Promise<ElementHandle<Element>> => {
|
export const getNotebookNode = async (frame: Frame, uploadNotebookName: string): Promise<ElementHandle<Element>> => {
|
||||||
const notebookResourceTree = await frame.waitForSelector(".notebookResourceTree");
|
const notebookResourceTree = await frame.waitForSelector(".notebookResourceTree");
|
||||||
|
await frame.waitFor(RENDER_DELAY);
|
||||||
let currentNotebookNode: ElementHandle<Element>;
|
let currentNotebookNode: ElementHandle<Element>;
|
||||||
|
|
||||||
const treeNodeHeaders = await notebookResourceTree.$$(".treeNodeHeader");
|
const treeNodeHeaders = await notebookResourceTree.$$(".treeNodeHeader");
|
||||||
|
|||||||
@@ -12,10 +12,27 @@ describe("Self Serve", () => {
|
|||||||
frame = await getTestExplorerFrame(
|
frame = await getTestExplorerFrame(
|
||||||
new Map<string, string>([[TestExplorerParams.selfServeType, SelfServeType.example]])
|
new Map<string, string>([[TestExplorerParams.selfServeType, SelfServeType.example]])
|
||||||
);
|
);
|
||||||
await frame.waitForSelector("#regions-dropown-input");
|
|
||||||
await frame.waitForSelector("#enableLogging-radioSwitch-input");
|
// id of the display element is in the format {PROPERTY_NAME}-{DISPLAY_NAME}-{DISPLAY_TYPE}
|
||||||
await frame.waitForSelector("#accountName-textBox-input");
|
await frame.waitForSelector("#description-text-display");
|
||||||
|
|
||||||
|
const regions = await frame.waitForSelector("#regions-dropdown-input");
|
||||||
|
let disabledLoggingToggle = await frame.$$("#enableLogging-toggle-input[disabled]");
|
||||||
|
expect(disabledLoggingToggle).toHaveLength(0);
|
||||||
|
await regions.click();
|
||||||
|
const regionsDropdownElement1 = await frame.waitForSelector("#regions-dropdown-input-list0");
|
||||||
|
await regionsDropdownElement1.click();
|
||||||
|
disabledLoggingToggle = await frame.$$("#enableLogging-toggle-input[disabled]");
|
||||||
|
expect(disabledLoggingToggle).toHaveLength(1);
|
||||||
|
|
||||||
|
await frame.waitForSelector("#accountName-textField-input");
|
||||||
|
|
||||||
|
const enableDbLevelThroughput = await frame.waitForSelector("#enableDbLevelThroughput-toggle-input");
|
||||||
|
const dbThroughput = await frame.$$("#dbThroughput-slider-input");
|
||||||
|
expect(dbThroughput).toHaveLength(0);
|
||||||
|
await enableDbLevelThroughput.click();
|
||||||
await frame.waitForSelector("#dbThroughput-slider-input");
|
await frame.waitForSelector("#dbThroughput-slider-input");
|
||||||
|
|
||||||
await frame.waitForSelector("#collectionThroughput-spinner-input");
|
await frame.waitForSelector("#collectionThroughput-spinner-input");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { MessageTypes } from "../../src/Contracts/ExplorerContracts";
|
|
||||||
import "../../less/hostedexplorer.less";
|
import "../../less/hostedexplorer.less";
|
||||||
import { TestExplorerParams } from "./TestExplorerParams";
|
import { TestExplorerParams } from "./TestExplorerParams";
|
||||||
import { ClientSecretCredential } from "@azure/identity";
|
|
||||||
import { DatabaseAccountsGetResponse } from "@azure/arm-cosmosdb/esm/models";
|
import { DatabaseAccountsGetResponse } from "@azure/arm-cosmosdb/esm/models";
|
||||||
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
|
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
|
||||||
import * as msRest from "@azure/ms-rest-js";
|
import * as msRest from "@azure/ms-rest-js";
|
||||||
@@ -19,26 +17,6 @@ class CustomSigner implements msRest.ServiceClientCredentials {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMessage = (event: MessageEvent): void => {
|
|
||||||
if (event.data.type === MessageTypes.InitTestExplorer) {
|
|
||||||
sendMessageToExplorerFrame(event.data);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const AADLogin = async (
|
|
||||||
notebooksTestRunnerApplicationId: string,
|
|
||||||
notebooksTestRunnerClientId: string,
|
|
||||||
notebooksTestRunnerClientSecret: string
|
|
||||||
): Promise<string> => {
|
|
||||||
const credentials = new ClientSecretCredential(
|
|
||||||
notebooksTestRunnerApplicationId,
|
|
||||||
notebooksTestRunnerClientId,
|
|
||||||
notebooksTestRunnerClientSecret
|
|
||||||
);
|
|
||||||
const token = await credentials.getToken("https://management.core.windows.net/.default");
|
|
||||||
return token.token;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDatabaseAccount = async (
|
const getDatabaseAccount = async (
|
||||||
token: string,
|
token: string,
|
||||||
notebooksAccountSubscriptonId: string,
|
notebooksAccountSubscriptonId: string,
|
||||||
@@ -49,34 +27,8 @@ const getDatabaseAccount = async (
|
|||||||
return client.databaseAccounts.get(notebooksAccountResourceGroup, notebooksAccountName);
|
return client.databaseAccounts.get(notebooksAccountResourceGroup, notebooksAccountName);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendMessageToExplorerFrame = (data: unknown): void => {
|
|
||||||
const explorerFrame = document.getElementById("explorerMenu") as HTMLIFrameElement;
|
|
||||||
|
|
||||||
explorerFrame &&
|
|
||||||
explorerFrame.contentDocument &&
|
|
||||||
explorerFrame.contentDocument.referrer &&
|
|
||||||
explorerFrame.contentWindow.postMessage(
|
|
||||||
{
|
|
||||||
signature: "pcIframe",
|
|
||||||
data: data,
|
|
||||||
},
|
|
||||||
explorerFrame.contentDocument.referrer || window.location.href
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const initTestExplorer = async (): Promise<void> => {
|
const initTestExplorer = async (): Promise<void> => {
|
||||||
window.addEventListener("message", handleMessage, false);
|
|
||||||
|
|
||||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||||
const notebooksTestRunnerTenantId = decodeURIComponent(
|
|
||||||
urlSearchParams.get(TestExplorerParams.notebooksTestRunnerTenantId)
|
|
||||||
);
|
|
||||||
const notebooksTestRunnerClientId = decodeURIComponent(
|
|
||||||
urlSearchParams.get(TestExplorerParams.notebooksTestRunnerClientId)
|
|
||||||
);
|
|
||||||
const notebooksTestRunnerClientSecret = decodeURIComponent(
|
|
||||||
urlSearchParams.get(TestExplorerParams.notebooksTestRunnerClientSecret)
|
|
||||||
);
|
|
||||||
const portalRunnerDatabaseAccount = decodeURIComponent(
|
const portalRunnerDatabaseAccount = decodeURIComponent(
|
||||||
urlSearchParams.get(TestExplorerParams.portalRunnerDatabaseAccount)
|
urlSearchParams.get(TestExplorerParams.portalRunnerDatabaseAccount)
|
||||||
);
|
);
|
||||||
@@ -89,11 +41,7 @@ const initTestExplorer = async (): Promise<void> => {
|
|||||||
);
|
);
|
||||||
const selfServeType = urlSearchParams.get(TestExplorerParams.selfServeType);
|
const selfServeType = urlSearchParams.get(TestExplorerParams.selfServeType);
|
||||||
|
|
||||||
const token = await AADLogin(
|
const token = decodeURIComponent(urlSearchParams.get(TestExplorerParams.token));
|
||||||
notebooksTestRunnerTenantId,
|
|
||||||
notebooksTestRunnerClientId,
|
|
||||||
notebooksTestRunnerClientSecret
|
|
||||||
);
|
|
||||||
const databaseAccount = await getDatabaseAccount(
|
const databaseAccount = await getDatabaseAccount(
|
||||||
token,
|
token,
|
||||||
portalRunnerSubscripton,
|
portalRunnerSubscripton,
|
||||||
@@ -102,7 +50,6 @@ const initTestExplorer = async (): Promise<void> => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const initTestExplorerContent = {
|
const initTestExplorerContent = {
|
||||||
type: MessageTypes.InitTestExplorer,
|
|
||||||
inputs: {
|
inputs: {
|
||||||
databaseAccount: databaseAccount,
|
databaseAccount: databaseAccount,
|
||||||
subscriptionId: portalRunnerSubscripton,
|
subscriptionId: portalRunnerSubscripton,
|
||||||
@@ -130,11 +77,35 @@ const initTestExplorer = async (): Promise<void> => {
|
|||||||
},
|
},
|
||||||
// add UI test only when feature is not dependent on flights anymore
|
// add UI test only when feature is not dependent on flights anymore
|
||||||
flights: [],
|
flights: [],
|
||||||
selfServeType: selfServeType,
|
selfServeType,
|
||||||
} as ViewModels.DataExplorerInputsFrame,
|
} as ViewModels.DataExplorerInputsFrame,
|
||||||
};
|
};
|
||||||
|
|
||||||
window.postMessage(initTestExplorerContent, window.location.href);
|
const iframe = document.createElement("iframe");
|
||||||
|
window.addEventListener(
|
||||||
|
"message",
|
||||||
|
(event) => {
|
||||||
|
// After we have received the "ready" message from the child iframe we can post configuration
|
||||||
|
// This simulates the same action that happens in the portal
|
||||||
|
console.dir(event.data);
|
||||||
|
if (event.data?.data === "ready") {
|
||||||
|
iframe.contentWindow.postMessage(
|
||||||
|
{
|
||||||
|
signature: "pcIframe",
|
||||||
|
data: initTestExplorerContent,
|
||||||
|
},
|
||||||
|
iframe.contentDocument.referrer || window.location.href
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
iframe.id = "explorerMenu";
|
||||||
|
iframe.name = "explorer";
|
||||||
|
iframe.classList.add("iframe");
|
||||||
|
iframe.title = "explorer";
|
||||||
|
iframe.src = "explorer.html?platform=Portal&disablePortalInitCache";
|
||||||
|
document.body.appendChild(iframe);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("load", initTestExplorer);
|
initTestExplorer();
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
export enum TestExplorerParams {
|
export enum TestExplorerParams {
|
||||||
notebooksTestRunnerTenantId = "notebooksTestRunnerTenantId",
|
|
||||||
notebooksTestRunnerClientId = "notebooksTestRunnerClientId",
|
|
||||||
notebooksTestRunnerClientSecret = "notebooksTestRunnerClientSecret",
|
|
||||||
portalRunnerDatabaseAccount = "portalRunnerDatabaseAccount",
|
portalRunnerDatabaseAccount = "portalRunnerDatabaseAccount",
|
||||||
portalRunnerDatabaseAccountKey = "portalRunnerDatabaseAccountKey",
|
portalRunnerDatabaseAccountKey = "portalRunnerDatabaseAccountKey",
|
||||||
portalRunnerSubscripton = "portalRunnerSubscripton",
|
portalRunnerSubscripton = "portalRunnerSubscripton",
|
||||||
portalRunnerResourceGroup = "portalRunnerResourceGroup",
|
portalRunnerResourceGroup = "portalRunnerResourceGroup",
|
||||||
selfServeType = "selfServeType",
|
selfServeType = "selfServeType",
|
||||||
|
token = "token",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Frame } from "puppeteer";
|
import { Frame } from "puppeteer";
|
||||||
import { TestExplorerParams } from "./TestExplorerParams";
|
import { TestExplorerParams } from "./TestExplorerParams";
|
||||||
|
import { ClientSecretCredential } from "@azure/identity";
|
||||||
|
|
||||||
let testExplorerFrame: Frame;
|
let testExplorerFrame: Frame;
|
||||||
export const getTestExplorerFrame = async (params?: Map<string, string>): Promise<Frame> => {
|
export const getTestExplorerFrame = async (params?: Map<string, string>): Promise<Frame> => {
|
||||||
@@ -15,19 +16,15 @@ export const getTestExplorerFrame = async (params?: Map<string, string>): Promis
|
|||||||
const portalRunnerSubscripton = process.env.PORTAL_RUNNER_SUBSCRIPTION;
|
const portalRunnerSubscripton = process.env.PORTAL_RUNNER_SUBSCRIPTION;
|
||||||
const portalRunnerResourceGroup = process.env.PORTAL_RUNNER_RESOURCE_GROUP;
|
const portalRunnerResourceGroup = process.env.PORTAL_RUNNER_RESOURCE_GROUP;
|
||||||
|
|
||||||
|
const credentials = new ClientSecretCredential(
|
||||||
|
notebooksTestRunnerTenantId,
|
||||||
|
notebooksTestRunnerClientId,
|
||||||
|
notebooksTestRunnerClientSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
const { token } = await credentials.getToken("https://management.core.windows.net/.default");
|
||||||
|
|
||||||
const testExplorerUrl = new URL("testExplorer.html", "https://localhost:1234");
|
const testExplorerUrl = new URL("testExplorer.html", "https://localhost:1234");
|
||||||
testExplorerUrl.searchParams.append(
|
|
||||||
TestExplorerParams.notebooksTestRunnerTenantId,
|
|
||||||
encodeURI(notebooksTestRunnerTenantId)
|
|
||||||
);
|
|
||||||
testExplorerUrl.searchParams.append(
|
|
||||||
TestExplorerParams.notebooksTestRunnerClientId,
|
|
||||||
encodeURI(notebooksTestRunnerClientId)
|
|
||||||
);
|
|
||||||
testExplorerUrl.searchParams.append(
|
|
||||||
TestExplorerParams.notebooksTestRunnerClientSecret,
|
|
||||||
encodeURI(notebooksTestRunnerClientSecret)
|
|
||||||
);
|
|
||||||
testExplorerUrl.searchParams.append(
|
testExplorerUrl.searchParams.append(
|
||||||
TestExplorerParams.portalRunnerDatabaseAccount,
|
TestExplorerParams.portalRunnerDatabaseAccount,
|
||||||
encodeURI(portalRunnerDatabaseAccount)
|
encodeURI(portalRunnerDatabaseAccount)
|
||||||
@@ -41,6 +38,7 @@ export const getTestExplorerFrame = async (params?: Map<string, string>): Promis
|
|||||||
TestExplorerParams.portalRunnerResourceGroup,
|
TestExplorerParams.portalRunnerResourceGroup,
|
||||||
encodeURI(portalRunnerResourceGroup)
|
encodeURI(portalRunnerResourceGroup)
|
||||||
);
|
);
|
||||||
|
testExplorerUrl.searchParams.append(TestExplorerParams.token, encodeURI(token));
|
||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
for (const key of params.keys()) {
|
for (const key of params.keys()) {
|
||||||
|
|||||||
@@ -6,13 +6,5 @@
|
|||||||
<link rel="shortcut icon" href="images/CosmosDB_rgb_ui_lighttheme.ico" type="image/x-icon" />
|
<link rel="shortcut icon" href="images/CosmosDB_rgb_ui_lighttheme.ico" type="image/x-icon" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body></body>
|
||||||
<iframe
|
|
||||||
id="explorerMenu"
|
|
||||||
name="explorer"
|
|
||||||
class="iframe"
|
|
||||||
title="explorer"
|
|
||||||
src="explorer.html?v=1.0.1&platform=Portal"
|
|
||||||
></iframe>
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
51
utils/cleanupDBs.js
Normal file
51
utils/cleanupDBs.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
const { CosmosClient } = require("@azure/cosmos");
|
||||||
|
|
||||||
|
// TODO: Add support for other API connection strings
|
||||||
|
const mongoRegex = RegExp("mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com");
|
||||||
|
|
||||||
|
const connectionString = process.env.PORTAL_RUNNER_CONNECTION_STRING;
|
||||||
|
|
||||||
|
async function cleanup() {
|
||||||
|
if (!connectionString) {
|
||||||
|
throw new Error("Connection string not provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
let client;
|
||||||
|
switch (true) {
|
||||||
|
case connectionString.includes("mongodb://"): {
|
||||||
|
const [, key, accountName] = connectionString.match(mongoRegex);
|
||||||
|
client = new CosmosClient({
|
||||||
|
key,
|
||||||
|
endpoint: `https://${accountName}.documents.azure.com:443/`,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// TODO: Add support for other API connection strings
|
||||||
|
default:
|
||||||
|
client = new CosmosClient(connectionString);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.databases.readAll().fetchAll();
|
||||||
|
return Promise.all(
|
||||||
|
response.resources.map(async (db) => {
|
||||||
|
const dbTimestamp = new Date(db._ts * 1000);
|
||||||
|
const twentyMinutesAgo = new Date(Date.now() - 1000 * 60 * 20);
|
||||||
|
if (dbTimestamp < twentyMinutesAgo) {
|
||||||
|
await client.database(db.id).delete();
|
||||||
|
console.log(`DELETED: ${db.id} | Timestamp: ${dbTimestamp}`);
|
||||||
|
} else {
|
||||||
|
console.log(`SKIPPED: ${db.id} | Timestamp: ${dbTimestamp}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup()
|
||||||
|
.then(() => {
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
34
utils/codeMetrics.js
Normal file
34
utils/codeMetrics.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
const fs = require("fs");
|
||||||
|
const fg = require("fast-glob");
|
||||||
|
const appInsights = require("applicationinsights");
|
||||||
|
appInsights.setup(process.env.CODE_METRICS_APP_ID).start();
|
||||||
|
|
||||||
|
const client = appInsights.defaultClient;
|
||||||
|
const htmlFiles = fg.sync(["**/*.html", "!node_modules"]);
|
||||||
|
const strictModeJSON = require("../tsconfig.strict.json");
|
||||||
|
const eslintIgnore = fs.readFileSync(".eslintignore", { encoding: "utf8" });
|
||||||
|
|
||||||
|
console.log("HTML File Count", htmlFiles.length);
|
||||||
|
client.trackMetric({
|
||||||
|
name: "HTML File Count",
|
||||||
|
value: htmlFiles.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("TypeScript Strict File Count", strictModeJSON.files.length);
|
||||||
|
client.trackMetric({
|
||||||
|
name: "TypeScript Strict File Count",
|
||||||
|
value: strictModeJSON.files.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Unlinted File Count", eslintIgnore.split("\n").length);
|
||||||
|
client.trackMetric({
|
||||||
|
name: "Unlinted File Count",
|
||||||
|
value: eslintIgnore.split("\n").length,
|
||||||
|
});
|
||||||
|
|
||||||
|
appInsights.defaultClient.flush({
|
||||||
|
callback: () => {
|
||||||
|
process.exitCode = 0;
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user