mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-26 12:21:23 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b0a98dce7 | ||
|
|
67ebce444f | ||
|
|
a09bcc7197 | ||
|
|
b2390e23e7 | ||
|
|
f922103e5c | ||
|
|
22d8a7a1be | ||
|
|
4210e0752b | ||
|
|
b217d4be1b | ||
|
|
81fd442fad | ||
|
|
87f7dd2230 | ||
|
|
9926fd97a2 | ||
|
|
faa98de9e9 | ||
|
|
2a7546e0de | ||
|
|
4b442dd869 | ||
|
|
f0b4737313 | ||
|
|
8dc5ed590a | ||
|
|
afaa844d28 | ||
|
|
3e5a876ef2 | ||
|
|
51abf1560a | ||
|
|
1c0fed88c0 | ||
|
|
93cfd52e36 | ||
|
|
3fd014ddad | ||
|
|
3b6fda4fa5 | ||
|
|
db7c45c9b8 | ||
|
|
4f6b75fe79 | ||
|
|
5038a01079 | ||
|
|
e0063c76d9 | ||
|
|
9278654479 | ||
|
|
59113d7bbf | ||
|
|
88d8200c14 | ||
|
|
6aaddd9c60 | ||
|
|
f8ede0cc1e | ||
|
|
bddb288a89 | ||
|
|
a14d20a88e | ||
|
|
f1db1ed978 | ||
|
|
86a483c3a4 | ||
|
|
263262a040 | ||
|
|
bd4d8da065 | ||
|
|
59ec18cd9b | ||
|
|
49bf8c60db | ||
|
|
b0b973b21a | ||
|
|
3529e80f0d |
@@ -87,7 +87,7 @@ src/Explorer/DataSamples/ContainerSampleGenerator.test.ts
|
|||||||
src/Explorer/DataSamples/ContainerSampleGenerator.ts
|
src/Explorer/DataSamples/ContainerSampleGenerator.ts
|
||||||
src/Explorer/DataSamples/DataSamplesUtil.test.ts
|
src/Explorer/DataSamples/DataSamplesUtil.test.ts
|
||||||
src/Explorer/DataSamples/DataSamplesUtil.ts
|
src/Explorer/DataSamples/DataSamplesUtil.ts
|
||||||
src/Explorer/Explorer.ts
|
src/Explorer/Explorer.tsx
|
||||||
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts
|
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts
|
||||||
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts
|
src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts
|
||||||
src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts
|
src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts
|
||||||
|
|||||||
17
.github/workflows/ci.yml
vendored
17
.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
|
||||||
@@ -151,6 +166,8 @@ jobs:
|
|||||||
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
|
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
|
||||||
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
|
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
|
||||||
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }}
|
PORTAL_RUNNER_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY }}
|
||||||
|
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT }}
|
||||||
|
PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY: ${{ secrets.PORTAL_RUNNER_MONGO_DATABASE_ACCOUNT_KEY }}
|
||||||
NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }}
|
NOTEBOOKS_TEST_RUNNER_TENANT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_TENANT_ID }}
|
||||||
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
|
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
|
||||||
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com"
|
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
|
||||||
|
"ENABLE_GALLERY_PUBLISH": true
|
||||||
}
|
}
|
||||||
|
|||||||
3
images/notebook/publish_content.svg
Normal file
3
images/notebook/publish_content.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.31449 2.01439L4.00103 5.31963L3.26105 4.57965L7.8407 0L12.4203 4.57965L11.6804 5.31963L8.36691 2.01439V12.8428H7.31449V2.01439ZM13.629 12.8428H14.6814V16H1V12.8428H2.05242V14.9476H13.629V12.8428Z" fill="#0078D4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 329 B |
@@ -57,6 +57,13 @@
|
|||||||
|
|
||||||
@FocusColor: #605e5c;
|
@FocusColor: #605e5c;
|
||||||
|
|
||||||
|
@GalleryBackgroundColor: #fdfdfd;
|
||||||
|
|
||||||
|
//Icons
|
||||||
|
@InfoIconColor: #0072c6;
|
||||||
|
@WarningIconColor: #db7500;
|
||||||
|
@ErrorIconColor: #b91f26;
|
||||||
|
|
||||||
/******************************************************************************
|
/******************************************************************************
|
||||||
METRICS
|
METRICS
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|||||||
@@ -1523,6 +1523,21 @@ p {
|
|||||||
.tooltipVisible();
|
.tooltipVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inputTooltip {
|
||||||
|
.inputTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputTooltip .inputTooltipText {
|
||||||
|
top: -68px;
|
||||||
|
.inputTooltipText();
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputTooltip .inputTooltipText::after {
|
||||||
|
border-width: @MediumSpace @MediumSpace 0 @MediumSpace;
|
||||||
|
top: 55px;
|
||||||
|
.inputTooltipTextAfter();
|
||||||
|
}
|
||||||
|
|
||||||
.infoTooltip a {
|
.infoTooltip a {
|
||||||
color: @AccentHigh;
|
color: @AccentHigh;
|
||||||
}
|
}
|
||||||
@@ -1694,6 +1709,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 +2099,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%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3027,3 +3043,45 @@ settings-pane {
|
|||||||
.collapsibleSection :hover {
|
.collapsibleSection :hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.messageBarInfoIcon {
|
||||||
|
color: @InfoIconColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageBarWarningIcon {
|
||||||
|
color: @WarningIconColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.freeTierInfoBanner {
|
||||||
|
background-color: @BaseLow;
|
||||||
|
display: inline-flex;
|
||||||
|
padding: @DefaultSpace;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.freeTierInfoIcon img {
|
||||||
|
height: 28px;
|
||||||
|
width: 28px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.freeTierInfoMessage {
|
||||||
|
margin: auto 0;
|
||||||
|
padding-left: @MediumSpace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.freeTierInlineWarning {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 8px 8px 8px 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.freeTierWarningIcon img {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.freeTierWarningMessage {
|
||||||
|
margin: auto 0;
|
||||||
|
padding-left: @SmallSpace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
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",
|
||||||
|
|||||||
@@ -119,7 +119,9 @@ export class Features {
|
|||||||
public static readonly enableSchema = "enableschema";
|
public static readonly enableSchema = "enableschema";
|
||||||
public static readonly enableSDKoperations = "enablesdkoperations";
|
public static readonly enableSDKoperations = "enablesdkoperations";
|
||||||
public static readonly showMinRUSurvey = "showminrusurvey";
|
public static readonly showMinRUSurvey = "showminrusurvey";
|
||||||
|
public static readonly enableDatabaseSettingsTabV1 = "enabledbsettingsv1";
|
||||||
public static readonly selfServeType = "selfservetype";
|
public static readonly selfServeType = "selfservetype";
|
||||||
|
public static readonly enableKOPanel = "enablekopanel";
|
||||||
}
|
}
|
||||||
|
|
||||||
// flight names returned from the portal are always lowercase
|
// flight names returned from the portal are always lowercase
|
||||||
@@ -128,6 +130,7 @@ export class Flights {
|
|||||||
public static readonly MongoIndexEditor = "mongoindexeditor";
|
public static readonly MongoIndexEditor = "mongoindexeditor";
|
||||||
public static readonly MongoIndexing = "mongoindexing";
|
public static readonly MongoIndexing = "mongoindexing";
|
||||||
public static readonly AutoscaleTest = "autoscaletest";
|
public static readonly AutoscaleTest = "autoscaletest";
|
||||||
|
public static readonly GalleryPublish = "gallerypublish";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AfecFeatures {
|
export class AfecFeatures {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export const getCollectionUsageSizeInKB = async (databaseName: string, container
|
|||||||
return dataUsageSizeInKb + indexUsageSizeInKb;
|
return dataUsageSizeInKb + indexUsageSizeInKb;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "getCollectionUsageSize");
|
handleError(error, "getCollectionUsageSize");
|
||||||
throw error;
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -26,6 +26,7 @@ interface ConfigContext {
|
|||||||
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
|
||||||
hostedExplorerURL: string;
|
hostedExplorerURL: string;
|
||||||
armAPIVersion?: string;
|
armAPIVersion?: string;
|
||||||
|
ENABLE_GALLERY_PUBLISH?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default configuration
|
// Default configuration
|
||||||
@@ -79,7 +80,11 @@ if (process.env.NODE_ENV === "development") {
|
|||||||
|
|
||||||
export async function initializeConfiguration(): Promise<ConfigContext> {
|
export async function initializeConfiguration(): Promise<ConfigContext> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("./config.json");
|
const response = await fetch("./config.json", {
|
||||||
|
headers: {
|
||||||
|
"If-None-Match": "", // disable client side cache
|
||||||
|
},
|
||||||
|
});
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
try {
|
try {
|
||||||
const { allowedParentFrameOrigins, ...externalConfig } = await response.json();
|
const { allowedParentFrameOrigins, ...externalConfig } = await response.json();
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -92,6 +91,7 @@ export interface Database extends TreeNode {
|
|||||||
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
|
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
|
||||||
onSettingsClick: () => void;
|
onSettingsClick: () => void;
|
||||||
loadOffer(): Promise<void>;
|
loadOffer(): Promise<void>;
|
||||||
|
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionBase extends TreeNode {
|
export interface CollectionBase extends TreeNode {
|
||||||
@@ -109,7 +109,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;
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,6 @@ export interface Collection extends CollectionBase {
|
|||||||
openTab(): void;
|
openTab(): void;
|
||||||
|
|
||||||
onSettingsClick: () => Promise<void>;
|
onSettingsClick: () => Promise<void>;
|
||||||
onDeleteCollectionContextMenuClick(source: Collection, event: MouseEvent): void;
|
|
||||||
|
|
||||||
onNewGraphClick(): void;
|
onNewGraphClick(): void;
|
||||||
onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string): void;
|
onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string): void;
|
||||||
@@ -176,9 +175,10 @@ 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;
|
||||||
|
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -293,10 +293,6 @@ export interface DocumentsTabOptions extends TabOptions {
|
|||||||
resourceTokenPartitionKey?: string;
|
resourceTokenPartitionKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsTabV2Options extends TabOptions {
|
|
||||||
getPendingNotification: Q.Promise<DataModels.Notification>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConflictsTabOptions extends TabOptions {
|
export interface ConflictsTabOptions extends TabOptions {
|
||||||
partitionKey: DataModels.PartitionKey;
|
partitionKey: DataModels.PartitionKey;
|
||||||
conflictIds: ko.ObservableArray<ConflictId>;
|
conflictIds: ko.ObservableArray<ConflictId>;
|
||||||
@@ -363,7 +359,8 @@ export enum CollectionTabKind {
|
|||||||
Gallery = 17,
|
Gallery = 17,
|
||||||
NotebookViewer = 18,
|
NotebookViewer = 18,
|
||||||
Schema = 19,
|
Schema = 19,
|
||||||
SettingsV2 = 20,
|
CollectionSettingsV2 = 20,
|
||||||
|
DatabaseSettingsV2 = 21,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TerminalKind {
|
export enum TerminalKind {
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ describe("Component Registerer", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should register settings-tab-v2 component", () => {
|
it("should register settings-tab-v2 component", () => {
|
||||||
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
|
expect(ko.components.isRegistered("database-settings-tab-v2")).toBe(true);
|
||||||
|
expect(ko.components.isRegistered("collection-settings-tab-v2")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should register query-tab component", () => {
|
it("should register query-tab component", () => {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTa
|
|||||||
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
|
ko.components.register("stored-procedure-tab", new TabComponents.StoredProcedureTab());
|
||||||
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
|
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
|
||||||
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
|
ko.components.register("user-defined-function-tab", new TabComponents.UserDefinedFunctionTab());
|
||||||
ko.components.register("settings-tab-v2", new TabComponents.SettingsTabV2());
|
ko.components.register("collection-settings-tab-v2", new TabComponents.SettingsTabV2());
|
||||||
ko.components.register("query-tab", new TabComponents.QueryTab());
|
ko.components.register("query-tab", new TabComponents.QueryTab());
|
||||||
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
|
ko.components.register("tables-query-tab", new TabComponents.QueryTablesTab());
|
||||||
ko.components.register("graph-tab", new TabComponents.GraphTab());
|
ko.components.register("graph-tab", new TabComponents.GraphTab());
|
||||||
@@ -45,6 +45,7 @@ ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTa
|
|||||||
|
|
||||||
// Database Tabs
|
// Database Tabs
|
||||||
ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab());
|
ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab());
|
||||||
|
ko.components.register("database-settings-tab-v2", new TabComponents.SettingsTabV2());
|
||||||
|
|
||||||
// Panes
|
// Panes
|
||||||
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
|
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
|
||||||
|
|||||||
@@ -112,10 +112,7 @@ export class ResourceTreeContextMenuButtonFactory {
|
|||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteCollectionIcon,
|
iconSrc: DeleteCollectionIcon,
|
||||||
onClick: () => {
|
onClick: () => container.openDeleteCollectionConfirmationPane(),
|
||||||
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
|
||||||
selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null);
|
|
||||||
},
|
|
||||||
label: container.deleteCollectionText(),
|
label: container.deleteCollectionText(),
|
||||||
styleClass: "deleteCollectionMenuItem",
|
styleClass: "deleteCollectionMenuItem",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import { Dialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric
|
|||||||
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
|
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
|
||||||
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
|
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
|
||||||
import { Link } from "office-ui-fabric-react/lib/Link";
|
import { Link } from "office-ui-fabric-react/lib/Link";
|
||||||
import { ChoiceGroup, FontIcon, IChoiceGroupProps } from "office-ui-fabric-react";
|
import {
|
||||||
|
ChoiceGroup,
|
||||||
|
FontIcon,
|
||||||
|
IChoiceGroupProps,
|
||||||
|
IProgressIndicatorProps,
|
||||||
|
ProgressIndicator,
|
||||||
|
} from "office-ui-fabric-react";
|
||||||
|
|
||||||
export interface TextFieldProps extends ITextFieldProps {
|
export interface TextFieldProps extends ITextFieldProps {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -27,6 +33,7 @@ export interface DialogProps {
|
|||||||
choiceGroupProps?: IChoiceGroupProps;
|
choiceGroupProps?: IChoiceGroupProps;
|
||||||
textFieldProps?: TextFieldProps;
|
textFieldProps?: TextFieldProps;
|
||||||
linkProps?: LinkProps;
|
linkProps?: LinkProps;
|
||||||
|
progressIndicatorProps?: IProgressIndicatorProps;
|
||||||
primaryButtonText: string;
|
primaryButtonText: string;
|
||||||
secondaryButtonText: string;
|
secondaryButtonText: string;
|
||||||
onPrimaryButtonClick: () => void;
|
onPrimaryButtonClick: () => void;
|
||||||
@@ -62,13 +69,14 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
|
|||||||
showCloseButton: this.props.showCloseButton || false,
|
showCloseButton: this.props.showCloseButton || false,
|
||||||
onDismiss: this.props.onDismiss,
|
onDismiss: this.props.onDismiss,
|
||||||
},
|
},
|
||||||
modalProps: { isBlocking: this.props.isModal },
|
modalProps: { isBlocking: this.props.isModal, isDarkOverlay: false },
|
||||||
minWidth: DIALOG_MIN_WIDTH,
|
minWidth: DIALOG_MIN_WIDTH,
|
||||||
maxWidth: DIALOG_MAX_WIDTH,
|
maxWidth: DIALOG_MAX_WIDTH,
|
||||||
};
|
};
|
||||||
const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps;
|
const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps;
|
||||||
const textFieldProps: ITextFieldProps = this.props.textFieldProps;
|
const textFieldProps: ITextFieldProps = this.props.textFieldProps;
|
||||||
const linkProps: LinkProps = this.props.linkProps;
|
const linkProps: LinkProps = this.props.linkProps;
|
||||||
|
const progressIndicatorProps: IProgressIndicatorProps = this.props.progressIndicatorProps;
|
||||||
const primaryButtonProps: IButtonProps = {
|
const primaryButtonProps: IButtonProps = {
|
||||||
text: this.props.primaryButtonText,
|
text: this.props.primaryButtonText,
|
||||||
disabled: this.props.primaryButtonDisabled || false,
|
disabled: this.props.primaryButtonDisabled || false,
|
||||||
@@ -91,6 +99,7 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
|
|||||||
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
|
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<PrimaryButton {...primaryButtonProps} />
|
<PrimaryButton {...primaryButtonProps} />
|
||||||
{secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}
|
{secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import * as React from "react";
|
|||||||
import { IGalleryItem } from "../../../../Juno/JunoClient";
|
import { IGalleryItem } from "../../../../Juno/JunoClient";
|
||||||
import { FileSystemUtil } from "../../../Notebook/FileSystemUtil";
|
import { FileSystemUtil } from "../../../Notebook/FileSystemUtil";
|
||||||
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
|
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
|
||||||
import { StyleConstants } from "../../../../Common/Constants";
|
|
||||||
|
|
||||||
export interface GalleryCardComponentProps {
|
export interface GalleryCardComponentProps {
|
||||||
data: IGalleryItem;
|
data: IGalleryItem;
|
||||||
@@ -38,7 +37,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
|||||||
private static readonly cardImageHeight = 144;
|
private static readonly cardImageHeight = 144;
|
||||||
public static readonly cardHeightToWidthRatio =
|
public static readonly cardHeightToWidthRatio =
|
||||||
GalleryCardComponent.cardImageHeight / GalleryCardComponent.CARD_WIDTH;
|
GalleryCardComponent.cardImageHeight / GalleryCardComponent.CARD_WIDTH;
|
||||||
private static readonly cardDescriptionMaxChars = 88;
|
private static readonly cardDescriptionMaxChars = 80;
|
||||||
private static readonly cardItemGapBig = 10;
|
private static readonly cardItemGapBig = 10;
|
||||||
private static readonly cardItemGapSmall = 8;
|
private static readonly cardItemGapSmall = 8;
|
||||||
|
|
||||||
@@ -54,6 +53,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
style={{ background: "white" }}
|
||||||
aria-label={cardTitle}
|
aria-label={cardTitle}
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
tokens={{ width: GalleryCardComponent.CARD_WIDTH, childrenGap: 0 }}
|
tokens={{ width: GalleryCardComponent.CARD_WIDTH, childrenGap: 0 }}
|
||||||
@@ -79,12 +79,16 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
|||||||
|
|
||||||
<Card.Section styles={{ root: { padding: GalleryCardComponent.cardItemGapBig } }}>
|
<Card.Section styles={{ root: { padding: GalleryCardComponent.cardItemGapBig } }}>
|
||||||
<Text variant="small" nowrap>
|
<Text variant="small" nowrap>
|
||||||
{this.props.data.tags?.map((tag, index, array) => (
|
{this.props.data.tags ? (
|
||||||
|
this.props.data.tags.map((tag, index, array) => (
|
||||||
<span key={tag}>
|
<span key={tag}>
|
||||||
<Link onClick={(event) => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link>
|
<Link onClick={(event) => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link>
|
||||||
{index === array.length - 1 ? <></> : ", "}
|
{index === array.length - 1 ? <></> : ", "}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<br />
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
@@ -101,13 +105,14 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text variant="small" styles={{ root: { height: 36 } }}>
|
<Text variant="small" styles={{ root: { height: 36 } }}>
|
||||||
{this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars)}
|
{this.renderTruncatedDescription()}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{this.generateIconText("RedEye", this.props.data.views.toString())}
|
{this.props.data.views !== undefined && this.generateIconText("RedEye", this.props.data.views.toString())}
|
||||||
{this.generateIconText("Download", this.props.data.downloads.toString())}
|
{this.props.data.downloads !== undefined &&
|
||||||
{this.props.isFavorite !== undefined &&
|
this.generateIconText("Download", this.props.data.downloads.toString())}
|
||||||
|
{this.props.data.favorites !== undefined &&
|
||||||
this.generateIconText("Heart", this.props.data.favorites.toString())}
|
this.generateIconText("Heart", this.props.data.favorites.toString())}
|
||||||
</span>
|
</span>
|
||||||
</Card.Section>
|
</Card.Section>
|
||||||
@@ -127,7 +132,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
|||||||
{this.props.isFavorite !== undefined &&
|
{this.props.isFavorite !== undefined &&
|
||||||
this.generateIconButtonWithTooltip(
|
this.generateIconButtonWithTooltip(
|
||||||
this.props.isFavorite ? "HeartFill" : "Heart",
|
this.props.isFavorite ? "HeartFill" : "Heart",
|
||||||
this.props.isFavorite ? "Unlike" : "Like",
|
this.props.isFavorite ? "Unfavorite" : "Favorite",
|
||||||
"left",
|
"left",
|
||||||
this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick
|
this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick
|
||||||
)}
|
)}
|
||||||
@@ -144,12 +149,17 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderTruncatedDescription = (): string => {
|
||||||
|
let truncatedDescription = this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars);
|
||||||
|
if (this.props.data.description.length > GalleryCardComponent.cardDescriptionMaxChars) {
|
||||||
|
truncatedDescription = `${truncatedDescription} ...`;
|
||||||
|
}
|
||||||
|
return truncatedDescription;
|
||||||
|
};
|
||||||
|
|
||||||
private generateIconText = (iconName: string, text: string): JSX.Element => {
|
private generateIconText = (iconName: string, text: string): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text variant="tiny" styles={{ root: { color: "#605E5C", paddingRight: GalleryCardComponent.cardItemGapSmall } }}>
|
||||||
variant="tiny"
|
|
||||||
styles={{ root: { color: StyleConstants.BaseMedium, paddingRight: GalleryCardComponent.cardItemGapSmall } }}
|
|
||||||
>
|
|
||||||
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} /> {text}
|
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} /> {text}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
aria-label="name"
|
aria-label="name"
|
||||||
data-is-focusable="true"
|
data-is-focusable="true"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"background": "white",
|
||||||
|
}
|
||||||
|
}
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 0,
|
"childrenGap": 0,
|
||||||
@@ -88,7 +93,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"color": undefined,
|
"color": "#605E5C",
|
||||||
"paddingRight": 8,
|
"paddingRight": 8,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -112,7 +117,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"color": undefined,
|
"color": "#605E5C",
|
||||||
"paddingRight": 8,
|
"paddingRight": 8,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -136,7 +141,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"color": undefined,
|
"color": "#605E5C",
|
||||||
"paddingRight": 8,
|
"paddingRight": 8,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -185,7 +190,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
"gapSpace": 0,
|
"gapSpace": 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
content="Like"
|
content="Favorite"
|
||||||
id="TooltipHost-IconButton-Heart"
|
id="TooltipHost-IconButton-Heart"
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
@@ -197,14 +202,14 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CustomizedIconButton
|
<CustomizedIconButton
|
||||||
ariaLabel="Like"
|
ariaLabel="Favorite"
|
||||||
iconProps={
|
iconProps={
|
||||||
Object {
|
Object {
|
||||||
"iconName": "Heart",
|
"iconName": "Heart",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
title="Like"
|
title="Favorite"
|
||||||
/>
|
/>
|
||||||
</StyledTooltipHostBase>
|
</StyledTooltipHostBase>
|
||||||
<StyledTooltipHostBase
|
<StyledTooltipHostBase
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import * as React from "react";
|
|||||||
import { JunoClient } from "../../../Juno/JunoClient";
|
import { JunoClient } from "../../../Juno/JunoClient";
|
||||||
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
|
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
|
||||||
import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react";
|
import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react";
|
||||||
import { handleError } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
|
||||||
|
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
|
||||||
export interface CodeOfConductComponentProps {
|
export interface CodeOfConductComponentProps {
|
||||||
junoClient: JunoClient;
|
junoClient: JunoClient;
|
||||||
@@ -14,11 +16,11 @@ interface CodeOfConductComponentState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CodeOfConductComponent extends React.Component<CodeOfConductComponentProps, CodeOfConductComponentState> {
|
export class CodeOfConductComponent extends React.Component<CodeOfConductComponentProps, CodeOfConductComponentState> {
|
||||||
|
private viewCodeOfConductTraced: boolean;
|
||||||
private descriptionPara1: string;
|
private descriptionPara1: string;
|
||||||
private descriptionPara2: string;
|
private descriptionPara2: string;
|
||||||
private descriptionPara3: string;
|
private descriptionPara3: string;
|
||||||
private link1: { label: string; url: string };
|
private link1: { label: string; url: string };
|
||||||
private link2: { label: string; url: string };
|
|
||||||
|
|
||||||
constructor(props: CodeOfConductComponentProps) {
|
constructor(props: CodeOfConductComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -27,23 +29,34 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
|
|||||||
readCodeOfConduct: false,
|
readCodeOfConduct: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.descriptionPara1 = "Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement";
|
this.descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct";
|
||||||
this.descriptionPara2 =
|
this.descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.";
|
||||||
"Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.";
|
this.descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the ";
|
||||||
this.descriptionPara3 = "In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the ";
|
this.link1 = { label: "code of conduct.", url: CodeOfConductEndpoints.codeOfConduct };
|
||||||
this.link1 = { label: "code of conduct", url: CodeOfConductEndpoints.codeOfConduct };
|
|
||||||
this.link2 = { label: "privacy statement", url: CodeOfConductEndpoints.privacyStatement };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async acceptCodeOfConduct(): Promise<void> {
|
private async acceptCodeOfConduct(): Promise<void> {
|
||||||
|
const startKey = traceStart(Action.NotebooksGalleryAcceptCodeOfConduct);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.props.junoClient.acceptCodeOfConduct();
|
const response = await this.props.junoClient.acceptCodeOfConduct();
|
||||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||||
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
|
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
traceSuccess(Action.NotebooksGalleryAcceptCodeOfConduct, startKey);
|
||||||
|
|
||||||
this.props.onAcceptCodeOfConduct(response.data);
|
this.props.onAcceptCodeOfConduct(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
traceFailure(
|
||||||
|
Action.NotebooksGalleryAcceptCodeOfConduct,
|
||||||
|
{
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
errorStack: getErrorStack(error),
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
|
||||||
handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct");
|
handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,6 +66,11 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
|
|||||||
};
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
|
if (!this.viewCodeOfConductTraced) {
|
||||||
|
this.viewCodeOfConductTraced = true;
|
||||||
|
trace(Action.NotebooksGalleryViewCodeOfConduct);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack tokens={{ childrenGap: 20 }}>
|
<Stack tokens={{ childrenGap: 20 }}>
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
@@ -69,10 +87,6 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
|
|||||||
<Link href={this.link1.url} target="_blank">
|
<Link href={this.link1.url} target="_blank">
|
||||||
{this.link1.label}
|
{this.link1.label}
|
||||||
</Link>
|
</Link>
|
||||||
{" and "}
|
|
||||||
<Link href={this.link2.url} target="_blank">
|
|
||||||
{this.link2.label}
|
|
||||||
</Link>
|
|
||||||
</Text>
|
</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
|
||||||
@@ -87,7 +101,7 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
label="I have read and accepted the code of conduct and privacy statement"
|
label="I have read and accept the code of conduct."
|
||||||
onChange={this.onChangeCheckbox}
|
onChange={this.onChangeCheckbox}
|
||||||
/>
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Explorer from "../../Explorer";
|
|||||||
|
|
||||||
export interface GalleryAndNotebookViewerComponentProps {
|
export interface GalleryAndNotebookViewerComponentProps {
|
||||||
container?: Explorer;
|
container?: Explorer;
|
||||||
|
isGalleryPublishEnabled: boolean;
|
||||||
junoClient: JunoClient;
|
junoClient: JunoClient;
|
||||||
notebookUrl?: string;
|
notebookUrl?: string;
|
||||||
galleryItem?: IGalleryItem;
|
galleryItem?: IGalleryItem;
|
||||||
@@ -60,6 +61,7 @@ export class GalleryAndNotebookViewerComponent extends React.Component<
|
|||||||
|
|
||||||
const props: GalleryViewerComponentProps = {
|
const props: GalleryViewerComponentProps = {
|
||||||
container: this.props.container,
|
container: this.props.container,
|
||||||
|
isGalleryPublishEnabled: this.props.isGalleryPublishEnabled,
|
||||||
junoClient: this.props.junoClient,
|
junoClient: this.props.junoClient,
|
||||||
selectedTab: this.state.selectedTab,
|
selectedTab: this.state.selectedTab,
|
||||||
sortBy: this.state.sortBy,
|
sortBy: this.state.sortBy,
|
||||||
|
|||||||
@@ -7,14 +7,20 @@ import {
|
|||||||
} from "./GalleryAndNotebookViewerComponent";
|
} from "./GalleryAndNotebookViewerComponent";
|
||||||
|
|
||||||
export class GalleryAndNotebookViewerComponentAdapter implements ReactAdapter {
|
export class GalleryAndNotebookViewerComponentAdapter implements ReactAdapter {
|
||||||
|
private key: string;
|
||||||
public parameters: ko.Observable<number>;
|
public parameters: ko.Observable<number>;
|
||||||
|
|
||||||
constructor(private props: GalleryAndNotebookViewerComponentProps) {
|
constructor(private props: GalleryAndNotebookViewerComponentProps) {
|
||||||
|
this.reset();
|
||||||
this.parameters = ko.observable<number>(Date.now());
|
this.parameters = ko.observable<number>(Date.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
public renderComponent(): JSX.Element {
|
||||||
return <GalleryAndNotebookViewerComponent {...this.props} />;
|
return <GalleryAndNotebookViewerComponent key={this.key} {...this.props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
public reset(): void {
|
||||||
|
this.key = `GalleryAndNotebookViewerComponent-${Date.now()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public triggerRender(): void {
|
public triggerRender(): void {
|
||||||
|
|||||||
@@ -6,4 +6,16 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-family: @DataExplorerFont;
|
font-family: @DataExplorerFont;
|
||||||
|
background: @GalleryBackgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publicGalleryTabContainer {
|
||||||
|
position: relative;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publicGalleryTabOverlayContent {
|
||||||
|
background: white;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 10%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { GalleryViewerComponent, GalleryViewerComponentProps, GalleryTab, SortBy
|
|||||||
describe("GalleryViewerComponent", () => {
|
describe("GalleryViewerComponent", () => {
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
const props: GalleryViewerComponentProps = {
|
const props: GalleryViewerComponentProps = {
|
||||||
|
isGalleryPublishEnabled: false,
|
||||||
junoClient: undefined,
|
junoClient: undefined,
|
||||||
selectedTab: GalleryTab.OfficialSamples,
|
selectedTab: GalleryTab.OfficialSamples,
|
||||||
sortBy: SortBy.MostViewed,
|
sortBy: SortBy.MostViewed,
|
||||||
|
|||||||
@@ -9,10 +9,14 @@ import {
|
|||||||
IPivotProps,
|
IPivotProps,
|
||||||
IRectangle,
|
IRectangle,
|
||||||
Label,
|
Label,
|
||||||
|
Link,
|
||||||
List,
|
List,
|
||||||
|
Overlay,
|
||||||
Pivot,
|
Pivot,
|
||||||
PivotItem,
|
PivotItem,
|
||||||
SearchBox,
|
SearchBox,
|
||||||
|
Spinner,
|
||||||
|
SpinnerSize,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
} from "office-ui-fabric-react";
|
} from "office-ui-fabric-react";
|
||||||
@@ -27,9 +31,12 @@ import Explorer from "../../Explorer";
|
|||||||
import { CodeOfConductComponent } from "./CodeOfConductComponent";
|
import { CodeOfConductComponent } from "./CodeOfConductComponent";
|
||||||
import { InfoComponent } from "./InfoComponent/InfoComponent";
|
import { InfoComponent } from "./InfoComponent/InfoComponent";
|
||||||
import { handleError } from "../../../Common/ErrorHandlingUtils";
|
import { handleError } from "../../../Common/ErrorHandlingUtils";
|
||||||
|
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
|
||||||
export interface GalleryViewerComponentProps {
|
export interface GalleryViewerComponentProps {
|
||||||
container?: Explorer;
|
container?: Explorer;
|
||||||
|
isGalleryPublishEnabled: boolean;
|
||||||
junoClient: JunoClient;
|
junoClient: JunoClient;
|
||||||
selectedTab: GalleryTab;
|
selectedTab: GalleryTab;
|
||||||
sortBy: SortBy;
|
sortBy: SortBy;
|
||||||
@@ -64,6 +71,8 @@ interface GalleryViewerComponentState {
|
|||||||
searchText: string;
|
searchText: string;
|
||||||
dialogProps: DialogProps;
|
dialogProps: DialogProps;
|
||||||
isCodeOfConductAccepted: boolean;
|
isCodeOfConductAccepted: boolean;
|
||||||
|
isFetchingPublishedNotebooks: boolean;
|
||||||
|
isFetchingFavouriteNotebooks: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GalleryTabInfo {
|
interface GalleryTabInfo {
|
||||||
@@ -74,18 +83,24 @@ interface GalleryTabInfo {
|
|||||||
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps, GalleryViewerComponentState> {
|
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps, GalleryViewerComponentState> {
|
||||||
public static readonly OfficialSamplesTitle = "Official samples";
|
public static readonly OfficialSamplesTitle = "Official samples";
|
||||||
public static readonly PublicGalleryTitle = "Public gallery";
|
public static readonly PublicGalleryTitle = "Public gallery";
|
||||||
public static readonly FavoritesTitle = "Liked";
|
public static readonly FavoritesTitle = "My favorites";
|
||||||
public static readonly PublishedTitle = "Your published work";
|
public static readonly PublishedTitle = "My published work";
|
||||||
|
|
||||||
private static readonly rowsPerPage = 5;
|
private static readonly rowsPerPage = 5;
|
||||||
|
|
||||||
private static readonly mostViewedText = "Most viewed";
|
private static readonly mostViewedText = "Most viewed";
|
||||||
private static readonly mostDownloadedText = "Most downloaded";
|
private static readonly mostDownloadedText = "Most downloaded";
|
||||||
private static readonly mostFavoritedText = "Most liked";
|
private static readonly mostFavoritedText = "Most favorited";
|
||||||
private static readonly mostRecentText = "Most recent";
|
private static readonly mostRecentText = "Most recent";
|
||||||
|
|
||||||
private readonly sortingOptions: IDropdownOption[];
|
private readonly sortingOptions: IDropdownOption[];
|
||||||
|
|
||||||
|
private viewGalleryTraced: boolean;
|
||||||
|
private viewOfficialSamplesTraced: boolean;
|
||||||
|
private viewPublicGalleryTraced: boolean;
|
||||||
|
private viewFavoritesTraced: boolean;
|
||||||
|
private viewPublishedNotebooksTraced: boolean;
|
||||||
|
|
||||||
private sampleNotebooks: IGalleryItem[];
|
private sampleNotebooks: IGalleryItem[];
|
||||||
private publicNotebooks: IGalleryItem[];
|
private publicNotebooks: IGalleryItem[];
|
||||||
private favoriteNotebooks: IGalleryItem[];
|
private favoriteNotebooks: IGalleryItem[];
|
||||||
@@ -107,6 +122,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
searchText: props.searchText,
|
searchText: props.searchText,
|
||||||
dialogProps: undefined,
|
dialogProps: undefined,
|
||||||
isCodeOfConductAccepted: undefined,
|
isCodeOfConductAccepted: undefined,
|
||||||
|
isFetchingFavouriteNotebooks: true,
|
||||||
|
isFetchingPublishedNotebooks: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sortingOptions = [
|
this.sortingOptions = [
|
||||||
@@ -137,9 +154,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
|
this.traceViewGallery();
|
||||||
|
|
||||||
const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||||
|
|
||||||
if (this.props.container?.isGalleryPublishEnabled()) {
|
if (this.props.isGalleryPublishEnabled) {
|
||||||
tabs.push(
|
tabs.push(
|
||||||
this.createPublicGalleryTab(
|
this.createPublicGalleryTab(
|
||||||
GalleryTab.PublicGallery,
|
GalleryTab.PublicGallery,
|
||||||
@@ -147,13 +166,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
this.state.isCodeOfConductAccepted
|
this.state.isCodeOfConductAccepted
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
|
||||||
|
|
||||||
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
|
|
||||||
// Displaying code of conduct component on gallery load should not be the default behavior.
|
|
||||||
if (this.state.isCodeOfConductAccepted !== false) {
|
|
||||||
tabs.push(this.createPublishedNotebooksTab(GalleryTab.Published, this.state.publishedNotebooks));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||||
|
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||||
|
tabs.push(this.createPublishedNotebooksTab(GalleryTab.Published, this.state.publishedNotebooks));
|
||||||
}
|
}
|
||||||
|
|
||||||
const pivotProps: IPivotProps = {
|
const pivotProps: IPivotProps = {
|
||||||
@@ -184,11 +201,58 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private traceViewGallery = (): void => {
|
||||||
|
if (!this.viewGalleryTraced) {
|
||||||
|
this.viewGalleryTraced = true;
|
||||||
|
trace(Action.NotebooksGalleryViewGallery);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.state.selectedTab) {
|
||||||
|
case GalleryTab.OfficialSamples:
|
||||||
|
if (!this.viewOfficialSamplesTraced) {
|
||||||
|
this.resetViewGalleryTabTracedFlags();
|
||||||
|
this.viewOfficialSamplesTraced = true;
|
||||||
|
trace(Action.NotebooksGalleryViewOfficialSamples);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case GalleryTab.PublicGallery:
|
||||||
|
if (!this.viewPublicGalleryTraced) {
|
||||||
|
this.resetViewGalleryTabTracedFlags();
|
||||||
|
this.viewPublicGalleryTraced = true;
|
||||||
|
trace(Action.NotebooksGalleryViewPublicGallery);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case GalleryTab.Favorites:
|
||||||
|
if (!this.viewFavoritesTraced) {
|
||||||
|
this.resetViewGalleryTabTracedFlags();
|
||||||
|
this.viewFavoritesTraced = true;
|
||||||
|
trace(Action.NotebooksGalleryViewFavorites);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case GalleryTab.Published:
|
||||||
|
if (!this.viewPublishedNotebooksTraced) {
|
||||||
|
this.resetViewGalleryTabTracedFlags();
|
||||||
|
this.viewPublishedNotebooksTraced = true;
|
||||||
|
trace(Action.NotebooksGalleryViewPublishedNotebooks);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown selected tab ${this.state.selectedTab}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private resetViewGalleryTabTracedFlags = (): void => {
|
||||||
|
this.viewOfficialSamplesTraced = false;
|
||||||
|
this.viewPublicGalleryTraced = false;
|
||||||
|
this.viewFavoritesTraced = false;
|
||||||
|
this.viewPublishedNotebooksTraced = false;
|
||||||
|
};
|
||||||
|
|
||||||
private isEmptyData = (data: IGalleryItem[]): boolean => {
|
private isEmptyData = (data: IGalleryItem[]): boolean => {
|
||||||
return !data || data.length === 0;
|
return !data || data.length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
private createEmptyTabContent = (iconName: string, line1: string, line2: string): JSX.Element => {
|
private createEmptyTabContent = (iconName: string, line1: JSX.Element, line2: JSX.Element): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}>
|
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}>
|
||||||
<FontIcon iconName={iconName} style={{ fontSize: 100, color: "lightgray", marginTop: 20 }} />
|
<FontIcon iconName={iconName} style={{ fontSize: 100, color: "lightgray", marginTop: 20 }} />
|
||||||
@@ -216,40 +280,63 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getFavouriteNotebooksTabContent = (data: IGalleryItem[]) => {
|
||||||
|
if (this.isEmptyData(data)) {
|
||||||
|
if (this.state.isFetchingFavouriteNotebooks) {
|
||||||
|
return <Spinner size={SpinnerSize.large} />;
|
||||||
|
}
|
||||||
|
return this.createEmptyTabContent(
|
||||||
|
"ContactHeart",
|
||||||
|
<>You don't have any favorites yet</>,
|
||||||
|
<>
|
||||||
|
Favorite any notebook from the{" "}
|
||||||
|
<Link onClick={() => this.setState({ selectedTab: GalleryTab.OfficialSamples })}>official samples</Link> or{" "}
|
||||||
|
<Link onClick={() => this.setState({ selectedTab: GalleryTab.PublicGallery })}>public gallery</Link>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.createSearchBarHeader(this.createCardsTabContent(data));
|
||||||
|
};
|
||||||
|
|
||||||
private createFavoritesTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
private createFavoritesTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||||
return {
|
return {
|
||||||
tab,
|
tab,
|
||||||
content: this.isEmptyData(data)
|
content: this.getFavouriteNotebooksTabContent(data),
|
||||||
? this.createEmptyTabContent(
|
|
||||||
"ContactHeart",
|
|
||||||
"You have not liked anything",
|
|
||||||
"Like any notebook from Official Samples or Public gallery"
|
|
||||||
)
|
|
||||||
: this.createSearchBarHeader(this.createCardsTabContent(data)),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPublishedNotebooksTabContent = (data: IGalleryItem[]) => {
|
||||||
|
if (this.isEmptyData(data)) {
|
||||||
|
if (this.state.isFetchingPublishedNotebooks) {
|
||||||
|
return <Spinner size={SpinnerSize.large} />;
|
||||||
|
}
|
||||||
|
return this.createEmptyTabContent(
|
||||||
|
"Contact",
|
||||||
|
<>
|
||||||
|
You have not published anything to the{" "}
|
||||||
|
<Link onClick={() => this.setState({ selectedTab: GalleryTab.PublicGallery })}>public gallery</Link> yet
|
||||||
|
</>,
|
||||||
|
<>Publish your notebooks to share your work with other users</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.createPublishedNotebooksTabContent(data);
|
||||||
|
};
|
||||||
|
|
||||||
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
private createPublishedNotebooksTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||||
return {
|
return {
|
||||||
tab,
|
tab,
|
||||||
content: this.isEmptyData(data)
|
content: this.getPublishedNotebooksTabContent(data),
|
||||||
? this.createEmptyTabContent(
|
|
||||||
"Contact",
|
|
||||||
"You have not published anything",
|
|
||||||
"Publish your sample notebooks to share your published work with others"
|
|
||||||
)
|
|
||||||
: this.createPublishedNotebooksTabContent(data),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
private createPublishedNotebooksTabContent = (data: IGalleryItem[]): JSX.Element => {
|
private createPublishedNotebooksTabContent = (data: IGalleryItem[]): JSX.Element => {
|
||||||
const { published, underReview, removed } = GalleryUtils.filterPublishedNotebooks(data);
|
const { published, underReview, removed } = GalleryUtils.filterPublishedNotebooks(data);
|
||||||
const content = (
|
const content = (
|
||||||
<Stack tokens={{ childrenGap: 10 }}>
|
<Stack tokens={{ childrenGap: 20 }}>
|
||||||
{published?.length > 0 &&
|
{published?.length > 0 &&
|
||||||
this.createPublishedNotebooksSectionContent(
|
this.createPublishedNotebooksSectionContent(
|
||||||
undefined,
|
undefined,
|
||||||
"You have successfully published the following notebook(s) to public gallery and shared with other Azure Cosmos DB users.",
|
"You have successfully published and shared the following notebook(s) to the public gallery.",
|
||||||
this.createCardsTabContent(published)
|
this.createCardsTabContent(published)
|
||||||
)}
|
)}
|
||||||
{underReview?.length > 0 &&
|
{underReview?.length > 0 &&
|
||||||
@@ -276,24 +363,33 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
content: JSX.Element
|
content: JSX.Element
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Stack tokens={{ childrenGap: 5 }}>
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
{title && <Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{title}</Text>}
|
{title && (
|
||||||
{description && <Text>{description}</Text>}
|
<Text styles={{ root: { fontWeight: FontWeights.semibold, marginLeft: 10, marginRight: 10 } }}>{title}</Text>
|
||||||
|
)}
|
||||||
|
{description && <Text styles={{ root: { marginLeft: 10, marginRight: 10 } }}>{description}</Text>}
|
||||||
{content}
|
{content}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private createPublicGalleryTabContent(data: IGalleryItem[], acceptedCodeOfConduct: boolean): JSX.Element {
|
private createPublicGalleryTabContent(data: IGalleryItem[], acceptedCodeOfConduct: boolean): JSX.Element {
|
||||||
return acceptedCodeOfConduct === false ? (
|
return (
|
||||||
|
<div className="publicGalleryTabContainer">
|
||||||
|
{this.createSearchBarHeader(this.createCardsTabContent(data))}
|
||||||
|
{acceptedCodeOfConduct === false && (
|
||||||
|
<Overlay isDarkThemed>
|
||||||
|
<div className="publicGalleryTabOverlayContent">
|
||||||
<CodeOfConductComponent
|
<CodeOfConductComponent
|
||||||
junoClient={this.props.junoClient}
|
junoClient={this.props.junoClient}
|
||||||
onAcceptCodeOfConduct={(result: boolean) => {
|
onAcceptCodeOfConduct={(result: boolean) => {
|
||||||
this.setState({ isCodeOfConductAccepted: result });
|
this.setState({ isCodeOfConductAccepted: result });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
</div>
|
||||||
this.createSearchBarHeader(this.createCardsTabContent(data))
|
</Overlay>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +406,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
<Stack.Item styles={{ root: { minWidth: 200 } }}>
|
<Stack.Item styles={{ root: { minWidth: 200 } }}>
|
||||||
<Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
|
<Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
{(!this.props.container || this.props.container.isGalleryPublishEnabled()) && (
|
{this.props.isGalleryPublishEnabled && (
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<InfoComponent />
|
<InfoComponent />
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
@@ -322,7 +418,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createCardsTabContent(data: IGalleryItem[]): JSX.Element {
|
private createCardsTabContent(data: IGalleryItem[]): JSX.Element {
|
||||||
return (
|
return data ? (
|
||||||
<FocusZone>
|
<FocusZone>
|
||||||
<List
|
<List
|
||||||
items={data}
|
items={data}
|
||||||
@@ -331,12 +427,14 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
onRenderCell={this.onRenderCell}
|
onRenderCell={this.onRenderCell}
|
||||||
/>
|
/>
|
||||||
</FocusZone>
|
</FocusZone>
|
||||||
|
) : (
|
||||||
|
<Spinner size={SpinnerSize.large} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private createPolicyViolationsListContent(data: IGalleryItem[]): JSX.Element {
|
private createPolicyViolationsListContent(data: IGalleryItem[]): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<table>
|
<table style={{ margin: 10 }}>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
@@ -385,6 +483,10 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.sampleNotebooks = response.data;
|
this.sampleNotebooks = response.data;
|
||||||
|
|
||||||
|
trace(Action.NotebooksGalleryOfficialSamplesCount, ActionModifiers.Mark, {
|
||||||
|
count: this.sampleNotebooks?.length,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "GalleryViewerComponent/loadSampleNotebooks", "Failed to load sample notebooks");
|
handleError(error, "GalleryViewerComponent/loadSampleNotebooks", "Failed to load sample notebooks");
|
||||||
}
|
}
|
||||||
@@ -411,6 +513,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||||
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
|
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trace(Action.NotebooksGalleryPublicGalleryCount, ActionModifiers.Mark, { count: this.publicNotebooks?.length });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "GalleryViewerComponent/loadPublicNotebooks", "Failed to load public notebooks");
|
handleError(error, "GalleryViewerComponent/loadPublicNotebooks", "Failed to load public notebooks");
|
||||||
}
|
}
|
||||||
@@ -425,14 +529,19 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
private async loadFavoriteNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
private async loadFavoriteNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||||
if (!offline) {
|
if (!offline) {
|
||||||
try {
|
try {
|
||||||
|
this.setState({ isFetchingFavouriteNotebooks: true });
|
||||||
const response = await this.props.junoClient.getFavoriteNotebooks();
|
const response = await this.props.junoClient.getFavoriteNotebooks();
|
||||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||||
throw new Error(`Received HTTP ${response.status} when loading favorite notebooks`);
|
throw new Error(`Received HTTP ${response.status} when loading favorite notebooks`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.favoriteNotebooks = response.data;
|
this.favoriteNotebooks = response.data;
|
||||||
|
|
||||||
|
trace(Action.NotebooksGalleryFavoritesCount, ActionModifiers.Mark, { count: this.favoriteNotebooks?.length });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "GalleryViewerComponent/loadFavoriteNotebooks", "Failed to load favorite notebooks");
|
handleError(error, "GalleryViewerComponent/loadFavoriteNotebooks", "Failed to load favorite notebooks");
|
||||||
|
} finally {
|
||||||
|
this.setState({ isFetchingFavouriteNotebooks: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -451,14 +560,25 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
private async loadPublishedNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
private async loadPublishedNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||||
if (!offline) {
|
if (!offline) {
|
||||||
try {
|
try {
|
||||||
|
this.setState({ isFetchingPublishedNotebooks: true });
|
||||||
const response = await this.props.junoClient.getPublishedNotebooks();
|
const response = await this.props.junoClient.getPublishedNotebooks();
|
||||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||||
throw new Error(`Received HTTP ${response.status} when loading published notebooks`);
|
throw new Error(`Received HTTP ${response.status} when loading published notebooks`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.publishedNotebooks = response.data;
|
this.publishedNotebooks = response.data;
|
||||||
|
|
||||||
|
const { published, underReview, removed } = GalleryUtils.filterPublishedNotebooks(this.publishedNotebooks);
|
||||||
|
trace(Action.NotebooksGalleryPublishedCount, ActionModifiers.Mark, {
|
||||||
|
count: this.publishedNotebooks?.length,
|
||||||
|
publishedCount: published.length,
|
||||||
|
underReviewCount: underReview.length,
|
||||||
|
removedCount: removed.length,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, "GalleryViewerComponent/loadPublishedNotebooks", "Failed to load published notebooks");
|
handleError(error, "GalleryViewerComponent/loadPublishedNotebooks", "Failed to load published notebooks");
|
||||||
|
} finally {
|
||||||
|
this.setState({ isFetchingPublishedNotebooks: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,35 +17,28 @@ exports[`CodeOfConductComponent renders 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement
|
Azure Cosmos DB Notebook Gallery - Code of Conduct
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<Text>
|
<Text>
|
||||||
Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.
|
The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<Text>
|
<Text>
|
||||||
In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the
|
In order to view and publish your samples to the gallery, you must accept the
|
||||||
<StyledLinkBase
|
<StyledLinkBase
|
||||||
href="https://aka.ms/cosmos-code-of-conduct"
|
href="https://aka.ms/cosmos-code-of-conduct"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
code of conduct
|
code of conduct.
|
||||||
</StyledLinkBase>
|
|
||||||
and
|
|
||||||
<StyledLinkBase
|
|
||||||
href="https://aka.ms/ms-privacy-policy"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
privacy statement
|
|
||||||
</StyledLinkBase>
|
</StyledLinkBase>
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
label="I have read and accepted the code of conduct and privacy statement"
|
label="I have read and accept the code of conduct."
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
|
|||||||
@@ -77,24 +77,11 @@ exports[`GalleryViewerComponent renders 1`] = `
|
|||||||
selectedKey={0}
|
selectedKey={0}
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
|
||||||
<InfoComponent />
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<FocusZone
|
<StyledSpinnerBase
|
||||||
direction={2}
|
size={3}
|
||||||
isCircularNavigation={false}
|
|
||||||
shouldRaiseClicks={true}
|
|
||||||
>
|
|
||||||
<List
|
|
||||||
getPageSpecification={[Function]}
|
|
||||||
onRenderCell={[Function]}
|
|
||||||
renderedWindowsAhead={3}
|
|
||||||
renderedWindowsBehind={2}
|
|
||||||
startIndex={0}
|
|
||||||
/>
|
/>
|
||||||
</FocusZone>
|
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
|
|||||||
@@ -31,6 +31,26 @@ export interface NotebookMetadataComponentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> {
|
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> {
|
||||||
|
private renderFavouriteButton = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
{this.props.isFavorite !== undefined ? (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }}
|
||||||
|
onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick}
|
||||||
|
/>
|
||||||
|
{this.props.data.favorites} likes
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon iconName="Heart" /> {this.props.data.favorites} likes
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const options: Intl.DateTimeFormatOptions = {
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
@@ -49,19 +69,7 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
|
|||||||
</Text>
|
</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
|
||||||
<Stack.Item>
|
<Stack.Item>{this.renderFavouriteButton()}</Stack.Item>
|
||||||
<Text>
|
|
||||||
{this.props.isFavorite !== undefined && (
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
iconProps={{ iconName: this.props.isFavorite ? "HeartFill" : "Heart" }}
|
|
||||||
onClick={this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick}
|
|
||||||
/>
|
|
||||||
{this.props.data.favorites} likes
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</Stack.Item>
|
|
||||||
|
|
||||||
{this.props.downloadButtonText && (
|
{this.props.downloadButtonText && (
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
|
|||||||
@@ -3,14 +3,11 @@
|
|||||||
*/
|
*/
|
||||||
import { Notebook } from "@nteract/commutable";
|
import { Notebook } from "@nteract/commutable";
|
||||||
import { createContentRef } from "@nteract/core";
|
import { createContentRef } from "@nteract/core";
|
||||||
import { IChoiceGroupProps, Icon, Link, ProgressIndicator } from "office-ui-fabric-react";
|
import { IChoiceGroupProps, Icon, IProgressIndicatorProps, Link, ProgressIndicator } from "office-ui-fabric-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { contents } from "rx-jupyter";
|
import { contents } from "rx-jupyter";
|
||||||
import * as Logger from "../../../Common/Logger";
|
|
||||||
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
|
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
|
||||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
|
||||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
|
||||||
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
|
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
|
||||||
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
||||||
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
|
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
|
||||||
@@ -21,7 +18,9 @@ import Explorer from "../../Explorer";
|
|||||||
import { NotebookV4 } from "@nteract/commutable/lib/v4";
|
import { NotebookV4 } from "@nteract/commutable/lib/v4";
|
||||||
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
|
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
|
||||||
import { DialogHost } from "../../../Utils/GalleryUtils";
|
import { DialogHost } from "../../../Utils/GalleryUtils";
|
||||||
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
|
||||||
|
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
|
||||||
export interface NotebookViewerComponentProps {
|
export interface NotebookViewerComponentProps {
|
||||||
container?: Explorer;
|
container?: Explorer;
|
||||||
@@ -80,6 +79,12 @@ export class NotebookViewerComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async loadNotebookContent(): Promise<void> {
|
private async loadNotebookContent(): Promise<void> {
|
||||||
|
const startKey = traceStart(Action.NotebooksGalleryViewNotebook, {
|
||||||
|
notebookUrl: this.props.notebookUrl,
|
||||||
|
notebookId: this.props.galleryItem?.id,
|
||||||
|
isSample: this.props.galleryItem?.isSample,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(this.props.notebookUrl);
|
const response = await fetch(this.props.notebookUrl);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -87,6 +92,16 @@ export class NotebookViewerComponent
|
|||||||
throw new Error(`Received HTTP ${response.status} while fetching ${this.props.notebookUrl}`);
|
throw new Error(`Received HTTP ${response.status} while fetching ${this.props.notebookUrl}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
traceSuccess(
|
||||||
|
Action.NotebooksGalleryViewNotebook,
|
||||||
|
{
|
||||||
|
notebookUrl: this.props.notebookUrl,
|
||||||
|
notebookId: this.props.galleryItem?.id,
|
||||||
|
isSample: this.props.galleryItem?.isSample,
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
|
||||||
const notebook: Notebook = await response.json();
|
const notebook: Notebook = await response.json();
|
||||||
this.removeNotebookViewerLink(notebook, this.props.galleryItem?.newCellId);
|
this.removeNotebookViewerLink(notebook, this.props.galleryItem?.newCellId);
|
||||||
this.notebookComponentBootstrapper.setContent("json", notebook);
|
this.notebookComponentBootstrapper.setContent("json", notebook);
|
||||||
@@ -101,6 +116,18 @@ export class NotebookViewerComponent
|
|||||||
SessionStorageUtility.setEntry(this.props.galleryItem?.id, "true");
|
SessionStorageUtility.setEntry(this.props.galleryItem?.id, "true");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
traceFailure(
|
||||||
|
Action.NotebooksGalleryViewNotebook,
|
||||||
|
{
|
||||||
|
notebookUrl: this.props.notebookUrl,
|
||||||
|
notebookId: this.props.galleryItem?.id,
|
||||||
|
isSample: this.props.galleryItem?.isSample,
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
errorStack: getErrorStack(error),
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
|
||||||
this.setState({ showProgressBar: false });
|
this.setState({ showProgressBar: false });
|
||||||
handleError(error, "NotebookViewerComponent/loadNotebookContent", "Failed to load notebook content");
|
handleError(error, "NotebookViewerComponent/loadNotebookContent", "Failed to load notebook content");
|
||||||
}
|
}
|
||||||
@@ -178,6 +205,32 @@ export class NotebookViewerComponent
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DialogHost
|
||||||
|
showOkModalDialog(
|
||||||
|
title: string,
|
||||||
|
msg: string,
|
||||||
|
okLabel: string,
|
||||||
|
onOk: () => void,
|
||||||
|
progressIndicatorProps?: IProgressIndicatorProps
|
||||||
|
): void {
|
||||||
|
this.setState({
|
||||||
|
dialogProps: {
|
||||||
|
isModal: true,
|
||||||
|
visible: true,
|
||||||
|
title,
|
||||||
|
subText: msg,
|
||||||
|
primaryButtonText: okLabel,
|
||||||
|
onPrimaryButtonClick: () => {
|
||||||
|
this.setState({ dialogProps: undefined });
|
||||||
|
onOk && onOk();
|
||||||
|
},
|
||||||
|
secondaryButtonText: undefined,
|
||||||
|
onSecondaryButtonClick: undefined,
|
||||||
|
progressIndicatorProps,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// DialogHost
|
// DialogHost
|
||||||
showOkCancelModalDialog(
|
showOkCancelModalDialog(
|
||||||
title: string,
|
title: string,
|
||||||
@@ -186,8 +239,10 @@ export class NotebookViewerComponent
|
|||||||
onOk: () => void,
|
onOk: () => void,
|
||||||
cancelLabel: string,
|
cancelLabel: string,
|
||||||
onCancel: () => void,
|
onCancel: () => void,
|
||||||
|
progressIndicatorProps?: IProgressIndicatorProps,
|
||||||
choiceGroupProps?: IChoiceGroupProps,
|
choiceGroupProps?: IChoiceGroupProps,
|
||||||
textFieldProps?: TextFieldProps
|
textFieldProps?: TextFieldProps,
|
||||||
|
primaryButtonDisabled?: boolean
|
||||||
): void {
|
): void {
|
||||||
this.setState({
|
this.setState({
|
||||||
dialogProps: {
|
dialogProps: {
|
||||||
@@ -205,8 +260,10 @@ export class NotebookViewerComponent
|
|||||||
this.setState({ dialogProps: undefined });
|
this.setState({ dialogProps: undefined });
|
||||||
onCancel && onCancel();
|
onCancel && onCancel();
|
||||||
},
|
},
|
||||||
|
progressIndicatorProps,
|
||||||
choiceGroupProps,
|
choiceGroupProps,
|
||||||
textFieldProps,
|
textFieldProps,
|
||||||
|
primaryButtonDisabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { shallow } from "enzyme";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { SettingsComponentProps, SettingsComponent, SettingsComponentState } from "./SettingsComponent";
|
import { SettingsComponentProps, SettingsComponent, SettingsComponentState } from "./SettingsComponent";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import SettingsTabV2 from "../../Tabs/SettingsTabV2";
|
import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||||
import { collection } from "./TestUtils";
|
import { collection } from "./TestUtils";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
@@ -31,25 +31,21 @@ 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),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("SettingsComponent", () => {
|
describe("SettingsComponent", () => {
|
||||||
const baseProps: SettingsComponentProps = {
|
const baseProps: SettingsComponentProps = {
|
||||||
settingsTab: new SettingsTabV2({
|
settingsTab: new CollectionSettingsTabV2({
|
||||||
collection: collection,
|
collection: collection,
|
||||||
tabKind: ViewModels.CollectionTabKind.SettingsV2,
|
tabKind: ViewModels.CollectionTabKind.CollectionSettingsV2,
|
||||||
title: "Scale & Settings",
|
title: "Scale & Settings",
|
||||||
tabPath: "",
|
tabPath: "",
|
||||||
node: undefined,
|
node: undefined,
|
||||||
hashLocation: "settings",
|
hashLocation: "settings",
|
||||||
isActive: ko.observable(false),
|
isActive: ko.observable(false),
|
||||||
onUpdateTabsButtons: undefined,
|
onUpdateTabsButtons: undefined,
|
||||||
getPendingNotification: Q.Promise<DataModels.Notification>(() => {
|
|
||||||
return;
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,6 +138,7 @@ describe("SettingsComponent", () => {
|
|||||||
readSettings: undefined,
|
readSettings: undefined,
|
||||||
onSettingsClick: undefined,
|
onSettingsClick: undefined,
|
||||||
loadOffer: undefined,
|
loadOffer: undefined,
|
||||||
|
getPendingThroughputSplitNotification: undefined,
|
||||||
} as ViewModels.Database;
|
} as ViewModels.Database;
|
||||||
newCollection.getDatabase = () => newDatabase;
|
newCollection.getDatabase = () => newDatabase;
|
||||||
newCollection.offer = ko.observable(undefined);
|
newCollection.offer = ko.observable(undefined);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import Explorer from "../../Explorer";
|
|||||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||||
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
import SettingsTab from "../../Tabs/SettingsTabV2";
|
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
||||||
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||||
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
||||||
import {
|
import {
|
||||||
@@ -58,7 +58,7 @@ interface ButtonV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsComponentProps {
|
export interface SettingsComponentProps {
|
||||||
settingsTab: SettingsTab;
|
settingsTab: SettingsTabV2;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsComponentState {
|
export interface SettingsComponentState {
|
||||||
@@ -116,7 +116,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
private discardSettingsChangesButton: ButtonV2;
|
private discardSettingsChangesButton: ButtonV2;
|
||||||
|
|
||||||
private isAnalyticalStorageEnabled: boolean;
|
private isAnalyticalStorageEnabled: boolean;
|
||||||
|
private isCollectionSettingsTab: boolean;
|
||||||
private collection: ViewModels.Collection;
|
private collection: ViewModels.Collection;
|
||||||
|
private database: ViewModels.Database;
|
||||||
|
private offer: DataModels.Offer;
|
||||||
private container: Explorer;
|
private container: Explorer;
|
||||||
private changeFeedPolicyVisible: boolean;
|
private changeFeedPolicyVisible: boolean;
|
||||||
private isFixedContainer: boolean;
|
private isFixedContainer: boolean;
|
||||||
@@ -126,8 +129,11 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
constructor(props: SettingsComponentProps) {
|
constructor(props: SettingsComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
this.isCollectionSettingsTab = this.props.settingsTab.tabKind === ViewModels.CollectionTabKind.CollectionSettingsV2;
|
||||||
|
if (this.isCollectionSettingsTab) {
|
||||||
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
|
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
|
||||||
this.container = this.collection?.container;
|
this.container = this.collection?.container;
|
||||||
|
this.offer = this.collection?.offer();
|
||||||
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
||||||
this.shouldShowIndexingPolicyEditor =
|
this.shouldShowIndexingPolicyEditor =
|
||||||
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB();
|
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB();
|
||||||
@@ -139,7 +145,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
// Mongo container with system partition key still treat as "Fixed"
|
// Mongo container with system partition key still treat as "Fixed"
|
||||||
this.isFixedContainer =
|
this.isFixedContainer =
|
||||||
this.container.isPreferredApiMongoDB() &&
|
this.container.isPreferredApiMongoDB() &&
|
||||||
(!this.collection.partitionKey || this.collection.partitionKey.systemKey);
|
(!this.collection?.partitionKey || this.collection?.partitionKey.systemKey);
|
||||||
|
} else {
|
||||||
|
this.database = this.props.settingsTab.database;
|
||||||
|
this.container = this.database?.container;
|
||||||
|
this.offer = this.database?.offer();
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
throughput: undefined,
|
throughput: undefined,
|
||||||
@@ -206,18 +217,21 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
|
if (this.isCollectionSettingsTab) {
|
||||||
this.refreshIndexTransformationProgress();
|
this.refreshIndexTransformationProgress();
|
||||||
this.loadMongoIndexes();
|
this.loadMongoIndexes();
|
||||||
|
}
|
||||||
|
|
||||||
this.setAutoPilotStates();
|
this.setAutoPilotStates();
|
||||||
this.setBaseline();
|
this.setBaseline();
|
||||||
if (this.props.settingsTab.isActive()) {
|
if (this.props.settingsTab.isActive()) {
|
||||||
this.props.settingsTab.getSettingsTabContainer().onUpdateTabsButtons(this.getTabsButtons());
|
this.props.settingsTab.getContainer().onUpdateTabsButtons(this.getTabsButtons());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(): void {
|
componentDidUpdate(): void {
|
||||||
if (this.props.settingsTab.isActive()) {
|
if (this.props.settingsTab.isActive()) {
|
||||||
this.props.settingsTab.getSettingsTabContainer().onUpdateTabsButtons(this.getTabsButtons());
|
this.props.settingsTab.getContainer().onUpdateTabsButtons(this.getTabsButtons());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +284,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
};
|
};
|
||||||
|
|
||||||
private setAutoPilotStates = (): void => {
|
private setAutoPilotStates = (): void => {
|
||||||
const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput;
|
const autoscaleMaxThroughput = this.offer?.autoscaleMaxThroughput;
|
||||||
|
|
||||||
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -295,7 +309,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
!!this.collection.conflictResolutionPolicy();
|
!!this.collection.conflictResolutionPolicy();
|
||||||
|
|
||||||
public isOfferReplacePending = (): boolean => {
|
public isOfferReplacePending = (): boolean => {
|
||||||
return this.collection?.offer()?.offerReplacePending;
|
return this.offer?.offerReplacePending;
|
||||||
};
|
};
|
||||||
|
|
||||||
public onSaveClick = async (): Promise<void> => {
|
public onSaveClick = async (): Promise<void> => {
|
||||||
@@ -309,174 +323,10 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
tabTitle: this.props.settingsTab.tabTitle(),
|
tabTitle: this.props.settingsTab.tabTitle(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (
|
await (this.isCollectionSettingsTab
|
||||||
this.state.isSubSettingsSaveable ||
|
? this.saveCollectionSettings(startKey)
|
||||||
this.state.isIndexingPolicyDirty ||
|
: this.saveDatabaseSettings(startKey));
|
||||||
this.state.isConflictResolutionDirty
|
|
||||||
) {
|
|
||||||
let defaultTtl: number;
|
|
||||||
switch (this.state.timeToLive) {
|
|
||||||
case TtlType.On:
|
|
||||||
defaultTtl = Number(this.state.timeToLiveSeconds);
|
|
||||||
break;
|
|
||||||
case TtlType.OnNoDefault:
|
|
||||||
defaultTtl = -1;
|
|
||||||
break;
|
|
||||||
case TtlType.Off:
|
|
||||||
default:
|
|
||||||
defaultTtl = undefined;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
|
|
||||||
newCollection.defaultTtl = defaultTtl;
|
|
||||||
|
|
||||||
newCollection.indexingPolicy = this.state.indexingPolicyContent;
|
|
||||||
|
|
||||||
newCollection.changeFeedPolicy =
|
|
||||||
this.changeFeedPolicyVisible && this.state.changeFeedPolicy === ChangeFeedPolicyState.On
|
|
||||||
? {
|
|
||||||
retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
newCollection.analyticalStorageTtl = this.getAnalyticalStorageTtl();
|
|
||||||
|
|
||||||
newCollection.geospatialConfig = {
|
|
||||||
type: this.state.geospatialConfigType,
|
|
||||||
};
|
|
||||||
|
|
||||||
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
|
|
||||||
if (conflictResolutionChanges) {
|
|
||||||
newCollection.conflictResolutionPolicy = conflictResolutionChanges;
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedCollection: DataModels.Collection = await updateCollection(
|
|
||||||
this.collection.databaseId,
|
|
||||||
this.collection.id(),
|
|
||||||
newCollection
|
|
||||||
);
|
|
||||||
this.collection.rawDataModel = updatedCollection;
|
|
||||||
this.collection.defaultTtl(updatedCollection.defaultTtl);
|
|
||||||
this.collection.analyticalStorageTtl(updatedCollection.analyticalStorageTtl);
|
|
||||||
this.collection.id(updatedCollection.id);
|
|
||||||
this.collection.indexingPolicy(updatedCollection.indexingPolicy);
|
|
||||||
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
|
|
||||||
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
|
||||||
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
|
||||||
|
|
||||||
if (wasIndexingPolicyModified) {
|
|
||||||
await this.refreshIndexTransformationProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isSubSettingsSaveable: false,
|
|
||||||
isSubSettingsDiscardable: false,
|
|
||||||
isIndexingPolicyDirty: false,
|
|
||||||
isConflictResolutionDirty: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) {
|
|
||||||
try {
|
|
||||||
const newMongoIndexes = this.getMongoIndexesToSave();
|
|
||||||
const newMongoCollection: MongoDBCollectionResource = {
|
|
||||||
...this.mongoDBCollectionResource,
|
|
||||||
indexes: newMongoIndexes,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP(
|
|
||||||
this.collection.databaseId,
|
|
||||||
this.collection.id(),
|
|
||||||
newMongoCollection
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.refreshIndexTransformationProgress();
|
|
||||||
this.setState({
|
|
||||||
isMongoIndexingPolicySaveable: false,
|
|
||||||
indexesToDrop: [],
|
|
||||||
indexesToAdd: [],
|
|
||||||
currentMongoIndexes: [...this.mongoDBCollectionResource.indexes],
|
|
||||||
});
|
|
||||||
traceSuccess(
|
|
||||||
Action.MongoIndexUpdated,
|
|
||||||
{
|
|
||||||
databaseAccountName: this.container.databaseAccount()?.name,
|
|
||||||
databaseName: this.collection?.databaseId,
|
|
||||||
collectionName: this.collection?.id(),
|
|
||||||
defaultExperience: this.container.defaultExperience(),
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.props.settingsTab.tabTitle(),
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
traceFailure(
|
|
||||||
Action.MongoIndexUpdated,
|
|
||||||
{
|
|
||||||
databaseAccountName: this.container.databaseAccount()?.name,
|
|
||||||
databaseName: this.collection?.databaseId,
|
|
||||||
collectionName: this.collection?.id(),
|
|
||||||
defaultExperience: this.container.defaultExperience(),
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.props.settingsTab.tabTitle(),
|
|
||||||
error: getErrorMessage(error),
|
|
||||||
errorStack: getErrorStack(error),
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.isScaleSaveable) {
|
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
|
||||||
databaseId: this.collection.databaseId,
|
|
||||||
collectionId: this.collection.id(),
|
|
||||||
currentOffer: this.collection.offer(),
|
|
||||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
|
||||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput,
|
|
||||||
};
|
|
||||||
if (this.hasProvisioningTypeChanged()) {
|
|
||||||
if (this.state.isAutoPilotSelected) {
|
|
||||||
updateOfferParams.migrateToAutoPilot = true;
|
|
||||||
} else {
|
|
||||||
updateOfferParams.migrateToManual = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
|
||||||
this.collection.offer(updatedOffer);
|
|
||||||
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
|
||||||
if (this.state.isAutoPilotSelected) {
|
|
||||||
this.setState({
|
|
||||||
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
|
|
||||||
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
throughput: updatedOffer.manualThroughput,
|
|
||||||
throughputBaseline: updatedOffer.manualThroughput,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.container.isRefreshingExplorer(false);
|
|
||||||
this.setBaseline();
|
|
||||||
this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected });
|
|
||||||
traceSuccess(
|
|
||||||
Action.SettingsV2Updated,
|
|
||||||
{
|
|
||||||
databaseAccountName: this.container.databaseAccount()?.name,
|
|
||||||
databaseName: this.collection?.databaseId,
|
|
||||||
collectionName: this.collection?.id(),
|
|
||||||
defaultExperience: this.container.defaultExperience(),
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.props.settingsTab.tabTitle(),
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.container.isRefreshingExplorer(false);
|
this.container.isRefreshingExplorer(false);
|
||||||
this.props.settingsTab.isExecutionError(true);
|
this.props.settingsTab.isExecutionError(true);
|
||||||
@@ -495,8 +345,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
},
|
},
|
||||||
startKey
|
startKey
|
||||||
);
|
);
|
||||||
}
|
} finally {
|
||||||
this.props.settingsTab.isExecuting(false);
|
this.props.settingsTab.isExecuting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public onRevertClick = (): void => {
|
public onRevertClick = (): void => {
|
||||||
@@ -693,6 +544,17 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
};
|
};
|
||||||
|
|
||||||
public setBaseline = (): void => {
|
public setBaseline = (): void => {
|
||||||
|
const offerThroughput = this.offer?.manualThroughput;
|
||||||
|
|
||||||
|
if (!this.isCollectionSettingsTab) {
|
||||||
|
this.setState({
|
||||||
|
throughput: offerThroughput,
|
||||||
|
throughputBaseline: offerThroughput,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const defaultTtl = this.collection.defaultTtl();
|
const defaultTtl = this.collection.defaultTtl();
|
||||||
|
|
||||||
let timeToLive: TtlType = this.state.timeToLive;
|
let timeToLive: TtlType = this.state.timeToLive;
|
||||||
@@ -725,7 +587,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const offerThroughput = this.collection.offer()?.manualThroughput;
|
|
||||||
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
const changeFeedPolicy = this.collection.rawDataModel?.changeFeedPolicy
|
||||||
? ChangeFeedPolicyState.On
|
? ChangeFeedPolicyState.On
|
||||||
: ChangeFeedPolicyState.Off;
|
: ChangeFeedPolicyState.Off;
|
||||||
@@ -811,9 +672,225 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
this.setState({ selectedTab: selectedTab });
|
this.setState({ selectedTab: selectedTab });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private saveDatabaseSettings = async (startKey: number): Promise<void> => {
|
||||||
|
if (this.state.isScaleSaveable) {
|
||||||
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
|
databaseId: this.database.id(),
|
||||||
|
currentOffer: this.database.offer(),
|
||||||
|
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||||
|
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput,
|
||||||
|
};
|
||||||
|
if (this.hasProvisioningTypeChanged()) {
|
||||||
|
if (this.state.isAutoPilotSelected) {
|
||||||
|
updateOfferParams.migrateToAutoPilot = true;
|
||||||
|
} else {
|
||||||
|
updateOfferParams.migrateToManual = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||||
|
this.database.offer(updatedOffer);
|
||||||
|
this.offer = updatedOffer;
|
||||||
|
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||||
|
if (this.state.isAutoPilotSelected) {
|
||||||
|
this.setState({
|
||||||
|
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
|
||||||
|
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
throughput: updatedOffer.manualThroughput,
|
||||||
|
throughputBaseline: updatedOffer.manualThroughput,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container.isRefreshingExplorer(false);
|
||||||
|
this.setBaseline();
|
||||||
|
this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected });
|
||||||
|
traceSuccess(
|
||||||
|
Action.SettingsV2Updated,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.container.databaseAccount()?.name,
|
||||||
|
databaseName: this.database.id(),
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
|
tabTitle: this.props.settingsTab.tabTitle(),
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private saveCollectionSettings = async (startKey: number): Promise<void> => {
|
||||||
|
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
|
||||||
|
|
||||||
|
if (this.state.isSubSettingsSaveable || this.state.isIndexingPolicyDirty || this.state.isConflictResolutionDirty) {
|
||||||
|
let defaultTtl: number;
|
||||||
|
switch (this.state.timeToLive) {
|
||||||
|
case TtlType.On:
|
||||||
|
defaultTtl = Number(this.state.timeToLiveSeconds);
|
||||||
|
break;
|
||||||
|
case TtlType.OnNoDefault:
|
||||||
|
defaultTtl = -1;
|
||||||
|
break;
|
||||||
|
case TtlType.Off:
|
||||||
|
default:
|
||||||
|
defaultTtl = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wasIndexingPolicyModified = this.state.isIndexingPolicyDirty;
|
||||||
|
newCollection.defaultTtl = defaultTtl;
|
||||||
|
|
||||||
|
newCollection.indexingPolicy = this.state.indexingPolicyContent;
|
||||||
|
|
||||||
|
newCollection.changeFeedPolicy =
|
||||||
|
this.changeFeedPolicyVisible && this.state.changeFeedPolicy === ChangeFeedPolicyState.On
|
||||||
|
? {
|
||||||
|
retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
newCollection.analyticalStorageTtl = this.getAnalyticalStorageTtl();
|
||||||
|
|
||||||
|
newCollection.geospatialConfig = {
|
||||||
|
type: this.state.geospatialConfigType,
|
||||||
|
};
|
||||||
|
|
||||||
|
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
|
||||||
|
if (conflictResolutionChanges) {
|
||||||
|
newCollection.conflictResolutionPolicy = conflictResolutionChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedCollection: DataModels.Collection = await updateCollection(
|
||||||
|
this.collection.databaseId,
|
||||||
|
this.collection.id(),
|
||||||
|
newCollection
|
||||||
|
);
|
||||||
|
this.collection.rawDataModel = updatedCollection;
|
||||||
|
this.collection.defaultTtl(updatedCollection.defaultTtl);
|
||||||
|
this.collection.analyticalStorageTtl(updatedCollection.analyticalStorageTtl);
|
||||||
|
this.collection.id(updatedCollection.id);
|
||||||
|
this.collection.indexingPolicy(updatedCollection.indexingPolicy);
|
||||||
|
this.collection.conflictResolutionPolicy(updatedCollection.conflictResolutionPolicy);
|
||||||
|
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
||||||
|
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
||||||
|
|
||||||
|
if (wasIndexingPolicyModified) {
|
||||||
|
await this.refreshIndexTransformationProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
isSubSettingsSaveable: false,
|
||||||
|
isSubSettingsDiscardable: false,
|
||||||
|
isIndexingPolicyDirty: false,
|
||||||
|
isConflictResolutionDirty: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.isMongoIndexingPolicySaveable && this.mongoDBCollectionResource) {
|
||||||
|
try {
|
||||||
|
const newMongoIndexes = this.getMongoIndexesToSave();
|
||||||
|
const newMongoCollection: MongoDBCollectionResource = {
|
||||||
|
...this.mongoDBCollectionResource,
|
||||||
|
indexes: newMongoIndexes,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP(
|
||||||
|
this.collection.databaseId,
|
||||||
|
this.collection.id(),
|
||||||
|
newMongoCollection
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.refreshIndexTransformationProgress();
|
||||||
|
this.setState({
|
||||||
|
isMongoIndexingPolicySaveable: false,
|
||||||
|
indexesToDrop: [],
|
||||||
|
indexesToAdd: [],
|
||||||
|
currentMongoIndexes: [...this.mongoDBCollectionResource.indexes],
|
||||||
|
});
|
||||||
|
traceSuccess(
|
||||||
|
Action.MongoIndexUpdated,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.container.databaseAccount()?.name,
|
||||||
|
databaseName: this.collection?.databaseId,
|
||||||
|
collectionName: this.collection?.id(),
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
|
tabTitle: this.props.settingsTab.tabTitle(),
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
traceFailure(
|
||||||
|
Action.MongoIndexUpdated,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.container.databaseAccount()?.name,
|
||||||
|
databaseName: this.collection?.databaseId,
|
||||||
|
collectionName: this.collection?.id(),
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
|
tabTitle: this.props.settingsTab.tabTitle(),
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
errorStack: getErrorStack(error),
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.isScaleSaveable) {
|
||||||
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
|
databaseId: this.collection.databaseId,
|
||||||
|
collectionId: this.collection.id(),
|
||||||
|
currentOffer: this.collection.offer(),
|
||||||
|
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||||
|
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput,
|
||||||
|
};
|
||||||
|
if (this.hasProvisioningTypeChanged()) {
|
||||||
|
if (this.state.isAutoPilotSelected) {
|
||||||
|
updateOfferParams.migrateToAutoPilot = true;
|
||||||
|
} else {
|
||||||
|
updateOfferParams.migrateToManual = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updatedOffer: DataModels.Offer = await updateOffer(updateOfferParams);
|
||||||
|
this.collection.offer(updatedOffer);
|
||||||
|
this.offer = updatedOffer;
|
||||||
|
this.setState({ isScaleSaveable: false, isScaleDiscardable: false });
|
||||||
|
if (this.state.isAutoPilotSelected) {
|
||||||
|
this.setState({
|
||||||
|
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
|
||||||
|
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
throughput: updatedOffer.manualThroughput,
|
||||||
|
throughputBaseline: updatedOffer.manualThroughput,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.container.isRefreshingExplorer(false);
|
||||||
|
this.setBaseline();
|
||||||
|
this.setState({ wasAutopilotOriginallySet: this.state.isAutoPilotSelected });
|
||||||
|
traceSuccess(
|
||||||
|
Action.SettingsV2Updated,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.container.databaseAccount()?.name,
|
||||||
|
databaseName: this.collection?.databaseId,
|
||||||
|
collectionName: this.collection?.id(),
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
|
tabTitle: this.props.settingsTab.tabTitle(),
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const scaleComponentProps: ScaleComponentProps = {
|
const scaleComponentProps: ScaleComponentProps = {
|
||||||
collection: this.collection,
|
collection: this.collection,
|
||||||
|
database: this.database,
|
||||||
container: this.container,
|
container: this.container,
|
||||||
isFixedContainer: this.isFixedContainer,
|
isFixedContainer: this.isFixedContainer,
|
||||||
onThroughputChange: this.onThroughputChange,
|
onThroughputChange: this.onThroughputChange,
|
||||||
@@ -830,6 +907,16 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
initialNotification: this.props.settingsTab.pendingNotification(),
|
initialNotification: this.props.settingsTab.pendingNotification(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!this.isCollectionSettingsTab) {
|
||||||
|
return (
|
||||||
|
<div className="settingsV2MainContainer">
|
||||||
|
<div className="settingsV2TabsContainer">
|
||||||
|
<ScaleComponent {...scaleComponentProps} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const subSettingsComponentProps: SubSettingsComponentProps = {
|
const subSettingsComponentProps: SubSettingsComponentProps = {
|
||||||
collection: this.collection,
|
collection: this.collection,
|
||||||
container: this.container,
|
container: this.container,
|
||||||
@@ -899,7 +986,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
};
|
};
|
||||||
|
|
||||||
const tabs: SettingsV2TabInfo[] = [];
|
const tabs: SettingsV2TabInfo[] = [];
|
||||||
if (!hasDatabaseSharedThroughput(this.collection) && this.collection.offer()) {
|
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
tab: SettingsV2TabTypes.ScaleTab,
|
tab: SettingsV2TabTypes.ScaleTab,
|
||||||
content: <ScaleComponent {...scaleComponentProps} />,
|
content: <ScaleComponent {...scaleComponentProps} />,
|
||||||
|
|||||||
@@ -375,7 +375,7 @@ export const getThroughputApplyShortDelayMessage = (
|
|||||||
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
|
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
|
||||||
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
||||||
<br />
|
<br />
|
||||||
Database: {databaseName}, Container: {collectionName}{" "}
|
{collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `}
|
||||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
|
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
@@ -392,7 +392,7 @@ export const getThroughputApplyLongDelayMessage = (
|
|||||||
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to
|
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to
|
||||||
complete. View the latest status in Notifications.
|
complete. View the latest status in Notifications.
|
||||||
<br />
|
<br />
|
||||||
Database: {databaseName}, Container: {collectionName}{" "}
|
{collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `}
|
||||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
|
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ describe("ScaleComponent", () => {
|
|||||||
|
|
||||||
const baseProps: ScaleComponentProps = {
|
const baseProps: ScaleComponentProps = {
|
||||||
collection: collection,
|
collection: collection,
|
||||||
|
database: undefined,
|
||||||
container: container,
|
container: container,
|
||||||
isFixedContainer: false,
|
isFixedContainer: false,
|
||||||
onThroughputChange: () => {
|
onThroughputChange: () => {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { configContext, Platform } from "../../../../ConfigContext";
|
|||||||
|
|
||||||
export interface ScaleComponentProps {
|
export interface ScaleComponentProps {
|
||||||
collection: ViewModels.Collection;
|
collection: ViewModels.Collection;
|
||||||
|
database: ViewModels.Database;
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
isFixedContainer: boolean;
|
isFixedContainer: boolean;
|
||||||
onThroughputChange: (newThroughput: number) => void;
|
onThroughputChange: (newThroughput: number) => void;
|
||||||
@@ -39,9 +40,16 @@ export interface ScaleComponentProps {
|
|||||||
|
|
||||||
export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||||
private isEmulator: boolean;
|
private isEmulator: boolean;
|
||||||
|
private offer: DataModels.Offer;
|
||||||
|
private databaseId: string;
|
||||||
|
private collectionId: string;
|
||||||
|
|
||||||
constructor(props: ScaleComponentProps) {
|
constructor(props: ScaleComponentProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.isEmulator = configContext.platform === Platform.Emulator;
|
this.isEmulator = configContext.platform === Platform.Emulator;
|
||||||
|
this.offer = this.props.database?.offer() || this.props.collection?.offer();
|
||||||
|
this.databaseId = this.props.database?.id() || this.props.collection.databaseId;
|
||||||
|
this.collectionId = this.props.collection?.id();
|
||||||
}
|
}
|
||||||
|
|
||||||
public isAutoScaleEnabled = (): boolean => {
|
public isAutoScaleEnabled = (): boolean => {
|
||||||
@@ -87,9 +95,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return this.offer?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||||
this.props.collection.offer()?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public getThroughputTitle = (): string => {
|
public getThroughputTitle = (): string => {
|
||||||
@@ -115,15 +121,14 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
return this.getLongDelayMessage();
|
return this.getLongDelayMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
const offer = this.props.collection?.offer();
|
if (this.offer?.offerReplacePending) {
|
||||||
if (offer?.offerReplacePending) {
|
const throughput = this.offer.manualThroughput || this.offer.autoscaleMaxThroughput;
|
||||||
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
|
||||||
return getThroughputApplyShortDelayMessage(
|
return getThroughputApplyShortDelayMessage(
|
||||||
this.props.isAutoPilotSelected,
|
this.props.isAutoPilotSelected,
|
||||||
throughput,
|
throughput,
|
||||||
throughputUnit,
|
throughputUnit,
|
||||||
this.props.collection.databaseId,
|
this.databaseId,
|
||||||
this.props.collection.id()
|
this.collectionId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +140,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
this.canThroughputExceedMaximumValue() &&
|
this.canThroughputExceedMaximumValue() &&
|
||||||
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||||
|
|
||||||
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
if (throughputExceedsBackendLimits && !this.props.isFixedContainer) {
|
||||||
return updateThroughputBeyondLimitWarningMessage;
|
return updateThroughputBeyondLimitWarningMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,8 +159,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
this.props.wasAutopilotOriginallySet,
|
this.props.wasAutopilotOriginallySet,
|
||||||
throughput,
|
throughput,
|
||||||
throughputUnit,
|
throughputUnit,
|
||||||
this.props.collection.databaseId,
|
this.databaseId,
|
||||||
this.props.collection.id(),
|
this.collectionId,
|
||||||
targetThroughput
|
targetThroughput
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -165,15 +170,15 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
private getThroughputInputComponent = (): JSX.Element => (
|
private getThroughputInputComponent = (): JSX.Element => (
|
||||||
<ThroughputInputAutoPilotV3Component
|
<ThroughputInputAutoPilotV3Component
|
||||||
databaseAccount={this.props.container.databaseAccount()}
|
databaseAccount={this.props.container.databaseAccount()}
|
||||||
databaseName={this.props.collection.databaseId}
|
databaseName={this.databaseId}
|
||||||
collectionName={this.props.collection.id()}
|
collectionName={this.collectionId}
|
||||||
serverId={this.props.container.serverId()}
|
serverId={this.props.container.serverId()}
|
||||||
throughput={this.props.throughput}
|
throughput={this.props.throughput}
|
||||||
throughputBaseline={this.props.throughputBaseline}
|
throughputBaseline={this.props.throughputBaseline}
|
||||||
onThroughputChange={this.props.onThroughputChange}
|
onThroughputChange={this.props.onThroughputChange}
|
||||||
minimum={this.getMinRUs()}
|
minimum={this.getMinRUs()}
|
||||||
maximum={this.getMaxRUs()}
|
maximum={this.getMaxRUs()}
|
||||||
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
isEnabled={!!this.props.database || !hasDatabaseSharedThroughput(this.props.collection)}
|
||||||
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
||||||
label={this.getThroughputTitle()}
|
label={this.getThroughputTitle()}
|
||||||
isEmulator={this.isEmulator}
|
isEmulator={this.isEmulator}
|
||||||
@@ -189,7 +194,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
||||||
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
||||||
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
||||||
usageSizeInKB={this.props.collection.usageSizeInKB()}
|
usageSizeInKB={this.props.collection?.usageSizeInKB()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -230,7 +235,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
{!this.isAutoScaleEnabled() && (
|
{!this.isAutoScaleEnabled() && (
|
||||||
<Stack {...subComponentStackProps}>
|
<Stack {...subComponentStackProps}>
|
||||||
{this.getThroughputInputComponent()}
|
{this.getThroughputInputComponent()}
|
||||||
{this.getStorageCapacityTitle()}
|
{!this.props.database && this.getStorageCapacityTitle()}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import { userContext } from "../../../../../UserContext";
|
|||||||
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
|
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
|
||||||
import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils";
|
import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils";
|
||||||
import { Features } from "../../../../../Common/Constants";
|
import { Features } from "../../../../../Common/Constants";
|
||||||
|
import { minAutoPilotThroughput } from "../../../../../Utils/AutoPilotUtils";
|
||||||
|
|
||||||
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
|
||||||
@@ -541,6 +542,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
||||||
onChange={this.onAutoPilotThroughputChange}
|
onChange={this.onAutoPilotThroughputChange}
|
||||||
|
min={minAutoPilotThroughput}
|
||||||
/>
|
/>
|
||||||
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
||||||
{this.minRUperGBSurvey()}
|
{this.minRUperGBSurvey()}
|
||||||
@@ -579,6 +581,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
|||||||
: this.props.throughput?.toString()
|
: this.props.throughput?.toString()
|
||||||
}
|
}
|
||||||
onChange={this.onThroughputChange}
|
onChange={this.onThroughputChange}
|
||||||
|
min={this.props.minimum}
|
||||||
/>
|
/>
|
||||||
{this.state.exceedFreeTierThroughput && (
|
{this.state.exceedFreeTierThroughput && (
|
||||||
<MessageBar
|
<MessageBar
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
|||||||
id="autopilotInput"
|
id="autopilotInput"
|
||||||
key="auto pilot throughput input"
|
key="auto pilot throughput input"
|
||||||
label="Max RU/s"
|
label="Max RU/s"
|
||||||
|
min={4000}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
required={true}
|
required={true}
|
||||||
step={1000}
|
step={1000}
|
||||||
@@ -260,6 +261,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
|||||||
disabled={false}
|
disabled={false}
|
||||||
id="throughputInput"
|
id="throughputInput"
|
||||||
key="provisioned throughput input"
|
key="provisioned throughput input"
|
||||||
|
min={10000}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
required={true}
|
required={true}
|
||||||
step={100}
|
step={100}
|
||||||
@@ -533,6 +535,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
|||||||
disabled={false}
|
disabled={false}
|
||||||
id="throughputInput"
|
id="throughputInput"
|
||||||
key="provisioned throughput input"
|
key="provisioned throughput input"
|
||||||
|
min={10000}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
required={true}
|
required={true}
|
||||||
step={100}
|
step={100}
|
||||||
|
|||||||
@@ -23,11 +23,7 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
|||||||
>
|
>
|
||||||
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
|
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
|
||||||
<br />
|
<br />
|
||||||
Database:
|
Database: test, Container: test
|
||||||
test
|
|
||||||
, Container:
|
|
||||||
test
|
|
||||||
|
|
||||||
, Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s
|
, Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s
|
||||||
</Text>
|
</Text>
|
||||||
</StyledMessageBarBase>
|
</StyledMessageBarBase>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ describe("SettingsUtils", () => {
|
|||||||
readSettings: undefined,
|
readSettings: undefined,
|
||||||
onSettingsClick: undefined,
|
onSettingsClick: undefined,
|
||||||
loadOffer: undefined,
|
loadOffer: undefined,
|
||||||
|
getPendingThroughputSplitNotification: undefined,
|
||||||
} as ViewModels.Database;
|
} as ViewModels.Database;
|
||||||
};
|
};
|
||||||
newCollection.offer(undefined);
|
newCollection.offer(undefined);
|
||||||
|
|||||||
@@ -804,6 +804,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
},
|
},
|
||||||
"clickHostedAccountSwitch": [Function],
|
"clickHostedAccountSwitch": [Function],
|
||||||
"clickHostedDirectorySwitch": [Function],
|
"clickHostedDirectorySwitch": [Function],
|
||||||
|
"closeSidePanel": undefined,
|
||||||
"collapsedResourceTreeWidth": 36,
|
"collapsedResourceTreeWidth": 36,
|
||||||
"collectionCreationDefaults": Object {
|
"collectionCreationDefaults": Object {
|
||||||
"storage": "100",
|
"storage": "100",
|
||||||
@@ -958,7 +959,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,16 +1018,11 @@ 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],
|
||||||
"onToggleKeyDown": [Function],
|
"onToggleKeyDown": [Function],
|
||||||
|
"openSidePanel": undefined,
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
@@ -1129,6 +1124,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],
|
||||||
@@ -2087,6 +2085,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
},
|
},
|
||||||
"clickHostedAccountSwitch": [Function],
|
"clickHostedAccountSwitch": [Function],
|
||||||
"clickHostedDirectorySwitch": [Function],
|
"clickHostedDirectorySwitch": [Function],
|
||||||
|
"closeSidePanel": undefined,
|
||||||
"collapsedResourceTreeWidth": 36,
|
"collapsedResourceTreeWidth": 36,
|
||||||
"collectionCreationDefaults": Object {
|
"collectionCreationDefaults": Object {
|
||||||
"storage": "100",
|
"storage": "100",
|
||||||
@@ -2241,7 +2240,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,16 +2299,11 @@ 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],
|
||||||
"onToggleKeyDown": [Function],
|
"onToggleKeyDown": [Function],
|
||||||
|
"openSidePanel": undefined,
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
@@ -2412,6 +2405,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],
|
||||||
@@ -3383,6 +3379,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
},
|
},
|
||||||
"clickHostedAccountSwitch": [Function],
|
"clickHostedAccountSwitch": [Function],
|
||||||
"clickHostedDirectorySwitch": [Function],
|
"clickHostedDirectorySwitch": [Function],
|
||||||
|
"closeSidePanel": undefined,
|
||||||
"collapsedResourceTreeWidth": 36,
|
"collapsedResourceTreeWidth": 36,
|
||||||
"collectionCreationDefaults": Object {
|
"collectionCreationDefaults": Object {
|
||||||
"storage": "100",
|
"storage": "100",
|
||||||
@@ -3537,7 +3534,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,16 +3593,11 @@ 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],
|
||||||
"onToggleKeyDown": [Function],
|
"onToggleKeyDown": [Function],
|
||||||
|
"openSidePanel": undefined,
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
@@ -3708,6 +3699,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],
|
||||||
@@ -4666,6 +4660,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
},
|
},
|
||||||
"clickHostedAccountSwitch": [Function],
|
"clickHostedAccountSwitch": [Function],
|
||||||
"clickHostedDirectorySwitch": [Function],
|
"clickHostedDirectorySwitch": [Function],
|
||||||
|
"closeSidePanel": undefined,
|
||||||
"collapsedResourceTreeWidth": 36,
|
"collapsedResourceTreeWidth": 36,
|
||||||
"collectionCreationDefaults": Object {
|
"collectionCreationDefaults": Object {
|
||||||
"storage": "100",
|
"storage": "100",
|
||||||
@@ -4820,7 +4815,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,16 +4874,11 @@ 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],
|
||||||
"onToggleKeyDown": [Function],
|
"onToggleKeyDown": [Function],
|
||||||
|
"openSidePanel": undefined,
|
||||||
"provideFeedbackEmail": [Function],
|
"provideFeedbackEmail": [Function],
|
||||||
"queriesClient": QueriesClient {
|
"queriesClient": QueriesClient {
|
||||||
"container": [Circular],
|
"container": [Circular],
|
||||||
@@ -4991,6 +4980,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],
|
||||||
|
|||||||
@@ -256,11 +256,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
>
|
>
|
||||||
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
A request to increase the throughput is currently in progress. This operation will take some time to complete.
|
||||||
<br />
|
<br />
|
||||||
Database:
|
Database: sampleDb, Container: sampleCollection
|
||||||
sampleDb
|
|
||||||
, Container:
|
|
||||||
sampleCollection
|
|
||||||
|
|
||||||
, Current manual throughput: 1000 RU/s
|
, Current manual throughput: 1000 RU/s
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -275,11 +271,7 @@ exports[`SettingsUtils functions render 1`] = `
|
|||||||
>
|
>
|
||||||
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
|
A request to increase the throughput is currently in progress. This operation will take 1-3 business days to complete. View the latest status in Notifications.
|
||||||
<br />
|
<br />
|
||||||
Database:
|
Database: sampleDb, Container: sampleCollection
|
||||||
sampleDb
|
|
||||||
, Container:
|
|
||||||
sampleCollection
|
|
||||||
|
|
||||||
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
|
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
|
|||||||
@@ -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
|
|
||||||
styles={
|
|
||||||
Object {
|
|
||||||
"root": Object {
|
|
||||||
"padding": 10,
|
|
||||||
"width": 400,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tokens={
|
|
||||||
Object {
|
|
||||||
"childrenGap": 20,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Stack
|
<Stack
|
||||||
className="widgetRendererContainer"
|
className="widgetRendererContainer"
|
||||||
tokens={
|
tokens={
|
||||||
Object {
|
Object {
|
||||||
"childrenGap": 15,
|
"childrenGap": 10,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<StyledMessageBarBase>
|
<StyledMessageBarBase
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
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,6 +622,9 @@ exports[`SmartUiComponent should render 1`] = `
|
|||||||
"fontSize": 12,
|
"fontSize": 12,
|
||||||
"fontWeight": 600,
|
"fontWeight": 600,
|
||||||
},
|
},
|
||||||
|
"root": Object {
|
||||||
|
"width": 400,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -285,5 +632,4 @@ exports[`SmartUiComponent should render 1`] = `
|
|||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import * as ComponentRegisterer from "./ComponentRegisterer";
|
import * as ComponentRegisterer from "./ComponentRegisterer";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
@@ -47,7 +48,6 @@ import { ExplorerMetrics } from "../Common/Constants";
|
|||||||
import { ExplorerSettings } from "../Shared/ExplorerSettings";
|
import { ExplorerSettings } from "../Shared/ExplorerSettings";
|
||||||
import { FileSystemUtil } from "./Notebook/FileSystemUtil";
|
import { FileSystemUtil } from "./Notebook/FileSystemUtil";
|
||||||
import { handleOpenAction } from "./OpenActions";
|
import { handleOpenAction } from "./OpenActions";
|
||||||
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
|
||||||
import { IGalleryItem } from "../Juno/JunoClient";
|
import { IGalleryItem } from "../Juno/JunoClient";
|
||||||
import { LoadQueryPane } from "./Panes/LoadQueryPane";
|
import { LoadQueryPane } from "./Panes/LoadQueryPane";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
@@ -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";
|
||||||
@@ -92,6 +91,8 @@ import { appInsights } from "../Shared/appInsights";
|
|||||||
import { SelfServeLoadingComponentAdapter } from "../SelfServe/SelfServeLoadingComponentAdapter";
|
import { SelfServeLoadingComponentAdapter } from "../SelfServe/SelfServeLoadingComponentAdapter";
|
||||||
import { SelfServeType } from "../SelfServe/SelfServeUtils";
|
import { SelfServeType } from "../SelfServe/SelfServeUtils";
|
||||||
import { SelfServeComponentAdapter } from "../SelfServe/SelfServeComponentAdapter";
|
import { SelfServeComponentAdapter } from "../SelfServe/SelfServeComponentAdapter";
|
||||||
|
import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent";
|
||||||
|
import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
|
||||||
|
|
||||||
BindingHandlersRegisterer.registerBindingHandlers();
|
BindingHandlersRegisterer.registerBindingHandlers();
|
||||||
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
|
||||||
@@ -107,6 +108,14 @@ interface AdHocAccessData {
|
|||||||
readUrl: string;
|
readUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExplorerParams {
|
||||||
|
setIsNotificationConsoleExpanded: (isExpanded: boolean) => void;
|
||||||
|
setNotificationConsoleData: (consoleData: ConsoleData) => void;
|
||||||
|
setInProgressConsoleDataIdToBeDeleted: (id: string) => void;
|
||||||
|
openSidePanel: (headerText: string, panelContent: JSX.Element) => void;
|
||||||
|
closeSidePanel: () => 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,11 +155,14 @@ 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[];
|
||||||
|
private openSidePanel: (headerText: string, panelContent: JSX.Element) => void;
|
||||||
|
private closeSidePanel: () => void;
|
||||||
|
|
||||||
// Resource Tree
|
// Resource Tree
|
||||||
public databases: ko.ObservableArray<ViewModels.Database>;
|
public databases: ko.ObservableArray<ViewModels.Database>;
|
||||||
@@ -260,7 +272,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 +280,13 @@ 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;
|
||||||
|
this.openSidePanel = params?.openSidePanel;
|
||||||
|
this.closeSidePanel = params?.closeSidePanel;
|
||||||
|
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
|
||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
});
|
});
|
||||||
@@ -414,8 +431,8 @@ export default class Explorer {
|
|||||||
this.shouldShowShareDialogContents = ko.observable<boolean>(false);
|
this.shouldShowShareDialogContents = ko.observable<boolean>(false);
|
||||||
this.shouldShowDataAccessExpiryDialog = ko.observable<boolean>(false);
|
this.shouldShowDataAccessExpiryDialog = ko.observable<boolean>(false);
|
||||||
this.shouldShowContextSwitchPrompt = ko.observable<boolean>(false);
|
this.shouldShowContextSwitchPrompt = ko.observable<boolean>(false);
|
||||||
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
|
this.isGalleryPublishEnabled = ko.computed<boolean>(
|
||||||
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
() => configContext.ENABLE_GALLERY_PUBLISH || this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
||||||
);
|
);
|
||||||
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
|
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
|
||||||
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
||||||
@@ -430,7 +447,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 +494,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 +907,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 +1363,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 +1728,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 +1833,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
|
||||||
@@ -1938,6 +1897,9 @@ export default class Explorer {
|
|||||||
if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) {
|
if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) {
|
||||||
this.isMongoIndexingEnabled(true);
|
this.isMongoIndexingEnabled(true);
|
||||||
}
|
}
|
||||||
|
if (flights.indexOf(Constants.Flights.GalleryPublish) !== -1) {
|
||||||
|
this.isGalleryPublishEnabled = ko.computed<boolean>(() => true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public findSelectedCollection(): ViewModels.Collection {
|
public findSelectedCollection(): ViewModels.Collection {
|
||||||
@@ -2298,7 +2260,7 @@ export default class Explorer {
|
|||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): Promise<void> {
|
public async publishNotebook(name: string, content: string | unknown, parentDomElement?: HTMLElement): Promise<void> {
|
||||||
if (this.notebookManager) {
|
if (this.notebookManager) {
|
||||||
await this.notebookManager.openPublishNotebookPane(
|
await this.notebookManager.openPublishNotebookPane(
|
||||||
name,
|
name,
|
||||||
@@ -2859,28 +2821,21 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openGallery(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) {
|
public async openGallery(
|
||||||
|
selectedTab?: GalleryTab,
|
||||||
|
notebookUrl?: string,
|
||||||
|
galleryItem?: IGalleryItem,
|
||||||
|
isFavorite?: boolean
|
||||||
|
) {
|
||||||
let title: string = "Gallery";
|
let title: string = "Gallery";
|
||||||
let hashLocation: string = "gallery";
|
let hashLocation: string = "gallery";
|
||||||
|
|
||||||
const galleryTabs = this.tabsManager.getTabs(
|
const galleryTabOptions: any = {
|
||||||
ViewModels.CollectionTabKind.Gallery,
|
|
||||||
(tab) => tab.hashLocation() == hashLocation
|
|
||||||
);
|
|
||||||
let galleryTab = galleryTabs && galleryTabs[0];
|
|
||||||
|
|
||||||
if (galleryTab) {
|
|
||||||
this.tabsManager.activateTab(galleryTab);
|
|
||||||
} else {
|
|
||||||
if (!this.galleryTab) {
|
|
||||||
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
|
|
||||||
}
|
|
||||||
|
|
||||||
const newTab = new this.galleryTab.default({
|
|
||||||
// GalleryTabOptions
|
// GalleryTabOptions
|
||||||
account: userContext.databaseAccount,
|
account: userContext.databaseAccount,
|
||||||
container: this,
|
container: this,
|
||||||
junoClient: this.notebookManager?.junoClient,
|
junoClient: this.notebookManager?.junoClient,
|
||||||
|
selectedTab: selectedTab || GalleryTab.OfficialSamples,
|
||||||
notebookUrl,
|
notebookUrl,
|
||||||
galleryItem,
|
galleryItem,
|
||||||
isFavorite,
|
isFavorite,
|
||||||
@@ -2894,8 +2849,22 @@ export default class Explorer {
|
|||||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||||
isTabsContentExpanded: ko.observable(true),
|
isTabsContentExpanded: ko.observable(true),
|
||||||
onLoadStartKey: null,
|
onLoadStartKey: null,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const galleryTabs = this.tabsManager.getTabs(
|
||||||
|
ViewModels.CollectionTabKind.Gallery,
|
||||||
|
(tab) => tab.hashLocation() == hashLocation
|
||||||
|
);
|
||||||
|
let galleryTab = galleryTabs && galleryTabs[0];
|
||||||
|
|
||||||
|
if (galleryTab) {
|
||||||
|
this.tabsManager.activateTab(galleryTab);
|
||||||
|
(galleryTab as any).reset(galleryTabOptions);
|
||||||
|
} else {
|
||||||
|
if (!this.galleryTab) {
|
||||||
|
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
|
||||||
|
}
|
||||||
|
const newTab = new this.galleryTab.default(galleryTabOptions);
|
||||||
this.tabsManager.activateNewTab(newTab);
|
this.tabsManager.activateNewTab(newTab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3077,4 +3046,17 @@ export default class Explorer {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public openDeleteCollectionConfirmationPane(): void {
|
||||||
|
this.isFeatureEnabled(Constants.Features.enableKOPanel)
|
||||||
|
? this.deleteCollectionConfirmationPane.open()
|
||||||
|
: this.openSidePanel(
|
||||||
|
"Delete Collection",
|
||||||
|
<DeleteCollectionConfirmationPanel
|
||||||
|
explorer={this}
|
||||||
|
closePanel={() => this.closeSidePanel()}
|
||||||
|
openNotificationConsole={() => this.expandConsole()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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,16 +102,19 @@ 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
|
||||||
className="notificationConsoleHeader"
|
className="notificationConsoleHeader"
|
||||||
|
id="notificationConsoleHeader"
|
||||||
ref={this.setElememntRef}
|
ref={this.setElememntRef}
|
||||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.expandCollapseConsole()}
|
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.expandCollapseConsole()}
|
||||||
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}
|
||||||
@@ -143,18 +144,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 +190,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 +210,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 +230,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 +243,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 +264,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,11 +1,176 @@
|
|||||||
// 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
|
<div
|
||||||
className="notificationConsoleContainer"
|
className="notificationConsoleContainer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="notificationConsoleHeader"
|
className="notificationConsoleHeader"
|
||||||
|
id="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
|
||||||
|
className="notificationConsoleContainer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="notificationConsoleHeader"
|
||||||
|
id="notificationConsoleHeader"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -64,18 +229,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 +267,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 {}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
jest.mock("../../Common/dataAccess/deleteCollection");
|
|
||||||
import * as ko from "knockout";
|
|
||||||
import * as sinon from "sinon";
|
|
||||||
import Q from "q";
|
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
|
||||||
import DeleteCollectionConfirmationPane from "./DeleteCollectionConfirmationPane";
|
|
||||||
import DeleteFeedback from "../../Common/DeleteFeedback";
|
|
||||||
import Explorer from "../Explorer";
|
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import { TreeNode } from "../../Contracts/ViewModels";
|
|
||||||
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
|
|
||||||
|
|
||||||
describe("Delete Collection Confirmation Pane", () => {
|
|
||||||
describe("Explorer.isLastCollection()", () => {
|
|
||||||
let explorer: Explorer;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
explorer = new Explorer();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be true if 1 database and 1 collection", () => {
|
|
||||||
let database = {} as ViewModels.Database;
|
|
||||||
database.collections = ko.observableArray<ViewModels.Collection>([{} as ViewModels.Collection]);
|
|
||||||
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
|
|
||||||
expect(explorer.isLastCollection()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be false if if 1 database and 2 collection", () => {
|
|
||||||
let database = {} as ViewModels.Database;
|
|
||||||
database.collections = ko.observableArray<ViewModels.Collection>([
|
|
||||||
{} as ViewModels.Collection,
|
|
||||||
{} as ViewModels.Collection,
|
|
||||||
]);
|
|
||||||
explorer.databases = ko.observableArray<ViewModels.Database>([database]);
|
|
||||||
expect(explorer.isLastCollection()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be false if 2 database and 1 collection each", () => {
|
|
||||||
let database = {} as ViewModels.Database;
|
|
||||||
database.collections = ko.observableArray<ViewModels.Collection>([{} as ViewModels.Collection]);
|
|
||||||
let database2 = {} as ViewModels.Database;
|
|
||||||
database2.collections = ko.observableArray<ViewModels.Collection>([{} as ViewModels.Collection]);
|
|
||||||
explorer.databases = ko.observableArray<ViewModels.Database>([database, database2]);
|
|
||||||
expect(explorer.isLastCollection()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be false if 0 databases", () => {
|
|
||||||
let database = {} as ViewModels.Database;
|
|
||||||
explorer.databases = ko.observableArray<ViewModels.Database>();
|
|
||||||
database.collections = ko.observableArray<ViewModels.Collection>();
|
|
||||||
expect(explorer.isLastCollection()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("shouldRecordFeedback()", () => {
|
|
||||||
it("should return true if last collection and database does not have shared throughput else false", () => {
|
|
||||||
let fakeExplorer = new Explorer();
|
|
||||||
fakeExplorer.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
|
||||||
fakeExplorer.refreshAllDatabases = () => Q.resolve();
|
|
||||||
|
|
||||||
let pane = new DeleteCollectionConfirmationPane({
|
|
||||||
id: "deletecollectionconfirmationpane",
|
|
||||||
visible: ko.observable<boolean>(false),
|
|
||||||
container: fakeExplorer,
|
|
||||||
});
|
|
||||||
|
|
||||||
fakeExplorer.isLastCollection = () => true;
|
|
||||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
|
||||||
expect(pane.shouldRecordFeedback()).toBe(true);
|
|
||||||
|
|
||||||
fakeExplorer.isLastCollection = () => true;
|
|
||||||
fakeExplorer.isSelectedDatabaseShared = () => true;
|
|
||||||
expect(pane.shouldRecordFeedback()).toBe(false);
|
|
||||||
|
|
||||||
fakeExplorer.isLastCollection = () => false;
|
|
||||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
|
||||||
expect(pane.shouldRecordFeedback()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("submit()", () => {
|
|
||||||
let telemetryProcessorSpy: sinon.SinonSpy;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
(deleteCollection as jest.Mock).mockResolvedValue(undefined);
|
|
||||||
telemetryProcessorSpy = sinon.spy(TelemetryProcessor, "trace");
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
telemetryProcessorSpy.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("it should log feedback if last collection and database is not shared", () => {
|
|
||||||
let selectedCollectionId = "testCol";
|
|
||||||
let fakeExplorer = {} as Explorer;
|
|
||||||
fakeExplorer.findSelectedCollection = () => {
|
|
||||||
return {
|
|
||||||
id: ko.observable<string>(selectedCollectionId),
|
|
||||||
rid: "test",
|
|
||||||
} as ViewModels.Collection;
|
|
||||||
};
|
|
||||||
fakeExplorer.isNotificationConsoleExpanded = ko.observable<boolean>(false);
|
|
||||||
fakeExplorer.selectedCollectionId = ko.computed<string>(() => selectedCollectionId);
|
|
||||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
|
||||||
const SubscriptionId = "testId";
|
|
||||||
const AccountName = "testAccount";
|
|
||||||
fakeExplorer.databaseAccount = ko.observable<DataModels.DatabaseAccount>({
|
|
||||||
id: SubscriptionId,
|
|
||||||
name: AccountName,
|
|
||||||
} as DataModels.DatabaseAccount);
|
|
||||||
|
|
||||||
fakeExplorer.defaultExperience = ko.observable<string>("DocumentDB");
|
|
||||||
fakeExplorer.isPreferredApiCassandra = ko.computed(() => {
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
fakeExplorer.selectedNode = ko.observable<TreeNode>();
|
|
||||||
fakeExplorer.isLastCollection = () => true;
|
|
||||||
fakeExplorer.isSelectedDatabaseShared = () => false;
|
|
||||||
fakeExplorer.refreshAllDatabases = () => Q.resolve();
|
|
||||||
|
|
||||||
let pane = new DeleteCollectionConfirmationPane({
|
|
||||||
id: "deletecollectionconfirmationpane",
|
|
||||||
visible: ko.observable<boolean>(false),
|
|
||||||
container: fakeExplorer as any,
|
|
||||||
});
|
|
||||||
pane.collectionIdConfirmation = ko.observable<string>(selectedCollectionId);
|
|
||||||
const Feedback = "my feedback";
|
|
||||||
pane.containerDeleteFeedback(Feedback);
|
|
||||||
|
|
||||||
return pane.submit().then(() => {
|
|
||||||
expect(telemetryProcessorSpy.called).toBe(true);
|
|
||||||
let deleteFeedback = new DeleteFeedback(SubscriptionId, AccountName, DataModels.ApiKind.SQL, Feedback);
|
|
||||||
expect(
|
|
||||||
telemetryProcessorSpy.calledWith(Action.DeleteCollection, ActionModifiers.Mark, {
|
|
||||||
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
|
|
||||||
})
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
174
src/Explorer/Panes/DeleteCollectionConfirmationPane.test.tsx
Normal file
174
src/Explorer/Panes/DeleteCollectionConfirmationPane.test.tsx
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
jest.mock("../../Common/dataAccess/deleteCollection");
|
||||||
|
jest.mock("../../Shared/Telemetry/TelemetryProcessor");
|
||||||
|
import * as ko from "knockout";
|
||||||
|
import { ApiKind, DatabaseAccount } from "../../Contracts/DataModels";
|
||||||
|
import { Collection, Database } from "../../Contracts/ViewModels";
|
||||||
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { mount, ReactWrapper, shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import DeleteFeedback from "../../Common/DeleteFeedback";
|
||||||
|
import Explorer from "../Explorer";
|
||||||
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { TreeNode } from "../../Contracts/ViewModels";
|
||||||
|
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
|
||||||
|
import { DeleteCollectionConfirmationPanel } from "./DeleteCollectionConfirmationPanel";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
import { updateUserContext } from "../../UserContext";
|
||||||
|
|
||||||
|
describe("Delete Collection Confirmation Pane", () => {
|
||||||
|
describe("Explorer.isLastCollection()", () => {
|
||||||
|
let explorer: Explorer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
explorer = new Explorer();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be true if 1 database and 1 collection", () => {
|
||||||
|
const database = {} as Database;
|
||||||
|
database.collections = ko.observableArray<Collection>([{} as Collection]);
|
||||||
|
explorer.databases = ko.observableArray<Database>([database]);
|
||||||
|
expect(explorer.isLastCollection()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be false if if 1 database and 2 collection", () => {
|
||||||
|
const database = {} as Database;
|
||||||
|
database.collections = ko.observableArray<Collection>([{} as Collection, {} as Collection]);
|
||||||
|
explorer.databases = ko.observableArray<Database>([database]);
|
||||||
|
expect(explorer.isLastCollection()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be false if 2 database and 1 collection each", () => {
|
||||||
|
const database = {} as Database;
|
||||||
|
database.collections = ko.observableArray<Collection>([{} as Collection]);
|
||||||
|
const database2 = {} as Database;
|
||||||
|
database2.collections = ko.observableArray<Collection>([{} as Collection]);
|
||||||
|
explorer.databases = ko.observableArray<Database>([database, database2]);
|
||||||
|
expect(explorer.isLastCollection()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be false if 0 databases", () => {
|
||||||
|
const database = {} as Database;
|
||||||
|
explorer.databases = ko.observableArray<Database>();
|
||||||
|
database.collections = ko.observableArray<Collection>();
|
||||||
|
expect(explorer.isLastCollection()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shouldRecordFeedback()", () => {
|
||||||
|
it("should return true if last collection and database does not have shared throughput else false", () => {
|
||||||
|
const fakeExplorer = new Explorer();
|
||||||
|
fakeExplorer.refreshAllDatabases = () => undefined;
|
||||||
|
fakeExplorer.isLastCollection = () => true;
|
||||||
|
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
explorer: fakeExplorer,
|
||||||
|
closePanel: (): void => undefined,
|
||||||
|
openNotificationConsole: (): void => undefined,
|
||||||
|
};
|
||||||
|
const wrapper = shallow(<DeleteCollectionConfirmationPanel {...props} />);
|
||||||
|
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(true);
|
||||||
|
|
||||||
|
props.explorer.isLastCollection = () => true;
|
||||||
|
props.explorer.isSelectedDatabaseShared = () => true;
|
||||||
|
wrapper.setProps(props);
|
||||||
|
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
|
||||||
|
|
||||||
|
props.explorer.isLastCollection = () => false;
|
||||||
|
props.explorer.isSelectedDatabaseShared = () => false;
|
||||||
|
wrapper.setProps(props);
|
||||||
|
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("submit()", () => {
|
||||||
|
let wrapper: ReactWrapper;
|
||||||
|
const selectedCollectionId = "testCol";
|
||||||
|
const databaseId = "testDatabase";
|
||||||
|
const fakeExplorer = {} as Explorer;
|
||||||
|
fakeExplorer.findSelectedCollection = () => {
|
||||||
|
return {
|
||||||
|
id: ko.observable<string>(selectedCollectionId),
|
||||||
|
databaseId,
|
||||||
|
rid: "test",
|
||||||
|
} as Collection;
|
||||||
|
};
|
||||||
|
fakeExplorer.selectedCollectionId = ko.computed<string>(() => selectedCollectionId);
|
||||||
|
fakeExplorer.selectedNode = ko.observable<TreeNode>();
|
||||||
|
fakeExplorer.refreshAllDatabases = () => undefined;
|
||||||
|
fakeExplorer.isLastCollection = () => true;
|
||||||
|
fakeExplorer.isSelectedDatabaseShared = () => false;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
name: "testDatabaseAccountName",
|
||||||
|
properties: {
|
||||||
|
cassandraEndpoint: "testEndpoint",
|
||||||
|
},
|
||||||
|
id: "testDatabaseAccountId",
|
||||||
|
} as DatabaseAccount,
|
||||||
|
defaultExperience: DefaultAccountExperienceType.DocumentDB,
|
||||||
|
});
|
||||||
|
(deleteCollection as jest.Mock).mockResolvedValue(undefined);
|
||||||
|
(TelemetryProcessor.trace as jest.Mock).mockReturnValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const props = {
|
||||||
|
explorer: fakeExplorer,
|
||||||
|
closePanel: (): void => undefined,
|
||||||
|
openNotificationConsole: (): void => undefined,
|
||||||
|
};
|
||||||
|
wrapper = mount(<DeleteCollectionConfirmationPanel {...props} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call delete collection", () => {
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
|
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
||||||
|
wrapper
|
||||||
|
.find("#confirmCollectionId")
|
||||||
|
.hostNodes()
|
||||||
|
.simulate("change", { target: { value: selectedCollectionId } });
|
||||||
|
|
||||||
|
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
|
||||||
|
wrapper.find("#sidePanelOkButton").hostNodes().simulate("click");
|
||||||
|
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should record feedback", async () => {
|
||||||
|
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
||||||
|
wrapper
|
||||||
|
.find("#confirmCollectionId")
|
||||||
|
.hostNodes()
|
||||||
|
.simulate("change", { target: { value: selectedCollectionId } });
|
||||||
|
|
||||||
|
expect(wrapper.exists("#deleteCollectionFeedbackInput")).toBe(true);
|
||||||
|
const feedbackText = "Test delete collection feedback text";
|
||||||
|
wrapper
|
||||||
|
.find("#deleteCollectionFeedbackInput")
|
||||||
|
.hostNodes()
|
||||||
|
.simulate("change", { target: { value: feedbackText } });
|
||||||
|
|
||||||
|
expect(wrapper.exists("#sidePanelOkButton")).toBe(true);
|
||||||
|
wrapper.find("#sidePanelOkButton").hostNodes().simulate("click");
|
||||||
|
expect(deleteCollection).toHaveBeenCalledWith(databaseId, selectedCollectionId);
|
||||||
|
|
||||||
|
const deleteFeedback = new DeleteFeedback(
|
||||||
|
"testDatabaseAccountId",
|
||||||
|
"testDatabaseAccountName",
|
||||||
|
ApiKind.SQL,
|
||||||
|
feedbackText
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
expect(TelemetryProcessor.trace).toHaveBeenCalledWith(Action.DeleteCollection, ActionModifiers.Mark, {
|
||||||
|
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import Q from "q";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
|
|
||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||||
@@ -50,18 +48,7 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase
|
|||||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||||
paneTitle: this.title(),
|
paneTitle: this.title(),
|
||||||
});
|
});
|
||||||
let promise: Promise<any>;
|
return deleteCollection(selectedCollection.databaseId, selectedCollection.id()).then(
|
||||||
if (this.container.isPreferredApiCassandra()) {
|
|
||||||
promise = ((<CassandraAPIDataClient>this.container.tableDataClient).deleteTableOrKeyspace(
|
|
||||||
this.container.databaseAccount().properties.cassandraEndpoint,
|
|
||||||
this.container.databaseAccount().id,
|
|
||||||
`DROP TABLE ${selectedCollection.databaseId}.${selectedCollection.id()};`,
|
|
||||||
this.container
|
|
||||||
) as unknown) as Promise<any>;
|
|
||||||
} else {
|
|
||||||
promise = deleteCollection(selectedCollection.databaseId, selectedCollection.id());
|
|
||||||
}
|
|
||||||
return promise.then(
|
|
||||||
() => {
|
() => {
|
||||||
this.isExecuting(false);
|
this.isExecuting(false);
|
||||||
this.close();
|
this.close();
|
||||||
|
|||||||
186
src/Explorer/Panes/DeleteCollectionConfirmationPanel.tsx
Normal file
186
src/Explorer/Panes/DeleteCollectionConfirmationPanel.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { PanelFooterComponent } from "./PanelFooterComponent";
|
||||||
|
import { Collection } from "../../Contracts/ViewModels";
|
||||||
|
import { Text, TextField } from "office-ui-fabric-react";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
import { Areas } from "../../Common/Constants";
|
||||||
|
import { deleteCollection } from "../../Common/dataAccess/deleteCollection";
|
||||||
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||||
|
import { PanelErrorComponent, PanelErrorProps } from "./PanelErrorComponent";
|
||||||
|
import DeleteFeedback from "../../Common/DeleteFeedback";
|
||||||
|
import Explorer from "../Explorer";
|
||||||
|
import LoadingIndicator_3Squares from "../../../images/LoadingIndicator_3Squares.gif";
|
||||||
|
|
||||||
|
export interface DeleteCollectionConfirmationPanelProps {
|
||||||
|
explorer: Explorer;
|
||||||
|
closePanel: () => void;
|
||||||
|
openNotificationConsole: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteCollectionConfirmationPanelState {
|
||||||
|
formError: string;
|
||||||
|
isExecuting: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DeleteCollectionConfirmationPanel extends React.Component<
|
||||||
|
DeleteCollectionConfirmationPanelProps,
|
||||||
|
DeleteCollectionConfirmationPanelState
|
||||||
|
> {
|
||||||
|
private inputCollectionName: string;
|
||||||
|
private deleteCollectionFeedback: string;
|
||||||
|
|
||||||
|
constructor(props: DeleteCollectionConfirmationPanelProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
formError: "",
|
||||||
|
isExecuting: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="panelContentContainer">
|
||||||
|
<PanelErrorComponent {...this.getPanelErrorProps()} />
|
||||||
|
<div className="panelMainContent">
|
||||||
|
<div className="confirmDeleteInput">
|
||||||
|
<span className="mandatoryStar">* </span>
|
||||||
|
<Text variant="small">Confirm by typing the collection id</Text>
|
||||||
|
<TextField
|
||||||
|
id="confirmCollectionId"
|
||||||
|
autoFocus
|
||||||
|
styles={{ fieldGroup: { width: 300 } }}
|
||||||
|
onChange={(event, newInput?: string) => {
|
||||||
|
this.inputCollectionName = newInput;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{this.shouldRecordFeedback() && (
|
||||||
|
<div className="deleteCollectionFeedback">
|
||||||
|
<Text variant="small" block>
|
||||||
|
Help us improve Azure Cosmos DB!
|
||||||
|
</Text>
|
||||||
|
<Text variant="small" block>
|
||||||
|
What is the reason why you are deleting this container?
|
||||||
|
</Text>
|
||||||
|
<TextField
|
||||||
|
id="deleteCollectionFeedbackInput"
|
||||||
|
styles={{ fieldGroup: { width: 300 } }}
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
onChange={(event, newInput?: string) => {
|
||||||
|
this.deleteCollectionFeedback = newInput;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<PanelFooterComponent buttonLabel="OK" onOKButtonClicked={() => this.submit()} />
|
||||||
|
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!this.state.isExecuting}>
|
||||||
|
<img className="dataExplorerLoader" src={LoadingIndicator_3Squares} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPanelErrorProps(): PanelErrorProps {
|
||||||
|
if (this.state.formError) {
|
||||||
|
return {
|
||||||
|
isWarning: false,
|
||||||
|
message: this.state.formError,
|
||||||
|
showErrorDetails: true,
|
||||||
|
openNotificationConsole: this.props.openNotificationConsole,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isWarning: true,
|
||||||
|
showErrorDetails: false,
|
||||||
|
message:
|
||||||
|
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldRecordFeedback(): boolean {
|
||||||
|
return this.props.explorer.isLastCollection() && !this.props.explorer.isSelectedDatabaseShared();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async submit(): Promise<void> {
|
||||||
|
const collection = this.props.explorer.findSelectedCollection();
|
||||||
|
|
||||||
|
if (!collection || this.inputCollectionName !== collection.id()) {
|
||||||
|
const errorMessage = "Input collection name does not match the selected collection";
|
||||||
|
this.setState({ formError: errorMessage });
|
||||||
|
NotificationConsoleUtils.logConsoleError(`Error while deleting collection ${collection.id()}: ${errorMessage}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ formError: "", isExecuting: true });
|
||||||
|
|
||||||
|
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteCollection, {
|
||||||
|
databaseAccountName: userContext.databaseAccount?.name,
|
||||||
|
defaultExperience: userContext.defaultExperience,
|
||||||
|
collectionId: collection.id(),
|
||||||
|
dataExplorerArea: Areas.ContextualPane,
|
||||||
|
paneTitle: "Delete Collection",
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteCollection(collection.databaseId, collection.id());
|
||||||
|
|
||||||
|
this.setState({ isExecuting: false });
|
||||||
|
this.props.explorer.selectedNode(collection.database);
|
||||||
|
this.props.explorer.tabsManager?.closeTabsByComparator(
|
||||||
|
(tab) => tab.node?.id() === collection.id() && (tab.node as Collection).databaseId === collection.databaseId
|
||||||
|
);
|
||||||
|
this.props.explorer.refreshAllDatabases();
|
||||||
|
|
||||||
|
TelemetryProcessor.traceSuccess(
|
||||||
|
Action.DeleteCollection,
|
||||||
|
{
|
||||||
|
databaseAccountName: userContext.databaseAccount?.name,
|
||||||
|
defaultExperience: userContext.defaultExperience,
|
||||||
|
collectionId: collection.id(),
|
||||||
|
dataExplorerArea: Areas.ContextualPane,
|
||||||
|
paneTitle: "Delete Collection",
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.shouldRecordFeedback()) {
|
||||||
|
const deleteFeedback = new DeleteFeedback(
|
||||||
|
userContext.databaseAccount?.id,
|
||||||
|
userContext.databaseAccount?.name,
|
||||||
|
DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.defaultExperience),
|
||||||
|
this.deleteCollectionFeedback
|
||||||
|
);
|
||||||
|
|
||||||
|
TelemetryProcessor.trace(Action.DeleteCollection, ActionModifiers.Mark, {
|
||||||
|
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.closePanel();
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = getErrorMessage(error);
|
||||||
|
this.setState({ formError: errorMessage, isExecuting: false });
|
||||||
|
TelemetryProcessor.traceFailure(
|
||||||
|
Action.DeleteCollection,
|
||||||
|
{
|
||||||
|
databaseAccountName: userContext.databaseAccount?.name,
|
||||||
|
defaultExperience: userContext.defaultExperience,
|
||||||
|
collectionId: collection.id(),
|
||||||
|
dataExplorerArea: Areas.ContextualPane,
|
||||||
|
paneTitle: "Delete Collection",
|
||||||
|
error: errorMessage,
|
||||||
|
errorStack: getErrorStack(error),
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -52,19 +52,8 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
|
|||||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||||
paneTitle: this.title(),
|
paneTitle: this.title(),
|
||||||
});
|
});
|
||||||
// TODO: Should not be a Q promise anymore, but the Cassandra code requires it
|
return Q(
|
||||||
let promise: Q.Promise<any>;
|
deleteDatabase(selectedDatabase.id()).then(
|
||||||
if (this.container.isPreferredApiCassandra()) {
|
|
||||||
promise = (<CassandraAPIDataClient>this.container.tableDataClient).deleteTableOrKeyspace(
|
|
||||||
this.container.databaseAccount().properties.cassandraEndpoint,
|
|
||||||
this.container.databaseAccount().id,
|
|
||||||
`DROP KEYSPACE ${selectedDatabase.id()};`,
|
|
||||||
this.container
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
promise = Q(deleteDatabase(selectedDatabase.id()));
|
|
||||||
}
|
|
||||||
return promise.then(
|
|
||||||
() => {
|
() => {
|
||||||
this.isExecuting(false);
|
this.isExecuting(false);
|
||||||
this.close();
|
this.close();
|
||||||
@@ -127,6 +116,7 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
|
|||||||
startKey
|
startKey
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
57
src/Explorer/Panes/PanelComponent.less
Normal file
57
src/Explorer/Panes/PanelComponent.less
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
@import "../../../less/Common/Constants";
|
||||||
|
|
||||||
|
.panelContentContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.panelMainContent {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelHeader {
|
||||||
|
color: @BaseDark;
|
||||||
|
font-size: @largeFontSize;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelWarningErrorContainer {
|
||||||
|
background-color: @BaseLow;
|
||||||
|
padding: @DefaultSpace;
|
||||||
|
display: inline-flex;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.panelWarningIcon {
|
||||||
|
font-size: @WarningErrorIconSize;
|
||||||
|
width: @WarningErrorIconSize;
|
||||||
|
margin: auto 0 auto @SmallSpace;
|
||||||
|
color: @WarningIconColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelErrorIcon {
|
||||||
|
font-size: @WarningErrorIconSize;
|
||||||
|
width: @WarningErrorIconSize;
|
||||||
|
margin: auto 0 auto @SmallSpace;
|
||||||
|
color: @ErrorIconColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelWarningErrorDetailsLinkContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-left: @MediumSpace;
|
||||||
|
|
||||||
|
.paneErrorLink {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: @mediumFontSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelFooter button {
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteCollectionFeedback {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
41
src/Explorer/Panes/PanelContainerComponent.test.tsx
Normal file
41
src/Explorer/Panes/PanelContainerComponent.test.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import { PanelContainerComponent, PanelContainerProps } from "./PanelContainerComponent";
|
||||||
|
|
||||||
|
describe("PaneContainerComponent test", () => {
|
||||||
|
it("should render with panel content and header", () => {
|
||||||
|
const panelContainerProps: PanelContainerProps = {
|
||||||
|
headerText: "test",
|
||||||
|
panelContent: <div></div>,
|
||||||
|
isOpen: true,
|
||||||
|
isConsoleExpanded: false,
|
||||||
|
closePanel: undefined,
|
||||||
|
};
|
||||||
|
const wrapper = shallow(<PanelContainerComponent {...panelContainerProps} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render nothing if content is undefined", () => {
|
||||||
|
const panelContainerProps: PanelContainerProps = {
|
||||||
|
headerText: "test",
|
||||||
|
panelContent: undefined,
|
||||||
|
isOpen: true,
|
||||||
|
isConsoleExpanded: false,
|
||||||
|
closePanel: undefined,
|
||||||
|
};
|
||||||
|
const wrapper = shallow(<PanelContainerComponent {...panelContainerProps} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be resize if notification console is expanded", () => {
|
||||||
|
const panelContainerProps: PanelContainerProps = {
|
||||||
|
headerText: "test",
|
||||||
|
panelContent: <div></div>,
|
||||||
|
isOpen: true,
|
||||||
|
isConsoleExpanded: true,
|
||||||
|
closePanel: undefined,
|
||||||
|
};
|
||||||
|
const wrapper = shallow(<PanelContainerComponent {...panelContainerProps} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
58
src/Explorer/Panes/PanelContainerComponent.tsx
Normal file
58
src/Explorer/Panes/PanelContainerComponent.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Panel, PanelType } from "office-ui-fabric-react";
|
||||||
|
|
||||||
|
export interface PanelContainerProps {
|
||||||
|
headerText: string;
|
||||||
|
panelContent: JSX.Element;
|
||||||
|
isConsoleExpanded: boolean;
|
||||||
|
isOpen: boolean;
|
||||||
|
closePanel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PanelContainerComponent extends React.Component<PanelContainerProps> {
|
||||||
|
private static readonly consoleHeaderHeight = 32;
|
||||||
|
private static readonly consoleContentHeight = 220;
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
if (!this.props.panelContent) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel
|
||||||
|
headerText={this.props.headerText}
|
||||||
|
isOpen={this.props.isOpen}
|
||||||
|
onDismiss={this.onDissmiss}
|
||||||
|
isLightDismiss
|
||||||
|
type={PanelType.custom}
|
||||||
|
closeButtonAriaLabel="Close"
|
||||||
|
customWidth="440px"
|
||||||
|
headerClassName="panelHeader"
|
||||||
|
styles={{
|
||||||
|
navigation: { borderBottom: "1px solid #cccccc" },
|
||||||
|
content: { padding: "24px 34px 20px 34px", height: "100%" },
|
||||||
|
scrollableContent: { height: "100%" },
|
||||||
|
}}
|
||||||
|
style={{ height: this.getPanelHeight() }}
|
||||||
|
>
|
||||||
|
{this.props.panelContent}
|
||||||
|
</Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDissmiss = (ev?: React.SyntheticEvent<HTMLElement>): void => {
|
||||||
|
if ((ev.target as HTMLElement).id === "notificationConsoleHeader") {
|
||||||
|
ev.preventDefault();
|
||||||
|
} else {
|
||||||
|
this.props.closePanel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private getPanelHeight = (): string => {
|
||||||
|
const consoleHeight = this.props.isConsoleExpanded
|
||||||
|
? PanelContainerComponent.consoleContentHeight + PanelContainerComponent.consoleHeaderHeight
|
||||||
|
: PanelContainerComponent.consoleHeaderHeight;
|
||||||
|
const panelHeight = window.innerHeight - consoleHeight;
|
||||||
|
return panelHeight + "px";
|
||||||
|
};
|
||||||
|
}
|
||||||
29
src/Explorer/Panes/PanelErrorComponent.tsx
Normal file
29
src/Explorer/Panes/PanelErrorComponent.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Icon, Text } from "office-ui-fabric-react";
|
||||||
|
|
||||||
|
export interface PanelErrorProps {
|
||||||
|
message: string;
|
||||||
|
isWarning: boolean;
|
||||||
|
showErrorDetails: boolean;
|
||||||
|
openNotificationConsole?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PanelErrorComponent: React.FunctionComponent<PanelErrorProps> = (props: PanelErrorProps): JSX.Element => (
|
||||||
|
<div className="panelWarningErrorContainer">
|
||||||
|
{props.isWarning ? (
|
||||||
|
<Icon iconName="WarningSolid" className="panelWarningIcon" />
|
||||||
|
) : (
|
||||||
|
<Icon iconName="StatusErrorFull" className="panelErrorIcon" />
|
||||||
|
)}
|
||||||
|
<span className="panelWarningErrorDetailsLinkContainer">
|
||||||
|
<Text className="panelWarningErrorMessage" variant="small">
|
||||||
|
{props.message}
|
||||||
|
</Text>
|
||||||
|
{props.showErrorDetails && (
|
||||||
|
<a className="paneErrorLink" role="link" onClick={props.openNotificationConsole}>
|
||||||
|
More details
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
15
src/Explorer/Panes/PanelFooterComponent.tsx
Normal file
15
src/Explorer/Panes/PanelFooterComponent.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { PrimaryButton } from "office-ui-fabric-react";
|
||||||
|
|
||||||
|
export interface PanelFooterProps {
|
||||||
|
buttonLabel: string;
|
||||||
|
onOKButtonClicked: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PanelFooterComponent: React.FunctionComponent<PanelFooterProps> = (
|
||||||
|
props: PanelFooterProps
|
||||||
|
): JSX.Element => (
|
||||||
|
<div className="panelFooter">
|
||||||
|
<PrimaryButton id="sidePanelOkButton" text={props.buttonLabel} onClick={() => props.onOKButtonClicked()} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -10,7 +10,11 @@ import { ImmutableNotebook } from "@nteract/commutable/src";
|
|||||||
import { toJS } from "@nteract/commutable";
|
import { toJS } from "@nteract/commutable";
|
||||||
import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent";
|
import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent";
|
||||||
import { HttpStatusCodes } from "../../Common/Constants";
|
import { HttpStatusCodes } from "../../Common/Constants";
|
||||||
import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
import { handleError, getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
import { GalleryTab } from "../Controls/NotebookGallery/GalleryViewerComponent";
|
||||||
|
import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { FileSystemUtil } from "../Notebook/FileSystemUtil";
|
||||||
|
|
||||||
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||||
parameters: ko.Observable<number>;
|
parameters: ko.Observable<number>;
|
||||||
@@ -66,7 +70,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
onChangeDescription: (newValue: string) => (this.description = newValue),
|
onChangeDescription: (newValue: string) => (this.description = newValue),
|
||||||
onChangeTags: (newValue: string) => (this.tags = newValue),
|
onChangeTags: (newValue: string) => (this.tags = newValue),
|
||||||
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
|
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
|
||||||
onError: this.createFormErrorForLargeImageSelection,
|
onError: this.createFormError,
|
||||||
clearFormError: this.clearFormError,
|
clearFormError: this.clearFormError,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -140,11 +144,22 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
this.isExecuting = true;
|
this.isExecuting = true;
|
||||||
this.triggerRender();
|
this.triggerRender();
|
||||||
|
|
||||||
try {
|
let startKey: number;
|
||||||
if (!this.name || !this.description || !this.author) {
|
|
||||||
throw new Error("Name, description, and author are required");
|
if (!this.name || !this.description || !this.author || !this.imageSrc) {
|
||||||
|
const formError = `Failed to publish ${this.name} to gallery`;
|
||||||
|
const formErrorDetail = "Name, description, author and cover image are required";
|
||||||
|
this.createFormError(formError, formErrorDetail, "PublishNotebookPaneAdapter/submit");
|
||||||
|
this.isExecuting = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
startKey = traceStart(Action.NotebooksGalleryPublish, {
|
||||||
|
databaseAccountName: this.container.databaseAccount()?.name,
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
});
|
||||||
|
|
||||||
const response = await this.junoClient.publishNotebook(
|
const response = await this.junoClient.publishNotebook(
|
||||||
this.name,
|
this.name,
|
||||||
this.description,
|
this.description,
|
||||||
@@ -157,17 +172,43 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
|
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
if (data) {
|
if (data) {
|
||||||
|
let isPublishPending = false;
|
||||||
|
|
||||||
if (data.pendingScanJobIds?.length > 0) {
|
if (data.pendingScanJobIds?.length > 0) {
|
||||||
|
isPublishPending = true;
|
||||||
NotificationConsoleUtils.logConsoleInfo(
|
NotificationConsoleUtils.logConsoleInfo(
|
||||||
`Content of ${this.name} is currently being scanned for illegal content. It will not be available in the public gallery until the review is complete (may take a few days).`
|
`Content of ${this.name} is currently being scanned for illegal content. It will not be available in the public gallery until the review is complete (may take a few days).`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
NotificationConsoleUtils.logConsoleInfo(`Published ${this.name} to gallery`);
|
NotificationConsoleUtils.logConsoleInfo(`Published ${this.name} to gallery`);
|
||||||
|
this.container.openGallery(GalleryTab.Published);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
traceSuccess(
|
||||||
|
Action.NotebooksGalleryPublish,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.container.databaseAccount()?.name,
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
notebookId: data.id,
|
||||||
|
isPublishPending,
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
traceFailure(
|
||||||
|
Action.NotebooksGalleryPublish,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.container.databaseAccount()?.name,
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
errorStack: getErrorStack(error),
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
this.formError = `Failed to publish ${this.name} to gallery`;
|
this.formError = `Failed to publish ${FileSystemUtil.stripExtension(this.name, "ipynb")} to gallery`;
|
||||||
this.formErrorDetail = `${errorMessage}`;
|
this.formErrorDetail = `${errorMessage}`;
|
||||||
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", this.formError);
|
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", this.formError);
|
||||||
return;
|
return;
|
||||||
@@ -180,7 +221,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private createFormErrorForLargeImageSelection = (formError: string, formErrorDetail: string, area: string): void => {
|
private createFormError = (formError: string, formErrorDetail: string, area: string): void => {
|
||||||
this.formError = formError;
|
this.formError = formError;
|
||||||
this.formErrorDetail = formErrorDetail;
|
this.formErrorDetail = formErrorDetail;
|
||||||
handleError(formErrorDetail, area, formError);
|
handleError(formErrorDetail, area, formError);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface PublishNotebookPaneProps {
|
|||||||
notebookAuthor: string;
|
notebookAuthor: string;
|
||||||
notebookCreatedDate: string;
|
notebookCreatedDate: string;
|
||||||
notebookObject: ImmutableNotebook;
|
notebookObject: ImmutableNotebook;
|
||||||
notebookParentDomElement: HTMLElement;
|
notebookParentDomElement?: HTMLElement;
|
||||||
onChangeName: (newValue: string) => void;
|
onChangeName: (newValue: string) => void;
|
||||||
onChangeDescription: (newValue: string) => void;
|
onChangeDescription: (newValue: string) => void;
|
||||||
onChangeTags: (newValue: string) => void;
|
onChangeTags: (newValue: string) => void;
|
||||||
@@ -54,7 +54,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
type: ImageTypes.Url,
|
type: ImageTypes.CustomImage,
|
||||||
notebookName: props.notebookName,
|
notebookName: props.notebookName,
|
||||||
notebookDescription: "",
|
notebookDescription: "",
|
||||||
notebookTags: "",
|
notebookTags: "",
|
||||||
@@ -110,7 +110,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.descriptionPara1 =
|
this.descriptionPara1 =
|
||||||
"This notebook has your data. Please make sure you delete any sensitive data/output before publishing.";
|
"When published, this notebook will appear in the Azure Cosmos DB notebooks public gallery. Make sure you have removed any sensitive data or output before publishing.";
|
||||||
|
|
||||||
this.descriptionPara2 = `Would you like to publish and share "${FileSystemUtil.stripExtension(
|
this.descriptionPara2 = `Would you like to publish and share "${FileSystemUtil.stripExtension(
|
||||||
this.props.notebookName,
|
this.props.notebookName,
|
||||||
@@ -120,6 +120,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
this.thumbnailUrlProps = {
|
this.thumbnailUrlProps = {
|
||||||
label: "Cover image url",
|
label: "Cover image url",
|
||||||
ariaLabel: "Cover image url",
|
ariaLabel: "Cover image url",
|
||||||
|
required: true,
|
||||||
onChange: (event, newValue) => {
|
onChange: (event, newValue) => {
|
||||||
this.props.onChangeImageSrc(newValue);
|
this.props.onChangeImageSrc(newValue);
|
||||||
this.setState({ imageSrc: newValue });
|
this.setState({ imageSrc: newValue });
|
||||||
@@ -140,17 +141,23 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
this.props.onError(formError, formErrorDetail, area);
|
this.props.onError(formError, formErrorDetail, area);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url];
|
||||||
|
|
||||||
|
if (this.props.notebookParentDomElement) {
|
||||||
|
options.push(ImageTypes.TakeScreenshot);
|
||||||
|
if (this.props.notebookObject) {
|
||||||
|
options.push(ImageTypes.UseFirstDisplayOutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.thumbnailSelectorProps = {
|
this.thumbnailSelectorProps = {
|
||||||
label: "Cover image",
|
label: "Cover image",
|
||||||
defaultSelectedKey: ImageTypes.Url,
|
defaultSelectedKey: ImageTypes.CustomImage,
|
||||||
ariaLabel: "Cover image",
|
ariaLabel: "Cover image",
|
||||||
options: [
|
options: options.map((value: string) => ({ text: value, key: value })),
|
||||||
ImageTypes.Url,
|
|
||||||
ImageTypes.CustomImage,
|
|
||||||
ImageTypes.TakeScreenshot,
|
|
||||||
ImageTypes.UseFirstDisplayOutput,
|
|
||||||
].map((value: string) => ({ text: value, key: value })),
|
|
||||||
onChange: async (event, options) => {
|
onChange: async (event, options) => {
|
||||||
|
this.setState({ imageSrc: undefined });
|
||||||
|
this.props.onChangeImageSrc(undefined);
|
||||||
this.props.clearFormError();
|
this.props.clearFormError();
|
||||||
if (options.text === ImageTypes.TakeScreenshot) {
|
if (options.text === ImageTypes.TakeScreenshot) {
|
||||||
try {
|
try {
|
||||||
@@ -172,11 +179,12 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
this.nameProps = {
|
this.nameProps = {
|
||||||
label: "Name",
|
label: "Name",
|
||||||
ariaLabel: "Name",
|
ariaLabel: "Name",
|
||||||
defaultValue: this.props.notebookName,
|
defaultValue: FileSystemUtil.stripExtension(this.props.notebookName, "ipynb"),
|
||||||
required: true,
|
required: true,
|
||||||
onChange: (event, newValue) => {
|
onChange: (event, newValue) => {
|
||||||
this.props.onChangeName(newValue);
|
const notebookName = newValue + ".ipynb";
|
||||||
this.setState({ notebookName: newValue });
|
this.props.onChangeName(notebookName);
|
||||||
|
this.setState({ notebookName });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -293,16 +301,16 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
thumbnailUrl: this.state.imageSrc,
|
thumbnailUrl: this.state.imageSrc,
|
||||||
created: this.props.notebookCreatedDate,
|
created: this.props.notebookCreatedDate,
|
||||||
isSample: false,
|
isSample: false,
|
||||||
downloads: 0,
|
downloads: undefined,
|
||||||
favorites: 0,
|
favorites: undefined,
|
||||||
views: 0,
|
views: undefined,
|
||||||
newCellId: undefined,
|
newCellId: undefined,
|
||||||
policyViolations: undefined,
|
policyViolations: undefined,
|
||||||
pendingScanJobIds: undefined,
|
pendingScanJobIds: undefined,
|
||||||
}}
|
}}
|
||||||
isFavorite={false}
|
isFavorite={undefined}
|
||||||
showDownload={true}
|
showDownload={false}
|
||||||
showDelete={true}
|
showDelete={false}
|
||||||
onClick={undefined}
|
onClick={undefined}
|
||||||
onTagClick={undefined}
|
onTagClick={undefined}
|
||||||
onFavoriteClick={undefined}
|
onFavoriteClick={undefined}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`PaneContainerComponent test should be resize if notification console is expanded 1`] = `
|
||||||
|
<StyledPanelBase
|
||||||
|
closeButtonAriaLabel="Close"
|
||||||
|
customWidth="440px"
|
||||||
|
headerClassName="panelHeader"
|
||||||
|
headerText="test"
|
||||||
|
isLightDismiss={true}
|
||||||
|
isOpen={true}
|
||||||
|
onDismiss={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": "516px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"content": Object {
|
||||||
|
"height": "100%",
|
||||||
|
"padding": "24px 34px 20px 34px",
|
||||||
|
},
|
||||||
|
"navigation": Object {
|
||||||
|
"borderBottom": "1px solid #cccccc",
|
||||||
|
},
|
||||||
|
"scrollableContent": Object {
|
||||||
|
"height": "100%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type={7}
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</StyledPanelBase>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`PaneContainerComponent test should render nothing if content is undefined 1`] = `<Fragment />`;
|
||||||
|
|
||||||
|
exports[`PaneContainerComponent test should render with panel content and header 1`] = `
|
||||||
|
<StyledPanelBase
|
||||||
|
closeButtonAriaLabel="Close"
|
||||||
|
customWidth="440px"
|
||||||
|
headerClassName="panelHeader"
|
||||||
|
headerText="test"
|
||||||
|
isLightDismiss={true}
|
||||||
|
isOpen={true}
|
||||||
|
onDismiss={[Function]}
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"height": "736px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"content": Object {
|
||||||
|
"height": "100%",
|
||||||
|
"padding": "24px 34px 20px 34px",
|
||||||
|
},
|
||||||
|
"navigation": Object {
|
||||||
|
"borderBottom": "1px solid #cccccc",
|
||||||
|
},
|
||||||
|
"scrollableContent": Object {
|
||||||
|
"height": "100%",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type={7}
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</StyledPanelBase>
|
||||||
|
`;
|
||||||
@@ -14,7 +14,7 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
|||||||
>
|
>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<Text>
|
<Text>
|
||||||
This notebook has your data. Please make sure you delete any sensitive data/output before publishing.
|
When published, this notebook will appear in the Azure Cosmos DB notebooks public gallery. Make sure you have removed any sensitive data or output before publishing.
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
@@ -25,7 +25,7 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
|||||||
<StackItem>
|
<StackItem>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
ariaLabel="Name"
|
ariaLabel="Name"
|
||||||
defaultValue="SampleNotebook.ipynb"
|
defaultValue="SampleNotebook"
|
||||||
label="Name"
|
label="Name"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
required={true}
|
required={true}
|
||||||
@@ -52,36 +52,29 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
|||||||
<StackItem>
|
<StackItem>
|
||||||
<StyledWithResponsiveMode
|
<StyledWithResponsiveMode
|
||||||
ariaLabel="Cover image"
|
ariaLabel="Cover image"
|
||||||
defaultSelectedKey="URL"
|
defaultSelectedKey="Custom Image"
|
||||||
label="Cover image"
|
label="Cover image"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
options={
|
options={
|
||||||
Array [
|
Array [
|
||||||
Object {
|
|
||||||
"key": "URL",
|
|
||||||
"text": "URL",
|
|
||||||
},
|
|
||||||
Object {
|
Object {
|
||||||
"key": "Custom Image",
|
"key": "Custom Image",
|
||||||
"text": "Custom Image",
|
"text": "Custom Image",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"key": "Take Screenshot",
|
"key": "URL",
|
||||||
"text": "Take Screenshot",
|
"text": "URL",
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"key": "Use First Display Output",
|
|
||||||
"text": "Use First Display Output",
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<StyledTextFieldBase
|
<input
|
||||||
ariaLabel="Cover image url"
|
accept="image/*"
|
||||||
label="Cover image url"
|
id="selectImageFile"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
|
type="file"
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
@@ -96,8 +89,8 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
|||||||
"author": "CosmosDB",
|
"author": "CosmosDB",
|
||||||
"created": "2020-07-17T00:00:00Z",
|
"created": "2020-07-17T00:00:00Z",
|
||||||
"description": "",
|
"description": "",
|
||||||
"downloads": 0,
|
"downloads": undefined,
|
||||||
"favorites": 0,
|
"favorites": undefined,
|
||||||
"gitSha": undefined,
|
"gitSha": undefined,
|
||||||
"id": undefined,
|
"id": undefined,
|
||||||
"isSample": false,
|
"isSample": false,
|
||||||
@@ -109,12 +102,11 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
|||||||
"",
|
"",
|
||||||
],
|
],
|
||||||
"thumbnailUrl": undefined,
|
"thumbnailUrl": undefined,
|
||||||
"views": 0,
|
"views": undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isFavorite={false}
|
showDelete={false}
|
||||||
showDelete={true}
|
showDownload={false}
|
||||||
showDownload={true}
|
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -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 -
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { createDocument } from "../../Common/dataAccess/createDocument";
|
|||||||
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
|
||||||
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
|
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
|
||||||
import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
import { updateDocument } from "../../Common/dataAccess/updateDocument";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export interface CassandraTableKeys {
|
export interface CassandraTableKeys {
|
||||||
partitionKeys: CassandraTableKey[];
|
partitionKeys: CassandraTableKey[];
|
||||||
@@ -345,7 +346,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
ConsoleDataType.InProgress,
|
ConsoleDataType.InProgress,
|
||||||
`Creating a new keyspace with query ${createKeyspaceQuery}`
|
`Creating a new keyspace with query ${createKeyspaceQuery}`
|
||||||
);
|
);
|
||||||
this.createOrDeleteQuery(cassandraEndpoint, resourceId, createKeyspaceQuery, explorer)
|
this.createOrDeleteQuery(cassandraEndpoint, resourceId, createKeyspaceQuery)
|
||||||
.then(
|
.then(
|
||||||
(data: any) => {
|
(data: any) => {
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
@@ -391,7 +392,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
ConsoleDataType.InProgress,
|
ConsoleDataType.InProgress,
|
||||||
`Creating a new table with query ${createTableQuery}`
|
`Creating a new table with query ${createTableQuery}`
|
||||||
);
|
);
|
||||||
this.createOrDeleteQuery(cassandraEndpoint, resourceId, createTableQuery, explorer)
|
this.createOrDeleteQuery(cassandraEndpoint, resourceId, createTableQuery)
|
||||||
.then(
|
.then(
|
||||||
(data: any) => {
|
(data: any) => {
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
@@ -416,41 +417,6 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
public deleteTableOrKeyspace(
|
|
||||||
cassandraEndpoint: string,
|
|
||||||
resourceId: string,
|
|
||||||
deleteQuery: string,
|
|
||||||
explorer: Explorer
|
|
||||||
): Q.Promise<any> {
|
|
||||||
const deferred = Q.defer<any>();
|
|
||||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.InProgress,
|
|
||||||
`Deleting resource with query ${deleteQuery}`
|
|
||||||
);
|
|
||||||
this.createOrDeleteQuery(cassandraEndpoint, resourceId, deleteQuery, explorer)
|
|
||||||
.then(
|
|
||||||
() => {
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.Info,
|
|
||||||
`Successfully deleted resource with query ${deleteQuery}`
|
|
||||||
);
|
|
||||||
deferred.resolve();
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
handleError(
|
|
||||||
error,
|
|
||||||
"DeleteKeyspaceOrTableCassandra",
|
|
||||||
`Error while deleting resource with query ${deleteQuery}`
|
|
||||||
);
|
|
||||||
deferred.reject(error);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.finally(() => {
|
|
||||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
|
||||||
});
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
public getTableKeys(collection: ViewModels.Collection): Q.Promise<CassandraTableKeys> {
|
||||||
if (!!collection.cassandraKeys) {
|
if (!!collection.cassandraKeys) {
|
||||||
return Q.resolve(collection.cassandraKeys);
|
return Q.resolve(collection.cassandraKeys);
|
||||||
@@ -551,12 +517,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private createOrDeleteQuery(
|
private createOrDeleteQuery(cassandraEndpoint: string, resourceId: string, query: string): Q.Promise<any> {
|
||||||
cassandraEndpoint: string,
|
|
||||||
resourceId: string,
|
|
||||||
query: string,
|
|
||||||
explorer: Explorer
|
|
||||||
): Q.Promise<any> {
|
|
||||||
const deferred = Q.defer();
|
const deferred = Q.defer();
|
||||||
const authType = window.authType;
|
const authType = window.authType;
|
||||||
const apiEndpoint: string =
|
const apiEndpoint: string =
|
||||||
@@ -566,7 +527,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
$.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
$.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
data: {
|
data: {
|
||||||
accountName: explorer.databaseAccount() && explorer.databaseAccount().name,
|
accountName: userContext.databaseAccount?.name,
|
||||||
cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint),
|
cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint),
|
||||||
resourceId: resourceId,
|
resourceId: resourceId,
|
||||||
query: query,
|
query: query,
|
||||||
|
|||||||
@@ -387,8 +387,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
tabTitle: this.tabTitle(),
|
tabTitle: this.tabTitle(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updateOfferParams: DataModels.UpdateOfferParams = {
|
const updateOfferParams: DataModels.UpdateOfferParams = {
|
||||||
databaseId: this.database.id(),
|
databaseId: this.database.id(),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface GalleryTabOptions extends ViewModels.TabOptions {
|
|||||||
account: DatabaseAccount;
|
account: DatabaseAccount;
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
junoClient: JunoClient;
|
junoClient: JunoClient;
|
||||||
|
selectedTab: GalleryViewerTab;
|
||||||
notebookUrl?: string;
|
notebookUrl?: string;
|
||||||
galleryItem?: IGalleryItem;
|
galleryItem?: IGalleryItem;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
@@ -21,27 +22,46 @@ interface GalleryTabOptions extends ViewModels.TabOptions {
|
|||||||
*/
|
*/
|
||||||
export default class GalleryTab extends TabsBase {
|
export default class GalleryTab extends TabsBase {
|
||||||
private container: Explorer;
|
private container: Explorer;
|
||||||
|
private galleryAndNotebookViewerComponentProps: GalleryAndNotebookViewerComponentProps;
|
||||||
public galleryAndNotebookViewerComponentAdapter: GalleryAndNotebookViewerComponentAdapter;
|
public galleryAndNotebookViewerComponentAdapter: GalleryAndNotebookViewerComponentAdapter;
|
||||||
|
|
||||||
constructor(options: GalleryTabOptions) {
|
constructor(options: GalleryTabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.container = options.container;
|
this.container = options.container;
|
||||||
const props: GalleryAndNotebookViewerComponentProps = {
|
|
||||||
|
this.galleryAndNotebookViewerComponentProps = {
|
||||||
container: options.container,
|
container: options.container,
|
||||||
|
isGalleryPublishEnabled: options.container.isGalleryPublishEnabled(),
|
||||||
junoClient: options.junoClient,
|
junoClient: options.junoClient,
|
||||||
notebookUrl: options.notebookUrl,
|
notebookUrl: options.notebookUrl,
|
||||||
galleryItem: options.galleryItem,
|
galleryItem: options.galleryItem,
|
||||||
isFavorite: options.isFavorite,
|
isFavorite: options.isFavorite,
|
||||||
selectedTab: GalleryViewerTab.OfficialSamples,
|
selectedTab: options.selectedTab,
|
||||||
sortBy: SortBy.MostViewed,
|
sortBy: SortBy.MostViewed,
|
||||||
searchText: undefined,
|
searchText: undefined,
|
||||||
};
|
};
|
||||||
|
this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter(
|
||||||
this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter(props);
|
this.galleryAndNotebookViewerComponentProps
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getContainer(): Explorer {
|
public reset(options: GalleryTabOptions) {
|
||||||
|
this.container = options.container;
|
||||||
|
|
||||||
|
this.galleryAndNotebookViewerComponentProps.container = options.container;
|
||||||
|
this.galleryAndNotebookViewerComponentProps.junoClient = options.junoClient;
|
||||||
|
this.galleryAndNotebookViewerComponentProps.notebookUrl = options.notebookUrl;
|
||||||
|
this.galleryAndNotebookViewerComponentProps.galleryItem = options.galleryItem;
|
||||||
|
this.galleryAndNotebookViewerComponentProps.isFavorite = options.isFavorite;
|
||||||
|
this.galleryAndNotebookViewerComponentProps.selectedTab = options.selectedTab;
|
||||||
|
this.galleryAndNotebookViewerComponentProps.sortBy = SortBy.MostViewed;
|
||||||
|
this.galleryAndNotebookViewerComponentProps.searchText = undefined;
|
||||||
|
|
||||||
|
this.galleryAndNotebookViewerComponentAdapter.reset();
|
||||||
|
this.galleryAndNotebookViewerComponentAdapter.triggerRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getContainer(): Explorer {
|
||||||
return this.container;
|
return this.container;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-out
|
|||||||
import InterruptKernelIcon from "../../../images/notebook/Notebook-stop.svg";
|
import InterruptKernelIcon from "../../../images/notebook/Notebook-stop.svg";
|
||||||
import KillKernelIcon from "../../../images/notebook/Notebook-stop.svg";
|
import KillKernelIcon from "../../../images/notebook/Notebook-stop.svg";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { Areas, ArmApiVersions } from "../../Common/Constants";
|
import { Areas, ArmApiVersions } from "../../Common/Constants";
|
||||||
import { CommandBarComponentButtonFactory } from "../Menus/CommandBar/CommandBarComponentButtonFactory";
|
import { CommandBarComponentButtonFactory } from "../Menus/CommandBar/CommandBarComponentButtonFactory";
|
||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
@@ -117,7 +117,7 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
return await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName());
|
return await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getContainer(): Explorer {
|
public getContainer(): Explorer {
|
||||||
return this.container;
|
return this.container;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,6 +485,10 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private publishToGallery = async () => {
|
private publishToGallery = async () => {
|
||||||
|
TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, {
|
||||||
|
source: Source.CommandBarMenu,
|
||||||
|
});
|
||||||
|
|
||||||
const notebookContent = this.notebookComponentAdapter.getContent();
|
const notebookContent = this.notebookComponentAdapter.getContent();
|
||||||
await this.container.publishNotebook(
|
await this.container.publishNotebook(
|
||||||
notebookContent.name,
|
notebookContent.name,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default class NotebookViewerTab extends TabsBase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getContainer(): Explorer {
|
public getContainer(): Explorer {
|
||||||
return this.container;
|
return this.container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import * as DataModels from "../../Contracts/DataModels";
|
|||||||
import TabsBase from "./TabsBase";
|
import TabsBase from "./TabsBase";
|
||||||
import { SettingsComponentAdapter } from "../Controls/Settings/SettingsComponentAdapter";
|
import { SettingsComponentAdapter } from "../Controls/Settings/SettingsComponentAdapter";
|
||||||
import { SettingsComponentProps } from "../Controls/Settings/SettingsComponent";
|
import { SettingsComponentProps } from "../Controls/Settings/SettingsComponent";
|
||||||
import Explorer from "../Explorer";
|
|
||||||
import { traceFailure } from "../../Shared/Telemetry/TelemetryProcessor";
|
import { traceFailure } from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
@@ -11,23 +10,27 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
|||||||
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
|
||||||
export default class SettingsTabV2 extends TabsBase {
|
export class SettingsTabV2 extends TabsBase {
|
||||||
public settingsComponentAdapter: SettingsComponentAdapter;
|
public settingsComponentAdapter: SettingsComponentAdapter;
|
||||||
private notificationRead: ko.Observable<boolean>;
|
|
||||||
private notification: DataModels.Notification;
|
|
||||||
private offerRead: ko.Observable<boolean>;
|
|
||||||
private currentCollection: ViewModels.Collection;
|
|
||||||
private options: ViewModels.SettingsTabV2Options;
|
|
||||||
|
|
||||||
constructor(options: ViewModels.SettingsTabV2Options) {
|
constructor(options: ViewModels.TabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
this.options = options;
|
|
||||||
this.tabId = "SettingsV2-" + this.tabId;
|
|
||||||
const props: SettingsComponentProps = {
|
const props: SettingsComponentProps = {
|
||||||
settingsTab: this,
|
settingsTab: this,
|
||||||
};
|
};
|
||||||
this.settingsComponentAdapter = new SettingsComponentAdapter(props);
|
this.settingsComponentAdapter = new SettingsComponentAdapter(props);
|
||||||
this.currentCollection = this.collection as ViewModels.Collection;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CollectionSettingsTabV2 extends SettingsTabV2 {
|
||||||
|
private notificationRead: ko.Observable<boolean>;
|
||||||
|
private notification: DataModels.Notification;
|
||||||
|
private offerRead: ko.Observable<boolean>;
|
||||||
|
|
||||||
|
constructor(options: ViewModels.TabOptions) {
|
||||||
|
super(options);
|
||||||
|
|
||||||
|
this.tabId = "SettingsV2-" + this.tabId;
|
||||||
this.notificationRead = ko.observable(false);
|
this.notificationRead = ko.observable(false);
|
||||||
this.offerRead = ko.observable(false);
|
this.offerRead = ko.observable(false);
|
||||||
this.settingsComponentAdapter.parameters = ko.computed<boolean>(() => {
|
this.settingsComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||||
@@ -45,49 +48,95 @@ export default class SettingsTabV2 extends TabsBase {
|
|||||||
public async onActivate(): Promise<void> {
|
public async onActivate(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.isExecuting(true);
|
this.isExecuting(true);
|
||||||
await this.currentCollection.loadOffer();
|
|
||||||
// passed in options and set by parent as "Settings" by default
|
|
||||||
this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings");
|
|
||||||
|
|
||||||
this.options.getPendingNotification.then(
|
const collection: ViewModels.Collection = this.collection as ViewModels.Collection;
|
||||||
(data: DataModels.Notification) => {
|
await collection.loadOffer();
|
||||||
|
// passed in options and set by parent as "Settings" by default
|
||||||
|
this.tabTitle(collection.offer() ? "Settings" : "Scale & Settings");
|
||||||
|
|
||||||
|
const data: DataModels.Notification = await collection.getPendingThroughputSplitNotification();
|
||||||
this.notification = data;
|
this.notification = data;
|
||||||
this.notificationRead(true);
|
this.notificationRead(true);
|
||||||
},
|
} catch (error) {
|
||||||
(error) => {
|
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
this.notification = undefined;
|
this.notification = undefined;
|
||||||
this.notificationRead(true);
|
this.notificationRead(true);
|
||||||
traceFailure(
|
traceFailure(
|
||||||
Action.Tab,
|
Action.Tab,
|
||||||
{
|
{
|
||||||
databaseAccountName: this.options.collection.container.databaseAccount().name,
|
databaseAccountName: this.collection.container.databaseAccount().name,
|
||||||
databaseName: this.options.collection.databaseId,
|
databaseName: this.collection.databaseId,
|
||||||
collectionName: this.options.collection.id(),
|
collectionName: this.collection.id(),
|
||||||
defaultExperience: this.options.collection.container.defaultExperience(),
|
defaultExperience: this.collection.container.defaultExperience(),
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
tabTitle: this.tabTitle,
|
tabTitle: this.tabTitle,
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
errorStack: getErrorStack(error),
|
errorStack: getErrorStack(error),
|
||||||
},
|
},
|
||||||
this.options.onLoadStartKey
|
this.onLoadStartKey
|
||||||
);
|
|
||||||
logConsoleError(
|
|
||||||
`Error while fetching container settings for container ${this.options.collection.id()}: ${errorMessage}`
|
|
||||||
);
|
);
|
||||||
|
logConsoleError(`Error while fetching container settings for container ${this.collection.id()}: ${errorMessage}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
this.offerRead(true);
|
this.offerRead(true);
|
||||||
this.isExecuting(false);
|
this.isExecuting(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onActivate();
|
super.onActivate();
|
||||||
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.SettingsV2);
|
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSettingsTabContainer(): Explorer {
|
export class DatabaseSettingsTabV2 extends SettingsTabV2 {
|
||||||
return this.getContainer();
|
private notificationRead: ko.Observable<boolean>;
|
||||||
|
private notification: DataModels.Notification;
|
||||||
|
|
||||||
|
constructor(options: ViewModels.TabOptions) {
|
||||||
|
super(options);
|
||||||
|
this.tabId = "DatabaseSettingsV2-" + this.tabId;
|
||||||
|
this.notificationRead = ko.observable(false);
|
||||||
|
this.settingsComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||||
|
if (this.notificationRead()) {
|
||||||
|
this.pendingNotification(this.notification);
|
||||||
|
this.notification = undefined;
|
||||||
|
this.notificationRead(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async onActivate(): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.isExecuting(true);
|
||||||
|
|
||||||
|
const data: DataModels.Notification = await this.database.getPendingThroughputSplitNotification();
|
||||||
|
this.notification = data;
|
||||||
|
this.notificationRead(true);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = getErrorMessage(error);
|
||||||
|
this.notification = undefined;
|
||||||
|
this.notificationRead(true);
|
||||||
|
traceFailure(
|
||||||
|
Action.Tab,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.database?.container.databaseAccount().name,
|
||||||
|
databaseName: this.database.id(),
|
||||||
|
defaultExperience: this.database?.container.defaultExperience(),
|
||||||
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
|
tabTitle: this.tabTitle,
|
||||||
|
error: errorMessage,
|
||||||
|
errorStack: getErrorStack(error),
|
||||||
|
},
|
||||||
|
this.onLoadStartKey
|
||||||
|
);
|
||||||
|
logConsoleError(`Error while fetching database settings for database ${this.database.id()}: ${errorMessage}`);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this.isExecuting(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onActivate();
|
||||||
|
this.database.selectedSubnodeKind(ViewModels.CollectionTabKind.DatabaseSettingsV2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default class SparkMasterTab extends TabsBase {
|
|||||||
this.sparkMasterSrc = ko.observable<string>(sparkMasterEndpoint && sparkMasterEndpoint.endpoint);
|
this.sparkMasterSrc = ko.observable<string>(sparkMasterEndpoint && sparkMasterEndpoint.endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getContainer() {
|
public getContainer() {
|
||||||
return this._container;
|
return this._container;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||||||
return Q();
|
return Q();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getContainer(): Explorer {
|
public getContainer(): Explorer {
|
||||||
return (this.collection && this.collection.container) || (this.database && this.database.container);
|
return (this.collection && this.collection.container) || (this.database && this.database.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -143,7 +143,11 @@
|
|||||||
<!-- /ko -->
|
<!-- /ko -->
|
||||||
|
|
||||||
<!-- ko if: $data.tabKind === 20 -->
|
<!-- ko if: $data.tabKind === 20 -->
|
||||||
<settings-tab-v2 params="{data: $data}"></settings-tab-v2>
|
<collection-settings-tab-v2 params="{data: $data}"></collection-settings-tab-v2>
|
||||||
|
<!-- /ko -->
|
||||||
|
|
||||||
|
<!-- ko if: $data.tabKind === 21 -->
|
||||||
|
<database-settings-tab-v2 params="{data: $data}"></database-settings-tab-v2>
|
||||||
<!-- /ko -->
|
<!-- /ko -->
|
||||||
</div>
|
</div>
|
||||||
<!-- /ko -->
|
<!-- /ko -->
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default class TerminalTab extends TabsBase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getContainer(): Explorer {
|
public getContainer(): Explorer {
|
||||||
return this.container;
|
return this.container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -27,7 +26,7 @@ import MongoQueryTab from "../Tabs/MongoQueryTab";
|
|||||||
import MongoShellTab from "../Tabs/MongoShellTab";
|
import MongoShellTab from "../Tabs/MongoShellTab";
|
||||||
import QueryTab from "../Tabs/QueryTab";
|
import QueryTab from "../Tabs/QueryTab";
|
||||||
import QueryTablesTab from "../Tabs/QueryTablesTab";
|
import QueryTablesTab from "../Tabs/QueryTablesTab";
|
||||||
import SettingsTabV2 from "../Tabs/SettingsTabV2";
|
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
|
||||||
import ConflictId from "./ConflictId";
|
import ConflictId from "./ConflictId";
|
||||||
import DocumentId from "./DocumentId";
|
import DocumentId from "./DocumentId";
|
||||||
import StoredProcedure from "./StoredProcedure";
|
import StoredProcedure from "./StoredProcedure";
|
||||||
@@ -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,10 +544,12 @@ 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 matchingTabs = this.container.tabsManager.getTabs(
|
||||||
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.SettingsV2, (tab) => {
|
ViewModels.CollectionTabKind.CollectionSettingsV2,
|
||||||
|
(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();
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const traceStartData = {
|
const traceStartData = {
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
@@ -572,26 +571,20 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
|
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
|
||||||
};
|
};
|
||||||
|
|
||||||
let settingsTabV2 = matchingTabs && (matchingTabs[0] as SettingsTabV2);
|
let settingsTabV2 = matchingTabs && (matchingTabs[0] as CollectionSettingsTabV2);
|
||||||
this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions, pendingNotificationsPromise);
|
this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
private launchSettingsTabV2 = (
|
private launchSettingsTabV2 = (
|
||||||
settingsTabV2: SettingsTabV2,
|
settingsTabV2: CollectionSettingsTabV2,
|
||||||
traceStartData: any,
|
traceStartData: any,
|
||||||
settingsTabOptions: ViewModels.TabOptions,
|
settingsTabOptions: ViewModels.TabOptions
|
||||||
getPendingNotification: Q.Promise<DataModels.Notification>
|
|
||||||
): void => {
|
): void => {
|
||||||
const settingsTabV2Options: ViewModels.SettingsTabV2Options = {
|
|
||||||
...settingsTabOptions,
|
|
||||||
getPendingNotification: getPendingNotification,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!settingsTabV2) {
|
if (!settingsTabV2) {
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, traceStartData);
|
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, traceStartData);
|
||||||
settingsTabV2Options.onLoadStartKey = startKey;
|
settingsTabOptions.onLoadStartKey = startKey;
|
||||||
settingsTabV2Options.tabKind = ViewModels.CollectionTabKind.SettingsV2;
|
settingsTabOptions.tabKind = ViewModels.CollectionTabKind.CollectionSettingsV2;
|
||||||
settingsTabV2 = new SettingsTabV2(settingsTabV2Options);
|
settingsTabV2 = new CollectionSettingsTabV2(settingsTabOptions);
|
||||||
this.container.tabsManager.activateNewTab(settingsTabV2);
|
this.container.tabsManager.activateNewTab(settingsTabV2);
|
||||||
} else {
|
} else {
|
||||||
this.container.tabsManager.activateTab(settingsTabV2);
|
this.container.tabsManager.activateTab(settingsTabV2);
|
||||||
@@ -976,23 +969,19 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
this.uploadFiles(event.originalEvent.dataTransfer.files);
|
this.uploadFiles(event.originalEvent.dataTransfer.files);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteCollectionContextMenuClick(source: ViewModels.Collection, event: MouseEvent | KeyboardEvent) {
|
public uploadFiles = (fileList: FileList): Promise<UploadDetails> => {
|
||||||
this.container.deleteCollectionConfirmationPane.open();
|
|
||||||
}
|
|
||||||
|
|
||||||
public uploadFiles = (fileList: FileList): Q.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 +990,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 +1024,68 @@ 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> {
|
public async getPendingThroughputSplitNotification(): Promise<DataModels.Notification> {
|
||||||
const deferred: Q.Deferred<UploadDetails> = Q.defer<UploadDetails>();
|
if (!this.container) {
|
||||||
const promises: Array<Q.Promise<UploadDetailsRecord>> = [];
|
return undefined;
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
|
||||||
promises.push(this._uploadFile(files[i]));
|
|
||||||
}
|
}
|
||||||
Q.all(promises).then((uploadDetails: Array<UploadDetailsRecord>) => {
|
|
||||||
deferred.resolve({ data: uploadDetails });
|
try {
|
||||||
|
const notifications: DataModels.Notification[] = await fetchPortalNotifications();
|
||||||
|
if (!notifications || notifications.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.find(notifications, (notification: DataModels.Notification) => {
|
||||||
|
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress");
|
||||||
|
return (
|
||||||
|
notification.kind === "message" &&
|
||||||
|
notification.collectionName === this.id() &&
|
||||||
|
notification.description &&
|
||||||
|
throughputUpdateRegExp.test(notification.description)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
Logger.logError(
|
||||||
|
JSON.stringify({
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
accountName: this.container && this.container.databaseAccount(),
|
||||||
|
databaseName: this.databaseId,
|
||||||
|
collectionName: this.id(),
|
||||||
|
}),
|
||||||
|
"Settings tree node"
|
||||||
|
);
|
||||||
|
|
||||||
return deferred.promise;
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _uploadFile(file: File): Q.Promise<UploadDetailsRecord> {
|
private async _uploadFilesCors(files: FileList): Promise<UploadDetails> {
|
||||||
const deferred: Q.Deferred<UploadDetailsRecord> = Q.defer();
|
const data = await Promise.all(Array.from(files).map((file) => this._uploadFile(file)));
|
||||||
|
|
||||||
|
return { data };
|
||||||
|
}
|
||||||
|
|
||||||
|
private _uploadFile(file: File): Promise<UploadDetailsRecord> {
|
||||||
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 +1093,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,48 +1131,6 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
|
|
||||||
if (!this.container) {
|
|
||||||
return Q.resolve(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
const deferred: Q.Deferred<DataModels.Notification> = Q.defer<DataModels.Notification>();
|
|
||||||
fetchPortalNotifications().then(
|
|
||||||
(notifications: DataModels.Notification[]) => {
|
|
||||||
if (!notifications || notifications.length === 0) {
|
|
||||||
deferred.resolve(undefined);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pendingNotification = _.find(notifications, (notification: DataModels.Notification) => {
|
|
||||||
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress");
|
|
||||||
return (
|
|
||||||
notification.kind === "message" &&
|
|
||||||
notification.collectionName === this.id() &&
|
|
||||||
notification.description &&
|
|
||||||
throughputUpdateRegExp.test(notification.description)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
deferred.resolve(pendingNotification);
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
Logger.logError(
|
|
||||||
JSON.stringify({
|
|
||||||
error: getErrorMessage(error),
|
|
||||||
accountName: this.container && this.container.databaseAccount(),
|
|
||||||
databaseName: this.databaseId,
|
|
||||||
collectionName: this.id(),
|
|
||||||
}),
|
|
||||||
"Settings tree node"
|
|
||||||
);
|
|
||||||
deferred.resolve(undefined);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _logUploadDetailsInConsole(uploadDetails: UploadDetails): void {
|
private _logUploadDetailsInConsole(uploadDetails: UploadDetails): void {
|
||||||
const uploadDetailsRecords: UploadDetailsRecord[] = uploadDetails.data;
|
const uploadDetailsRecords: UploadDetailsRecord[] = uploadDetails.data;
|
||||||
const numFiles: number = uploadDetailsRecords.length;
|
const numFiles: number = uploadDetailsRecords.length;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import Q from "q";
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import DatabaseSettingsTab from "../Tabs/DatabaseSettingsTab";
|
import DatabaseSettingsTab from "../Tabs/DatabaseSettingsTab";
|
||||||
|
import { DatabaseSettingsTabV2 } from "../Tabs/SettingsTabV2";
|
||||||
import Collection from "./Collection";
|
import Collection from "./Collection";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
@@ -16,7 +16,6 @@ import { readCollections } from "../../Common/dataAccess/readCollections";
|
|||||||
import { JunoClient, IJunoResponse } from "../../Juno/JunoClient";
|
import { JunoClient, IJunoResponse } from "../../Juno/JunoClient";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
|
||||||
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
|
||||||
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
|
||||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||||
|
|
||||||
@@ -59,12 +58,17 @@ export default class Database implements ViewModels.Database {
|
|||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
});
|
});
|
||||||
|
|
||||||
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
|
const pendingNotificationsPromise: Promise<DataModels.Notification> = this.getPendingThroughputSplitNotification();
|
||||||
const matchingTabs = this.container.tabsManager.getTabs(
|
const useDatabaseSettingsTabV1: boolean = this.container.isFeatureEnabled(
|
||||||
ViewModels.CollectionTabKind.DatabaseSettings,
|
Constants.Features.enableDatabaseSettingsTabV1
|
||||||
(tab) => tab.node?.id() === this.id()
|
|
||||||
);
|
);
|
||||||
let settingsTab: DatabaseSettingsTab = matchingTabs && (matchingTabs[0] as DatabaseSettingsTab);
|
const tabKind: ViewModels.CollectionTabKind = useDatabaseSettingsTabV1
|
||||||
|
? ViewModels.CollectionTabKind.DatabaseSettings
|
||||||
|
: ViewModels.CollectionTabKind.DatabaseSettingsV2;
|
||||||
|
const matchingTabs = this.container.tabsManager.getTabs(tabKind, (tab) => tab.node?.id() === this.id());
|
||||||
|
let settingsTab: DatabaseSettingsTab | DatabaseSettingsTabV2 = useDatabaseSettingsTabV1
|
||||||
|
? (matchingTabs?.[0] as DatabaseSettingsTab)
|
||||||
|
: (matchingTabs?.[0] as DatabaseSettingsTabV2);
|
||||||
if (!settingsTab) {
|
if (!settingsTab) {
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
|
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
@@ -75,9 +79,11 @@ export default class Database implements ViewModels.Database {
|
|||||||
});
|
});
|
||||||
pendingNotificationsPromise.then(
|
pendingNotificationsPromise.then(
|
||||||
(data: any) => {
|
(data: any) => {
|
||||||
const pendingNotification: DataModels.Notification = data && data[0];
|
const pendingNotification: DataModels.Notification = data?.[0];
|
||||||
settingsTab = new DatabaseSettingsTab({
|
const tabOptions: ViewModels.TabOptions = {
|
||||||
tabKind: ViewModels.CollectionTabKind.DatabaseSettings,
|
tabKind: useDatabaseSettingsTabV1
|
||||||
|
? ViewModels.CollectionTabKind.DatabaseSettings
|
||||||
|
: ViewModels.CollectionTabKind.DatabaseSettingsV2,
|
||||||
title: "Scale",
|
title: "Scale",
|
||||||
tabPath: "",
|
tabPath: "",
|
||||||
node: this,
|
node: this,
|
||||||
@@ -87,8 +93,10 @@ export default class Database implements ViewModels.Database {
|
|||||||
isActive: ko.observable(false),
|
isActive: ko.observable(false),
|
||||||
onLoadStartKey: startKey,
|
onLoadStartKey: startKey,
|
||||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
|
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
|
||||||
});
|
};
|
||||||
|
settingsTab = useDatabaseSettingsTabV1
|
||||||
|
? new DatabaseSettingsTab(tabOptions)
|
||||||
|
: new DatabaseSettingsTabV2(tabOptions);
|
||||||
settingsTab.pendingNotification(pendingNotification);
|
settingsTab.pendingNotification(pendingNotification);
|
||||||
this.container.tabsManager.activateNewTab(settingsTab);
|
this.container.tabsManager.activateNewTab(settingsTab);
|
||||||
},
|
},
|
||||||
@@ -221,20 +229,18 @@ export default class Database implements ViewModels.Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
|
public 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>();
|
try {
|
||||||
fetchPortalNotifications().then(
|
const notifications: DataModels.Notification[] = await fetchPortalNotifications();
|
||||||
(notifications) => {
|
|
||||||
if (!notifications || notifications.length === 0) {
|
if (!notifications || notifications.length === 0) {
|
||||||
deferred.resolve(undefined);
|
return undefined;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const pendingNotification = _.find(notifications, (notification: DataModels.Notification) => {
|
return _.find(notifications, (notification: DataModels.Notification) => {
|
||||||
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress");
|
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress");
|
||||||
return (
|
return (
|
||||||
notification.kind === "message" &&
|
notification.kind === "message" &&
|
||||||
@@ -244,10 +250,7 @@ export default class Database implements ViewModels.Database {
|
|||||||
throughputUpdateRegExp.test(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),
|
||||||
@@ -257,11 +260,9 @@ export default class Database implements ViewModels.Database {
|
|||||||
}),
|
}),
|
||||||
"Settings tree node"
|
"Settings tree node"
|
||||||
);
|
);
|
||||||
deferred.resolve(undefined);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return deferred.promise;
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getDeltaCollections(
|
private getDeltaCollections(
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -15,12 +15,13 @@ import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
|
|||||||
import RefreshIcon from "../../../images/refresh-cosmos.svg";
|
import RefreshIcon from "../../../images/refresh-cosmos.svg";
|
||||||
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
|
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
|
||||||
import FileIcon from "../../../images/notebook/file-cosmos.svg";
|
import FileIcon from "../../../images/notebook/file-cosmos.svg";
|
||||||
|
import PublishIcon from "../../../images/notebook/publish_content.svg";
|
||||||
import { ArrayHashMap } from "../../Common/ArrayHashMap";
|
import { ArrayHashMap } from "../../Common/ArrayHashMap";
|
||||||
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { Areas } from "../../Common/Constants";
|
import { Areas } from "../../Common/Constants";
|
||||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
import GalleryIcon from "../../../images/GalleryIcon.svg";
|
import GalleryIcon from "../../../images/GalleryIcon.svg";
|
||||||
@@ -716,6 +717,23 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (this.container.isGalleryPublishEnabled() && item.type === NotebookContentItemType.Notebook) {
|
||||||
|
items.push({
|
||||||
|
label: "Publish to gallery",
|
||||||
|
iconSrc: PublishIcon,
|
||||||
|
onClick: async () => {
|
||||||
|
TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, {
|
||||||
|
source: Source.ResourceTreeMenu,
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = await this.container.readFile(item);
|
||||||
|
if (content) {
|
||||||
|
await this.container.publishNotebook(item.name, content);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// "Copy to ..." isn't needed if github locations are not available
|
// "Copy to ..." isn't needed if github locations are not available
|
||||||
if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
||||||
items = items.filter((item) => item.label !== "Copy to ...");
|
items = items.filter((item) => item.label !== "Copy to ...");
|
||||||
|
|||||||
5
src/GalleryViewer/GalleryViewer.less
Normal file
5
src/GalleryViewer/GalleryViewer.less
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@import "../../less/Common/Constants";
|
||||||
|
|
||||||
|
.standalone-gallery-root {
|
||||||
|
background: @GalleryBackgroundColor;
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import "bootstrap/dist/css/bootstrap.css";
|
import "bootstrap/dist/css/bootstrap.css";
|
||||||
|
import "./GalleryViewer.less";
|
||||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||||
import { Text, Link } from "office-ui-fabric-react";
|
import { Text, Link } from "office-ui-fabric-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
import { initializeConfiguration } from "../ConfigContext";
|
import { configContext, initializeConfiguration } from "../ConfigContext";
|
||||||
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
|
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
|
||||||
import {
|
import {
|
||||||
GalleryAndNotebookViewerComponent,
|
GalleryAndNotebookViewerComponent,
|
||||||
@@ -24,6 +25,7 @@ const onInit = async () => {
|
|||||||
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
|
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
|
||||||
|
|
||||||
const props: GalleryAndNotebookViewerComponentProps = {
|
const props: GalleryAndNotebookViewerComponentProps = {
|
||||||
|
isGalleryPublishEnabled: configContext.ENABLE_GALLERY_PUBLISH,
|
||||||
junoClient: new JunoClient(),
|
junoClient: new JunoClient(),
|
||||||
selectedTab: galleryViewerProps.selectedTab || GalleryTab.OfficialSamples,
|
selectedTab: galleryViewerProps.selectedTab || GalleryTab.OfficialSamples,
|
||||||
sortBy: galleryViewerProps.sortBy || SortBy.MostViewed,
|
sortBy: galleryViewerProps.sortBy || SortBy.MostViewed,
|
||||||
@@ -31,7 +33,7 @@ const onInit = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const element = (
|
const element = (
|
||||||
<>
|
<div className="standalone-gallery-root">
|
||||||
<header>
|
<header>
|
||||||
<GalleryHeaderComponent />
|
<GalleryHeaderComponent />
|
||||||
</header>
|
</header>
|
||||||
@@ -52,7 +54,7 @@ const onInit = async () => {
|
|||||||
|
|
||||||
<GalleryAndNotebookViewerComponent {...props} />
|
<GalleryAndNotebookViewerComponent {...props} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
ReactDOM.render(element, document.getElementById("galleryContent"));
|
ReactDOM.render(element, document.getElementById("galleryContent"));
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,7 +225,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}/downloads`,
|
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}/downloads`,
|
||||||
{
|
{
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -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/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}/favorite`,
|
||||||
{
|
{
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -270,13 +270,16 @@ describe("Gallery", () => {
|
|||||||
|
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/unfavorite`, {
|
expect(window.fetch).toBeCalledWith(
|
||||||
|
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}/unfavorite`,
|
||||||
|
{
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
[authorizationHeader.header]: authorizationHeader.token,
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
[HttpHeaders.contentType]: "application/json",
|
[HttpHeaders.contentType]: "application/json",
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getFavoriteNotebooks", async () => {
|
it("getFavoriteNotebooks", async () => {
|
||||||
@@ -289,12 +292,15 @@ describe("Gallery", () => {
|
|||||||
|
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/favorites`, {
|
expect(window.fetch).toBeCalledWith(
|
||||||
|
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/favorites`,
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
[authorizationHeader.header]: authorizationHeader.token,
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
[HttpHeaders.contentType]: "application/json",
|
[HttpHeaders.contentType]: "application/json",
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getPublishedNotebooks", async () => {
|
it("getPublishedNotebooks", async () => {
|
||||||
@@ -308,7 +314,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/${sampleSubscriptionId}/gallery/published`,
|
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/published`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
[authorizationHeader.header]: authorizationHeader.token,
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
@@ -329,13 +335,16 @@ describe("Gallery", () => {
|
|||||||
|
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}`, {
|
expect(window.fetch).toBeCalledWith(
|
||||||
|
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}`,
|
||||||
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
[authorizationHeader.header]: authorizationHeader.token,
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
[HttpHeaders.contentType]: "application/json",
|
[HttpHeaders.contentType]: "application/json",
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("publishNotebook", async () => {
|
it("publishNotebook", async () => {
|
||||||
@@ -364,7 +373,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/${sampleSubscriptionId}/${sampleDatabaseAccount.name}/gallery`,
|
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery`,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
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,11 +185,8 @@ 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, { headers: JunoClient.getHeaders() });
|
||||||
method: "PATCH",
|
|
||||||
headers: JunoClient.getHeaders(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let data: IPublicGalleryData;
|
let data: IPublicGalleryData;
|
||||||
if (response.status === HttpStatusCodes.OK) {
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
@@ -197,7 +200,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,11 +218,8 @@ 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, { headers: JunoClient.getHeaders() });
|
||||||
method: "PATCH",
|
|
||||||
headers: JunoClient.getHeaders(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let data: boolean;
|
let data: boolean;
|
||||||
if (response.status === HttpStatusCodes.OK) {
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
@@ -277,7 +277,7 @@ export class JunoClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async increaseNotebookDownloadCount(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
public async increaseNotebookDownloadCount(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
||||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery/${id}/downloads`, {
|
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/${id}/downloads`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
});
|
});
|
||||||
@@ -294,7 +294,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(),
|
||||||
});
|
});
|
||||||
@@ -311,7 +311,7 @@ export class JunoClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async unfavoriteNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
public async unfavoriteNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
||||||
const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/${id}/unfavorite`, {
|
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/${id}/unfavorite`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
});
|
});
|
||||||
@@ -328,19 +328,19 @@ export class JunoClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getFavoriteNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
public async getFavoriteNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
||||||
return await this.getNotebooks(`${this.getNotebooksUrl()}/gallery/favorites`, {
|
return await this.getNotebooks(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/favorites`, {
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPublishedNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
public async getPublishedNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
||||||
return await this.getNotebooks(`${this.getNotebooksUrl()}/${this.getSubscriptionId()}/gallery/published`, {
|
return await this.getNotebooks(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/published`, {
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
public async deleteNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
||||||
const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/${id}`, {
|
const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
});
|
});
|
||||||
@@ -365,9 +365,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 +377,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) {
|
||||||
@@ -498,8 +495,8 @@ export class JunoClient {
|
|||||||
return userContext.subscriptionId;
|
return userContext.subscriptionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNotebooksAccountUrl(): string {
|
private getNotebooksSubscriptionIdAccountUrl(): string {
|
||||||
return `${this.getNotebooksUrl()}/${this.getAccount()}`;
|
return `${this.getNotebooksUrl()}/subscriptions/${this.getSubscriptionId()}/databaseAccounts/${this.getAccount()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAnalyticsUrl(): string {
|
private getAnalyticsUrl(): string {
|
||||||
|
|||||||
50
src/Localization/en/translations.json
Normal file
50
src/Localization/en/translations.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"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": {
|
||||||
|
"DedicatedGatewayDescription": "Provisioning dedicated gateways for SqlX accounts.",
|
||||||
|
"DedicatedGateway": "Dedicated Gateway",
|
||||||
|
"Enable": "Enable",
|
||||||
|
"Disable": "Disable",
|
||||||
|
"LearnAboutDedicatedGateway": "Learn more about dedicated gateway.",
|
||||||
|
"SKUs": "SKUs",
|
||||||
|
"NumberOfInstances": "Number of instances",
|
||||||
|
"CosmosD4s": "Cosmos.D4s",
|
||||||
|
"CosmosD8s": "Cosmos.D8s",
|
||||||
|
"CosmosD16s": "Cosmos.D16s",
|
||||||
|
"CosmosD32s": "Cosmos.D32s",
|
||||||
|
"CreateMessage": "DedicatedGateway resource is being created.",
|
||||||
|
"UpdateMessage": "DedicatedGateway resource is being updated.",
|
||||||
|
"DeleteMessage": "DedicatedGateway resource is being deleted.",
|
||||||
|
"CannotSave": "Cannot save the changes to the DedicatedGateway resource at the moment",
|
||||||
|
"DedicatedGatewayEndpoint": "DedicatedGatewayEndpoint",
|
||||||
|
"NoValue": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
237
src/Main.tsx
237
src/Main.tsx
@@ -14,6 +14,7 @@ import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
|
|||||||
import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
|
import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
|
||||||
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
|
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
|
||||||
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
|
||||||
|
import "./Explorer/Panes/PanelComponent.less";
|
||||||
import "../less/TableStyles/queryBuilder.less";
|
import "../less/TableStyles/queryBuilder.less";
|
||||||
import "../externals/jquery.dataTables.min.css";
|
import "../externals/jquery.dataTables.min.css";
|
||||||
import "../less/TableStyles/fulldatatables.less";
|
import "../less/TableStyles/fulldatatables.less";
|
||||||
@@ -53,215 +54,40 @@ 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 { useSidePanel } from "./hooks/useSidePanel";
|
||||||
import { emulatorAccount } from "./Platform/Emulator/emulatorAccount";
|
import { NotificationConsoleComponent } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { HostedExplorerChildFrame } from "./HostedExplorerChildFrame";
|
import { PanelContainerComponent } from "./Explorer/Panes/PanelContainerComponent";
|
||||||
import {
|
|
||||||
getDatabaseAccountKindFromExperience,
|
initializeIcons();
|
||||||
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;
|
|
||||||
explorer = new Explorer();
|
|
||||||
explorer.selfServeType(SelfServeType.none);
|
|
||||||
if (win.hostedConfig.authType === AuthType.EncryptedToken) {
|
|
||||||
// TODO: Remove window.authType
|
|
||||||
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(
|
const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel();
|
||||||
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(
|
const explorerParams: ExplorerParams = {
|
||||||
win.hostedConfig.encryptedTokenMetadata.apiKind
|
setIsNotificationConsoleExpanded,
|
||||||
);
|
setNotificationConsoleData,
|
||||||
explorer.initDataExplorerWithFrameInputs({
|
setInProgressConsoleDataIdToBeDeleted,
|
||||||
databaseAccount: {
|
openSidePanel,
|
||||||
id: "",
|
closeSidePanel,
|
||||||
// id: Main._databaseAccountId,
|
};
|
||||||
name: win.hostedConfig.encryptedTokenMetadata.accountName,
|
const config = useConfig();
|
||||||
kind: getDatabaseAccountKindFromExperience(apiExperience),
|
useKnockoutExplorer(config, explorerParams);
|
||||||
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 +289,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">
|
||||||
@@ -485,6 +317,13 @@ const App: React.FunctionComponent = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Global loader - End */}
|
{/* Global loader - End */}
|
||||||
|
<PanelContainerComponent
|
||||||
|
isOpen={isPanelOpen}
|
||||||
|
panelContent={panelContent}
|
||||||
|
headerText={headerText}
|
||||||
|
closePanel={closeSidePanel}
|
||||||
|
isConsoleExpanded={isNotificationConsoleExpanded}
|
||||||
|
/>
|
||||||
<div data-bind="react:uploadItemsPaneAdapter" />
|
<div data-bind="react:uploadItemsPaneAdapter" />
|
||||||
<div data-bind='component: { name: "add-database-pane", params: {data: addDatabasePane} }' />
|
<div data-bind='component: { name: "add-database-pane", params: {data: addDatabasePane} }' />
|
||||||
<div data-bind='component: { name: "add-collection-pane", params: { data: addCollectionPane} }' />
|
<div data-bind='component: { name: "add-collection-pane", params: { data: addCollectionPane} }' />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user