mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-26 04:11:24 +00:00
Compare commits
1 Commits
sqlxEdits
...
users/tami
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acdd05a79b |
@@ -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.tsx
|
src/Explorer/Explorer.ts
|
||||||
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
|
||||||
|
|||||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -156,7 +156,6 @@ 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
|
||||||
@@ -166,8 +165,6 @@ 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,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
|
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com"
|
||||||
"ENABLE_GALLERY_PUBLISH": true
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 329 B |
@@ -57,13 +57,6 @@
|
|||||||
|
|
||||||
@FocusColor: #605e5c;
|
@FocusColor: #605e5c;
|
||||||
|
|
||||||
@GalleryBackgroundColor: #fdfdfd;
|
|
||||||
|
|
||||||
//Icons
|
|
||||||
@InfoIconColor: #0072c6;
|
|
||||||
@WarningIconColor: #db7500;
|
|
||||||
@ErrorIconColor: #b91f26;
|
|
||||||
|
|
||||||
/******************************************************************************
|
/******************************************************************************
|
||||||
METRICS
|
METRICS
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|||||||
@@ -1523,21 +1523,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -3043,45 +3028,3 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
4249
package-lock.json
generated
4249
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
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.2",
|
"@jupyterlab/services": "6.0.0-rc.2",
|
||||||
"@jupyterlab/terminal": "3.0.3",
|
"@jupyterlab/terminal": "3.0.0-rc.2",
|
||||||
"@microsoft/applicationinsights-web": "2.5.9",
|
"@microsoft/applicationinsights-web": "2.5.9",
|
||||||
"@nteract/commutable": "7.4.2",
|
"@nteract/commutable": "7.3.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,9 +64,6 @@
|
|||||||
"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",
|
||||||
@@ -89,7 +86,6 @@
|
|||||||
"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",
|
||||||
@@ -128,7 +124,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": "17.0.0",
|
"@types/react": "16.9.56",
|
||||||
"@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",
|
||||||
|
|||||||
@@ -119,9 +119,7 @@ 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
|
||||||
@@ -130,7 +128,6 @@ 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");
|
||||||
return undefined;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export 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
|
||||||
@@ -80,11 +79,7 @@ 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();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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";
|
||||||
@@ -91,7 +92,6 @@ 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(): void;
|
expandCollection(): Q.Promise<any>;
|
||||||
collapseCollection(): void;
|
collapseCollection(): void;
|
||||||
getDatabase(): Database;
|
getDatabase(): Database;
|
||||||
}
|
}
|
||||||
@@ -138,6 +138,7 @@ 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;
|
||||||
@@ -175,10 +176,9 @@ 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): Promise<UploadDetails>;
|
uploadFiles(fileList: FileList): Q.Promise<UploadDetails>;
|
||||||
|
|
||||||
getLabel(): string;
|
getLabel(): string;
|
||||||
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -293,6 +293,10 @@ 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>;
|
||||||
@@ -359,8 +363,7 @@ export enum CollectionTabKind {
|
|||||||
Gallery = 17,
|
Gallery = 17,
|
||||||
NotebookViewer = 18,
|
NotebookViewer = 18,
|
||||||
Schema = 19,
|
Schema = 19,
|
||||||
CollectionSettingsV2 = 20,
|
SettingsV2 = 20,
|
||||||
DatabaseSettingsV2 = 21,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TerminalKind {
|
export enum TerminalKind {
|
||||||
|
|||||||
@@ -45,8 +45,7 @@ describe("Component Registerer", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should register settings-tab-v2 component", () => {
|
it("should register settings-tab-v2 component", () => {
|
||||||
expect(ko.components.isRegistered("database-settings-tab-v2")).toBe(true);
|
expect(ko.components.isRegistered("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", () => {
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponent
|
|||||||
ko.components.register("tabs-manager", TabsManagerKOComponent());
|
ko.components.register("tabs-manager", TabsManagerKOComponent());
|
||||||
|
|
||||||
// Collection Tabs
|
// Collection Tabs
|
||||||
ko.components.register("documents-tab", new TabComponents.DocumentsTab());
|
ko.components.register("documents-tab", new TabComponents.MongoDocumentsTabV2());
|
||||||
ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab());
|
ko.components.register("mongo-documents-tab", new TabComponents.MongoDocumentsTab());
|
||||||
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("collection-settings-tab-v2", new TabComponents.SettingsTabV2());
|
ko.components.register("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,7 +45,6 @@ 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,7 +112,10 @@ export class ResourceTreeContextMenuButtonFactory {
|
|||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteCollectionIcon,
|
iconSrc: DeleteCollectionIcon,
|
||||||
onClick: () => container.openDeleteCollectionConfirmationPane(),
|
onClick: () => {
|
||||||
|
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
|
||||||
|
selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null);
|
||||||
|
},
|
||||||
label: container.deleteCollectionText(),
|
label: container.deleteCollectionText(),
|
||||||
styleClass: "deleteCollectionMenuItem",
|
styleClass: "deleteCollectionMenuItem",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,13 +3,7 @@ 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 {
|
import { ChoiceGroup, FontIcon, IChoiceGroupProps } from "office-ui-fabric-react";
|
||||||
ChoiceGroup,
|
|
||||||
FontIcon,
|
|
||||||
IChoiceGroupProps,
|
|
||||||
IProgressIndicatorProps,
|
|
||||||
ProgressIndicator,
|
|
||||||
} from "office-ui-fabric-react";
|
|
||||||
|
|
||||||
export interface TextFieldProps extends ITextFieldProps {
|
export interface TextFieldProps extends ITextFieldProps {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -33,7 +27,6 @@ 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;
|
||||||
@@ -69,14 +62,13 @@ 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, isDarkOverlay: false },
|
modalProps: { isBlocking: this.props.isModal },
|
||||||
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,
|
||||||
@@ -99,7 +91,6 @@ 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,6 +18,7 @@ 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;
|
||||||
@@ -37,7 +38,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 = 80;
|
private static readonly cardDescriptionMaxChars = 88;
|
||||||
private static readonly cardItemGapBig = 10;
|
private static readonly cardItemGapBig = 10;
|
||||||
private static readonly cardItemGapSmall = 8;
|
private static readonly cardItemGapSmall = 8;
|
||||||
|
|
||||||
@@ -53,7 +54,6 @@ 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,16 +79,12 @@ 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 ? (
|
{this.props.data.tags?.map((tag, index, array) => (
|
||||||
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
|
||||||
@@ -105,14 +101,13 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text variant="small" styles={{ root: { height: 36 } }}>
|
<Text variant="small" styles={{ root: { height: 36 } }}>
|
||||||
{this.renderTruncatedDescription()}
|
{this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{this.props.data.views !== undefined && this.generateIconText("RedEye", this.props.data.views.toString())}
|
{this.generateIconText("RedEye", this.props.data.views.toString())}
|
||||||
{this.props.data.downloads !== undefined &&
|
{this.generateIconText("Download", this.props.data.downloads.toString())}
|
||||||
this.generateIconText("Download", this.props.data.downloads.toString())}
|
{this.props.isFavorite !== undefined &&
|
||||||
{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>
|
||||||
@@ -132,7 +127,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 ? "Unfavorite" : "Favorite",
|
this.props.isFavorite ? "Unlike" : "Like",
|
||||||
"left",
|
"left",
|
||||||
this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick
|
this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick
|
||||||
)}
|
)}
|
||||||
@@ -149,17 +144,12 @@ 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 variant="tiny" styles={{ root: { color: "#605E5C", paddingRight: GalleryCardComponent.cardItemGapSmall } }}>
|
<Text
|
||||||
|
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,11 +5,6 @@ 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,
|
||||||
@@ -93,7 +88,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"color": "#605E5C",
|
"color": undefined,
|
||||||
"paddingRight": 8,
|
"paddingRight": 8,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -117,7 +112,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"color": "#605E5C",
|
"color": undefined,
|
||||||
"paddingRight": 8,
|
"paddingRight": 8,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -141,7 +136,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
"root": Object {
|
"root": Object {
|
||||||
"color": "#605E5C",
|
"color": undefined,
|
||||||
"paddingRight": 8,
|
"paddingRight": 8,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -190,7 +185,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
"gapSpace": 0,
|
"gapSpace": 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
content="Favorite"
|
content="Like"
|
||||||
id="TooltipHost-IconButton-Heart"
|
id="TooltipHost-IconButton-Heart"
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
@@ -202,14 +197,14 @@ exports[`GalleryCardComponent renders 1`] = `
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CustomizedIconButton
|
<CustomizedIconButton
|
||||||
ariaLabel="Favorite"
|
ariaLabel="Like"
|
||||||
iconProps={
|
iconProps={
|
||||||
Object {
|
Object {
|
||||||
"iconName": "Heart",
|
"iconName": "Heart",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
title="Favorite"
|
title="Like"
|
||||||
/>
|
/>
|
||||||
</StyledTooltipHostBase>
|
</StyledTooltipHostBase>
|
||||||
<StyledTooltipHostBase
|
<StyledTooltipHostBase
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ 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 { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
|
import { 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;
|
||||||
@@ -16,11 +14,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);
|
||||||
@@ -29,34 +27,23 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
|
|||||||
readCodeOfConduct: false,
|
readCodeOfConduct: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct";
|
this.descriptionPara1 = "Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement";
|
||||||
this.descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.";
|
this.descriptionPara2 =
|
||||||
this.descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the ";
|
"Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.";
|
||||||
this.link1 = { label: "code of conduct.", url: CodeOfConductEndpoints.codeOfConduct };
|
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.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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,11 +53,6 @@ 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>
|
||||||
@@ -87,6 +69,10 @@ 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>
|
||||||
|
|
||||||
@@ -101,7 +87,7 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
label="I have read and accept the code of conduct."
|
label="I have read and accepted the code of conduct and privacy statement"
|
||||||
onChange={this.onChangeCheckbox}
|
onChange={this.onChangeCheckbox}
|
||||||
/>
|
/>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ 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;
|
||||||
@@ -61,7 +60,6 @@ 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,20 +7,14 @@ 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 key={this.key} {...this.props} />;
|
return <GalleryAndNotebookViewerComponent {...this.props} />;
|
||||||
}
|
|
||||||
|
|
||||||
public reset(): void {
|
|
||||||
this.key = `GalleryAndNotebookViewerComponent-${Date.now()}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public triggerRender(): void {
|
public triggerRender(): void {
|
||||||
|
|||||||
@@ -6,16 +6,4 @@
|
|||||||
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,7 +5,6 @@ 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,14 +9,10 @@ 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";
|
||||||
@@ -31,12 +27,9 @@ 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;
|
||||||
@@ -71,8 +64,6 @@ interface GalleryViewerComponentState {
|
|||||||
searchText: string;
|
searchText: string;
|
||||||
dialogProps: DialogProps;
|
dialogProps: DialogProps;
|
||||||
isCodeOfConductAccepted: boolean;
|
isCodeOfConductAccepted: boolean;
|
||||||
isFetchingPublishedNotebooks: boolean;
|
|
||||||
isFetchingFavouriteNotebooks: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GalleryTabInfo {
|
interface GalleryTabInfo {
|
||||||
@@ -83,24 +74,18 @@ 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 = "My favorites";
|
public static readonly FavoritesTitle = "Liked";
|
||||||
public static readonly PublishedTitle = "My published work";
|
public static readonly PublishedTitle = "Your 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 favorited";
|
private static readonly mostFavoritedText = "Most liked";
|
||||||
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[];
|
||||||
@@ -122,8 +107,6 @@ 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 = [
|
||||||
@@ -154,11 +137,9 @@ 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.isGalleryPublishEnabled) {
|
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||||
tabs.push(
|
tabs.push(
|
||||||
this.createPublicGalleryTab(
|
this.createPublicGalleryTab(
|
||||||
GalleryTab.PublicGallery,
|
GalleryTab.PublicGallery,
|
||||||
@@ -166,11 +147,13 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
this.state.isCodeOfConductAccepted
|
this.state.isCodeOfConductAccepted
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.container?.isGalleryPublishEnabled()) {
|
|
||||||
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||||
tabs.push(this.createPublishedNotebooksTab(GalleryTab.Published, this.state.publishedNotebooks));
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pivotProps: IPivotProps = {
|
const pivotProps: IPivotProps = {
|
||||||
@@ -201,58 +184,11 @@ 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: JSX.Element, line2: JSX.Element): JSX.Element => {
|
private createEmptyTabContent = (iconName: string, line1: string, line2: string): 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 }} />
|
||||||
@@ -280,63 +216,40 @@ 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.getFavouriteNotebooksTabContent(data),
|
content: this.isEmptyData(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.getPublishedNotebooksTabContent(data),
|
content: this.isEmptyData(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: 20 }}>
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
{published?.length > 0 &&
|
{published?.length > 0 &&
|
||||||
this.createPublishedNotebooksSectionContent(
|
this.createPublishedNotebooksSectionContent(
|
||||||
undefined,
|
undefined,
|
||||||
"You have successfully published and shared the following notebook(s) to the public gallery.",
|
"You have successfully published the following notebook(s) to public gallery and shared with other Azure Cosmos DB users.",
|
||||||
this.createCardsTabContent(published)
|
this.createCardsTabContent(published)
|
||||||
)}
|
)}
|
||||||
{underReview?.length > 0 &&
|
{underReview?.length > 0 &&
|
||||||
@@ -363,33 +276,24 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
content: JSX.Element
|
content: JSX.Element
|
||||||
): JSX.Element => {
|
): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Stack tokens={{ childrenGap: 10 }}>
|
<Stack tokens={{ childrenGap: 5 }}>
|
||||||
{title && (
|
{title && <Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{title}</Text>}
|
||||||
<Text styles={{ root: { fontWeight: FontWeights.semibold, marginLeft: 10, marginRight: 10 } }}>{title}</Text>
|
{description && <Text>{description}</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 (
|
return acceptedCodeOfConduct === false ? (
|
||||||
<div className="publicGalleryTabContainer">
|
<CodeOfConductComponent
|
||||||
{this.createSearchBarHeader(this.createCardsTabContent(data))}
|
junoClient={this.props.junoClient}
|
||||||
{acceptedCodeOfConduct === false && (
|
onAcceptCodeOfConduct={(result: boolean) => {
|
||||||
<Overlay isDarkThemed>
|
this.setState({ isCodeOfConductAccepted: result });
|
||||||
<div className="publicGalleryTabOverlayContent">
|
}}
|
||||||
<CodeOfConductComponent
|
/>
|
||||||
junoClient={this.props.junoClient}
|
) : (
|
||||||
onAcceptCodeOfConduct={(result: boolean) => {
|
this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||||
this.setState({ isCodeOfConductAccepted: result });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Overlay>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +310,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.isGalleryPublishEnabled && (
|
{(!this.props.container || this.props.container.isGalleryPublishEnabled()) && (
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<InfoComponent />
|
<InfoComponent />
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
@@ -418,7 +322,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createCardsTabContent(data: IGalleryItem[]): JSX.Element {
|
private createCardsTabContent(data: IGalleryItem[]): JSX.Element {
|
||||||
return data ? (
|
return (
|
||||||
<FocusZone>
|
<FocusZone>
|
||||||
<List
|
<List
|
||||||
items={data}
|
items={data}
|
||||||
@@ -427,14 +331,12 @@ 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 style={{ margin: 10 }}>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
@@ -483,10 +385,6 @@ 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");
|
||||||
}
|
}
|
||||||
@@ -513,8 +411,6 @@ 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");
|
||||||
}
|
}
|
||||||
@@ -529,19 +425,14 @@ 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 });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -560,25 +451,14 @@ 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,28 +17,35 @@ exports[`CodeOfConductComponent renders 1`] = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Azure Cosmos DB Notebook Gallery - Code of Conduct
|
Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<Text>
|
<Text>
|
||||||
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.
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<Text>
|
<Text>
|
||||||
In order to view and publish your samples to the gallery, you must accept the
|
In order to access Azure Cosmos DB Notebook Gallery resources, 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 accept the code of conduct."
|
label="I have read and accepted the code of conduct and privacy statement"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
styles={
|
styles={
|
||||||
Object {
|
Object {
|
||||||
|
|||||||
@@ -77,11 +77,24 @@ exports[`GalleryViewerComponent renders 1`] = `
|
|||||||
selectedKey={0}
|
selectedKey={0}
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<InfoComponent />
|
||||||
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<StyledSpinnerBase
|
<FocusZone
|
||||||
size={3}
|
direction={2}
|
||||||
/>
|
isCircularNavigation={false}
|
||||||
|
shouldRaiseClicks={true}
|
||||||
|
>
|
||||||
|
<List
|
||||||
|
getPageSpecification={[Function]}
|
||||||
|
onRenderCell={[Function]}
|
||||||
|
renderedWindowsAhead={3}
|
||||||
|
renderedWindowsBehind={2}
|
||||||
|
startIndex={0}
|
||||||
|
/>
|
||||||
|
</FocusZone>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
|
|||||||
@@ -31,26 +31,6 @@ 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",
|
||||||
@@ -69,7 +49,19 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
|
|||||||
</Text>
|
</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
|
||||||
<Stack.Item>{this.renderFavouriteButton()}</Stack.Item>
|
<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,11 +3,14 @@
|
|||||||
*/
|
*/
|
||||||
import { Notebook } from "@nteract/commutable";
|
import { Notebook } from "@nteract/commutable";
|
||||||
import { createContentRef } from "@nteract/core";
|
import { createContentRef } from "@nteract/core";
|
||||||
import { IChoiceGroupProps, Icon, IProgressIndicatorProps, Link, ProgressIndicator } from "office-ui-fabric-react";
|
import { IChoiceGroupProps, Icon, 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";
|
||||||
@@ -18,9 +21,7 @@ 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, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
|
import { getErrorMessage, 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;
|
||||||
@@ -79,12 +80,6 @@ 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) {
|
||||||
@@ -92,16 +87,6 @@ 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);
|
||||||
@@ -116,18 +101,6 @@ 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");
|
||||||
}
|
}
|
||||||
@@ -205,32 +178,6 @@ 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,
|
||||||
@@ -239,10 +186,8 @@ 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: {
|
||||||
@@ -260,10 +205,8 @@ 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 { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
import SettingsTabV2 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,21 +31,25 @@ 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 CollectionSettingsTabV2({
|
settingsTab: new SettingsTabV2({
|
||||||
collection: collection,
|
collection: collection,
|
||||||
tabKind: ViewModels.CollectionTabKind.CollectionSettingsV2,
|
tabKind: ViewModels.CollectionTabKind.SettingsV2,
|
||||||
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;
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -138,7 +142,6 @@ 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 { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
|
import SettingsTab 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: SettingsTabV2;
|
settingsTab: SettingsTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsComponentState {
|
export interface SettingsComponentState {
|
||||||
@@ -116,10 +116,7 @@ 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;
|
||||||
@@ -129,28 +126,20 @@ 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;
|
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
|
||||||
if (this.isCollectionSettingsTab) {
|
this.container = this.collection?.container;
|
||||||
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
|
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
||||||
this.container = this.collection?.container;
|
this.shouldShowIndexingPolicyEditor =
|
||||||
this.offer = this.collection?.offer();
|
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB();
|
||||||
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
|
||||||
this.shouldShowIndexingPolicyEditor =
|
|
||||||
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB();
|
|
||||||
|
|
||||||
this.changeFeedPolicyVisible = this.collection?.container.isFeatureEnabled(
|
this.changeFeedPolicyVisible = this.collection?.container.isFeatureEnabled(
|
||||||
Constants.Features.enableChangeFeedPolicy
|
Constants.Features.enableChangeFeedPolicy
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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,
|
||||||
@@ -217,21 +206,18 @@ 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.getContainer().onUpdateTabsButtons(this.getTabsButtons());
|
this.props.settingsTab.getSettingsTabContainer().onUpdateTabsButtons(this.getTabsButtons());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(): void {
|
componentDidUpdate(): void {
|
||||||
if (this.props.settingsTab.isActive()) {
|
if (this.props.settingsTab.isActive()) {
|
||||||
this.props.settingsTab.getContainer().onUpdateTabsButtons(this.getTabsButtons());
|
this.props.settingsTab.getSettingsTabContainer().onUpdateTabsButtons(this.getTabsButtons());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +270,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
};
|
};
|
||||||
|
|
||||||
private setAutoPilotStates = (): void => {
|
private setAutoPilotStates = (): void => {
|
||||||
const autoscaleMaxThroughput = this.offer?.autoscaleMaxThroughput;
|
const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput;
|
||||||
|
|
||||||
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -309,7 +295,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
!!this.collection.conflictResolutionPolicy();
|
!!this.collection.conflictResolutionPolicy();
|
||||||
|
|
||||||
public isOfferReplacePending = (): boolean => {
|
public isOfferReplacePending = (): boolean => {
|
||||||
return this.offer?.offerReplacePending;
|
return this.collection?.offer()?.offerReplacePending;
|
||||||
};
|
};
|
||||||
|
|
||||||
public onSaveClick = async (): Promise<void> => {
|
public onSaveClick = async (): Promise<void> => {
|
||||||
@@ -323,10 +309,174 @@ 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 {
|
||||||
await (this.isCollectionSettingsTab
|
if (
|
||||||
? this.saveCollectionSettings(startKey)
|
this.state.isSubSettingsSaveable ||
|
||||||
: this.saveDatabaseSettings(startKey));
|
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.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);
|
||||||
@@ -345,9 +495,8 @@ 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 => {
|
||||||
@@ -544,17 +693,6 @@ 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;
|
||||||
@@ -587,6 +725,7 @@ 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;
|
||||||
@@ -672,225 +811,9 @@ 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,
|
||||||
@@ -907,16 +830,6 @@ 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,
|
||||||
@@ -986,7 +899,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
|||||||
};
|
};
|
||||||
|
|
||||||
const tabs: SettingsV2TabInfo[] = [];
|
const tabs: SettingsV2TabInfo[] = [];
|
||||||
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
|
if (!hasDatabaseSharedThroughput(this.collection) && this.collection.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 />
|
||||||
{collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `}
|
Database: {databaseName}, Container: {collectionName}{" "}
|
||||||
{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 />
|
||||||
{collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `}
|
Database: {databaseName}, Container: {collectionName}{" "}
|
||||||
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
|
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ 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,7 +21,6 @@ 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;
|
||||||
@@ -40,16 +39,9 @@ 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 => {
|
||||||
@@ -95,7 +87,9 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.offer?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400;
|
return (
|
||||||
|
this.props.collection.offer()?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
public getThroughputTitle = (): string => {
|
public getThroughputTitle = (): string => {
|
||||||
@@ -121,14 +115,15 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
return this.getLongDelayMessage();
|
return this.getLongDelayMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.offer?.offerReplacePending) {
|
const offer = this.props.collection?.offer();
|
||||||
const throughput = this.offer.manualThroughput || this.offer.autoscaleMaxThroughput;
|
if (offer?.offerReplacePending) {
|
||||||
|
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
|
||||||
return getThroughputApplyShortDelayMessage(
|
return getThroughputApplyShortDelayMessage(
|
||||||
this.props.isAutoPilotSelected,
|
this.props.isAutoPilotSelected,
|
||||||
throughput,
|
throughput,
|
||||||
throughputUnit,
|
throughputUnit,
|
||||||
this.databaseId,
|
this.props.collection.databaseId,
|
||||||
this.collectionId
|
this.props.collection.id()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +135,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.isFixedContainer) {
|
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
|
||||||
return updateThroughputBeyondLimitWarningMessage;
|
return updateThroughputBeyondLimitWarningMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,8 +154,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
this.props.wasAutopilotOriginallySet,
|
this.props.wasAutopilotOriginallySet,
|
||||||
throughput,
|
throughput,
|
||||||
throughputUnit,
|
throughputUnit,
|
||||||
this.databaseId,
|
this.props.collection.databaseId,
|
||||||
this.collectionId,
|
this.props.collection.id(),
|
||||||
targetThroughput
|
targetThroughput
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -170,15 +165,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.databaseId}
|
databaseName={this.props.collection.databaseId}
|
||||||
collectionName={this.collectionId}
|
collectionName={this.props.collection.id()}
|
||||||
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={!!this.props.database || !hasDatabaseSharedThroughput(this.props.collection)}
|
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
|
||||||
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
|
||||||
label={this.getThroughputTitle()}
|
label={this.getThroughputTitle()}
|
||||||
isEmulator={this.isEmulator}
|
isEmulator={this.isEmulator}
|
||||||
@@ -194,7 +189,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()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -235,7 +230,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
|||||||
{!this.isAutoScaleEnabled() && (
|
{!this.isAutoScaleEnabled() && (
|
||||||
<Stack {...subComponentStackProps}>
|
<Stack {...subComponentStackProps}>
|
||||||
{this.getThroughputInputComponent()}
|
{this.getThroughputInputComponent()}
|
||||||
{!this.props.database && this.getStorageCapacityTitle()}
|
{this.getStorageCapacityTitle()}
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ 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";
|
||||||
@@ -542,7 +541,6 @@ 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()}
|
||||||
@@ -581,7 +579,6 @@ 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,7 +142,6 @@ 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}
|
||||||
@@ -261,7 +260,6 @@ 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}
|
||||||
@@ -535,7 +533,6 @@ 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,7 +23,11 @@ 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: test, Container: test
|
Database:
|
||||||
|
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,7 +46,6 @@ 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,7 +804,6 @@ 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",
|
||||||
@@ -1022,7 +1021,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"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],
|
||||||
@@ -2085,7 +2083,6 @@ 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",
|
||||||
@@ -2303,7 +2300,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"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],
|
||||||
@@ -3379,7 +3375,6 @@ 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",
|
||||||
@@ -3597,7 +3592,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"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],
|
||||||
@@ -4660,7 +4654,6 @@ 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",
|
||||||
@@ -4878,7 +4871,6 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"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],
|
||||||
|
|||||||
@@ -256,7 +256,11 @@ 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: sampleDb, Container: sampleCollection
|
Database:
|
||||||
|
sampleDb
|
||||||
|
, Container:
|
||||||
|
sampleCollection
|
||||||
|
|
||||||
, Current manual throughput: 1000 RU/s
|
, Current manual throughput: 1000 RU/s
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -271,7 +275,11 @@ 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: sampleDb, Container: sampleCollection
|
Database:
|
||||||
|
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
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ describe("SmartUiComponent", () => {
|
|||||||
root: {
|
root: {
|
||||||
id: "root",
|
id: "root",
|
||||||
info: {
|
info: {
|
||||||
messageTKey: "Start at $24/mo per database",
|
message: "Start at $24/mo per database",
|
||||||
link: {
|
link: {
|
||||||
href: "https://aka.ms/azure-cosmos-db-pricing",
|
href: "https://aka.ms/azure-cosmos-db-pricing",
|
||||||
textTKey: "More Details",
|
text: "More Details",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
@@ -21,10 +21,10 @@ describe("SmartUiComponent", () => {
|
|||||||
dataFieldName: "description",
|
dataFieldName: "description",
|
||||||
type: "string",
|
type: "string",
|
||||||
description: {
|
description: {
|
||||||
textTKey: "this is an example description text.",
|
text: "this is an example description text.",
|
||||||
link: {
|
link: {
|
||||||
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
||||||
textTKey: "Click here for more information.",
|
text: "Click here for more information.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -32,7 +32,7 @@ describe("SmartUiComponent", () => {
|
|||||||
{
|
{
|
||||||
id: "throughput",
|
id: "throughput",
|
||||||
input: {
|
input: {
|
||||||
labelTKey: "Throughput (input)",
|
label: "Throughput (input)",
|
||||||
dataFieldName: "throughput",
|
dataFieldName: "throughput",
|
||||||
type: "number",
|
type: "number",
|
||||||
min: 400,
|
min: 400,
|
||||||
@@ -45,7 +45,7 @@ describe("SmartUiComponent", () => {
|
|||||||
{
|
{
|
||||||
id: "throughput2",
|
id: "throughput2",
|
||||||
input: {
|
input: {
|
||||||
labelTKey: "Throughput (Slider)",
|
label: "Throughput (Slider)",
|
||||||
dataFieldName: "throughput2",
|
dataFieldName: "throughput2",
|
||||||
type: "number",
|
type: "number",
|
||||||
min: 400,
|
min: 400,
|
||||||
@@ -58,7 +58,7 @@ describe("SmartUiComponent", () => {
|
|||||||
{
|
{
|
||||||
id: "throughput3",
|
id: "throughput3",
|
||||||
input: {
|
input: {
|
||||||
labelTKey: "Throughput (invalid)",
|
label: "Throughput (invalid)",
|
||||||
dataFieldName: "throughput3",
|
dataFieldName: "throughput3",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
min: 400,
|
min: 400,
|
||||||
@@ -72,7 +72,7 @@ describe("SmartUiComponent", () => {
|
|||||||
{
|
{
|
||||||
id: "containerId",
|
id: "containerId",
|
||||||
input: {
|
input: {
|
||||||
labelTKey: "Container id",
|
label: "Container id",
|
||||||
dataFieldName: "containerId",
|
dataFieldName: "containerId",
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
@@ -80,9 +80,9 @@ describe("SmartUiComponent", () => {
|
|||||||
{
|
{
|
||||||
id: "analyticalStore",
|
id: "analyticalStore",
|
||||||
input: {
|
input: {
|
||||||
labelTKey: "Analytical Store",
|
label: "Analytical Store",
|
||||||
trueLabelTKey: "Enabled",
|
trueLabel: "Enabled",
|
||||||
falseLabelTKey: "Disabled",
|
falseLabel: "Disabled",
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
dataFieldName: "analyticalStore",
|
dataFieldName: "analyticalStore",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
@@ -91,7 +91,7 @@ describe("SmartUiComponent", () => {
|
|||||||
{
|
{
|
||||||
id: "database",
|
id: "database",
|
||||||
input: {
|
input: {
|
||||||
labelTKey: "Database",
|
label: "Database",
|
||||||
dataFieldName: "database",
|
dataFieldName: "database",
|
||||||
type: "object",
|
type: "object",
|
||||||
choices: [
|
choices: [
|
||||||
@@ -117,9 +117,6 @@ describe("SmartUiComponent", () => {
|
|||||||
onError={() => {
|
onError={() => {
|
||||||
return;
|
return;
|
||||||
}}
|
}}
|
||||||
getTranslation={(key: string) => {
|
|
||||||
return key;
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
@@ -148,9 +145,6 @@ describe("SmartUiComponent", () => {
|
|||||||
onError={() => {
|
onError={() => {
|
||||||
return;
|
return;
|
||||||
}}
|
}}
|
||||||
getTranslation={(key: string) => {
|
|
||||||
return key;
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
NumberUiType,
|
NumberUiType,
|
||||||
SmartUiInput,
|
SmartUiInput,
|
||||||
} from "../../../SelfServe/SelfServeTypes";
|
} from "../../../SelfServe/SelfServeTypes";
|
||||||
import { TFunction } from "i18next";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic UX renderer
|
* Generic UX renderer
|
||||||
@@ -35,8 +34,8 @@ interface BaseDisplay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface BaseInput extends BaseDisplay {
|
interface BaseInput extends BaseDisplay {
|
||||||
labelTKey: string;
|
label: string;
|
||||||
placeholderTKey?: string;
|
placeholder?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,8 +51,8 @@ interface NumberInput extends BaseInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface BooleanInput extends BaseInput {
|
interface BooleanInput extends BaseInput {
|
||||||
trueLabelTKey: string;
|
trueLabel: string;
|
||||||
falseLabelTKey: string;
|
falseLabel: string;
|
||||||
defaultValue?: boolean;
|
defaultValue?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +89,6 @@ export interface SmartUiComponentProps {
|
|||||||
onInputChange: (input: AnyDisplay, newValue: InputType) => void;
|
onInputChange: (input: AnyDisplay, newValue: InputType) => void;
|
||||||
onError: (hasError: boolean) => void;
|
onError: (hasError: boolean) => void;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
getTranslation: TFunction;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SmartUiComponentState {
|
interface SmartUiComponentState {
|
||||||
@@ -124,10 +122,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
private renderInfo(info: Info): JSX.Element {
|
private renderInfo(info: Info): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<MessageBar styles={{ root: { width: 400 } }}>
|
<MessageBar styles={{ root: { width: 400 } }}>
|
||||||
{this.props.getTranslation(info.messageTKey)}
|
{info.message}
|
||||||
{info.link && (
|
{info.link && (
|
||||||
<Link href={info.link.href} target="_blank">
|
<Link href={info.link.href} target="_blank">
|
||||||
{this.props.getTranslation(info.link.textTKey)}
|
{info.link.text}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</MessageBar>
|
</MessageBar>
|
||||||
@@ -141,10 +139,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
<div className="stringInputContainer">
|
<div className="stringInputContainer">
|
||||||
<TextField
|
<TextField
|
||||||
id={`${input.dataFieldName}-textField-input`}
|
id={`${input.dataFieldName}-textField-input`}
|
||||||
label={this.props.getTranslation(input.labelTKey)}
|
label={input.label}
|
||||||
type="text"
|
type="text"
|
||||||
value={value || ""}
|
value={value || ""}
|
||||||
placeholder={this.props.getTranslation(input.placeholderTKey)}
|
placeholder={input.placeholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
|
onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
|
||||||
styles={{
|
styles={{
|
||||||
@@ -167,10 +165,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
const description = input.description;
|
const description = input.description;
|
||||||
return (
|
return (
|
||||||
<Text id={`${input.dataFieldName}-text-display`}>
|
<Text id={`${input.dataFieldName}-text-display`}>
|
||||||
{this.props.getTranslation(input.description.textTKey)}{" "}
|
{input.description.text}{" "}
|
||||||
{description.link && (
|
{description.link && (
|
||||||
<Link target="_blank" href={input.description.link.href}>
|
<Link target="_blank" href={input.description.link.href}>
|
||||||
{this.props.getTranslation(input.description.link.textTKey)}
|
{input.description.link.text}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -221,12 +219,12 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
};
|
};
|
||||||
|
|
||||||
private renderNumberInput(input: NumberInput): JSX.Element {
|
private renderNumberInput(input: NumberInput): JSX.Element {
|
||||||
const { labelTKey, min, max, dataFieldName, step } = input;
|
const { label, min, max, dataFieldName, step } = input;
|
||||||
const props = {
|
const props = {
|
||||||
label: this.props.getTranslation(labelTKey),
|
label: label,
|
||||||
min: min,
|
min: min,
|
||||||
max: max,
|
max: max,
|
||||||
ariaLabel: labelTKey,
|
ariaLabel: label,
|
||||||
step: step,
|
step: step,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -286,10 +284,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
return (
|
return (
|
||||||
<Toggle
|
<Toggle
|
||||||
id={`${input.dataFieldName}-toggle-input`}
|
id={`${input.dataFieldName}-toggle-input`}
|
||||||
label={this.props.getTranslation(input.labelTKey)}
|
label={input.label}
|
||||||
checked={value || false}
|
checked={value || false}
|
||||||
onText={this.props.getTranslation(input.trueLabelTKey)}
|
onText={input.trueLabel}
|
||||||
offText={this.props.getTranslation(input.falseLabelTKey)}
|
offText={input.falseLabel}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(event, checked: boolean) => this.props.onInputChange(input, checked)}
|
onChange={(event, checked: boolean) => this.props.onInputChange(input, checked)}
|
||||||
styles={{ root: { width: 400 } }}
|
styles={{ root: { width: 400 } }}
|
||||||
@@ -298,7 +296,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderChoiceInput(input: ChoiceInput): JSX.Element {
|
private renderChoiceInput(input: ChoiceInput): JSX.Element {
|
||||||
const { labelTKey, defaultKey, dataFieldName, choices, placeholderTKey } = input;
|
const { label, defaultKey: defaultKey, dataFieldName, choices, placeholder } = input;
|
||||||
const value = this.props.currentValues.get(dataFieldName)?.value as string;
|
const value = this.props.currentValues.get(dataFieldName)?.value as string;
|
||||||
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
|
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
|
||||||
let selectedKey = value ? value : defaultKey;
|
let selectedKey = value ? value : defaultKey;
|
||||||
@@ -308,14 +306,14 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
|||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
id={`${input.dataFieldName}-dropdown-input`}
|
id={`${input.dataFieldName}-dropdown-input`}
|
||||||
label={this.props.getTranslation(labelTKey)}
|
label={label}
|
||||||
selectedKey={selectedKey}
|
selectedKey={selectedKey}
|
||||||
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
|
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
|
||||||
placeholder={this.props.getTranslation(placeholderTKey)}
|
placeholder={placeholder}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
options={choices.map((c) => ({
|
options={choices.map((c) => ({
|
||||||
key: c.key,
|
key: c.key,
|
||||||
text: this.props.getTranslation(c.label),
|
text: c.label,
|
||||||
}))}
|
}))}
|
||||||
styles={{
|
styles={{
|
||||||
root: { width: 400 },
|
root: { width: 400 },
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
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";
|
||||||
@@ -48,6 +47,7 @@ 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";
|
||||||
@@ -91,8 +91,6 @@ 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
|
||||||
@@ -112,8 +110,6 @@ export interface ExplorerParams {
|
|||||||
setIsNotificationConsoleExpanded: (isExpanded: boolean) => void;
|
setIsNotificationConsoleExpanded: (isExpanded: boolean) => void;
|
||||||
setNotificationConsoleData: (consoleData: ConsoleData) => void;
|
setNotificationConsoleData: (consoleData: ConsoleData) => void;
|
||||||
setInProgressConsoleDataIdToBeDeleted: (id: string) => void;
|
setInProgressConsoleDataIdToBeDeleted: (id: string) => void;
|
||||||
openSidePanel: (headerText: string, panelContent: JSX.Element) => void;
|
|
||||||
closeSidePanel: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Explorer {
|
export default class Explorer {
|
||||||
@@ -161,8 +157,6 @@ export default class Explorer {
|
|||||||
|
|
||||||
// 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>;
|
||||||
@@ -284,8 +278,6 @@ export default class Explorer {
|
|||||||
this.setIsNotificationConsoleExpanded = params?.setIsNotificationConsoleExpanded;
|
this.setIsNotificationConsoleExpanded = params?.setIsNotificationConsoleExpanded;
|
||||||
this.setNotificationConsoleData = params?.setNotificationConsoleData;
|
this.setNotificationConsoleData = params?.setNotificationConsoleData;
|
||||||
this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted;
|
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,
|
||||||
@@ -431,8 +423,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>(() =>
|
||||||
() => configContext.ENABLE_GALLERY_PUBLISH || this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
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)
|
||||||
@@ -1897,9 +1889,6 @@ 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 {
|
||||||
@@ -2260,7 +2249,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,
|
||||||
@@ -2357,13 +2346,11 @@ export default class Explorer {
|
|||||||
this.tabsManager.activateTab(notebookTab);
|
this.tabsManager.activateTab(notebookTab);
|
||||||
} else {
|
} else {
|
||||||
const options: NotebookTabOptions = {
|
const options: NotebookTabOptions = {
|
||||||
account: userContext.databaseAccount,
|
|
||||||
tabKind: ViewModels.CollectionTabKind.NotebookV2,
|
tabKind: ViewModels.CollectionTabKind.NotebookV2,
|
||||||
node: null,
|
node: null,
|
||||||
title: notebookContentItem.name,
|
title: notebookContentItem.name,
|
||||||
tabPath: notebookContentItem.path,
|
tabPath: notebookContentItem.path,
|
||||||
collection: null,
|
collection: null,
|
||||||
masterKey: userContext.masterKey || "",
|
|
||||||
hashLocation: "notebooks",
|
hashLocation: "notebooks",
|
||||||
isActive: ko.observable(false),
|
isActive: ko.observable(false),
|
||||||
isTabsContentExpanded: ko.observable(true),
|
isTabsContentExpanded: ko.observable(true),
|
||||||
@@ -2821,36 +2808,10 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openGallery(
|
public async openGallery(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) {
|
||||||
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 galleryTabOptions: any = {
|
|
||||||
// GalleryTabOptions
|
|
||||||
account: userContext.databaseAccount,
|
|
||||||
container: this,
|
|
||||||
junoClient: this.notebookManager?.junoClient,
|
|
||||||
selectedTab: selectedTab || GalleryTab.OfficialSamples,
|
|
||||||
notebookUrl,
|
|
||||||
galleryItem,
|
|
||||||
isFavorite,
|
|
||||||
// TabOptions
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Gallery,
|
|
||||||
title: title,
|
|
||||||
tabPath: title,
|
|
||||||
documentClientUtility: null,
|
|
||||||
isActive: ko.observable(false),
|
|
||||||
hashLocation: hashLocation,
|
|
||||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
|
||||||
isTabsContentExpanded: ko.observable(true),
|
|
||||||
onLoadStartKey: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const galleryTabs = this.tabsManager.getTabs(
|
const galleryTabs = this.tabsManager.getTabs(
|
||||||
ViewModels.CollectionTabKind.Gallery,
|
ViewModels.CollectionTabKind.Gallery,
|
||||||
(tab) => tab.hashLocation() == hashLocation
|
(tab) => tab.hashLocation() == hashLocation
|
||||||
@@ -2859,12 +2820,31 @@ export default class Explorer {
|
|||||||
|
|
||||||
if (galleryTab) {
|
if (galleryTab) {
|
||||||
this.tabsManager.activateTab(galleryTab);
|
this.tabsManager.activateTab(galleryTab);
|
||||||
(galleryTab as any).reset(galleryTabOptions);
|
|
||||||
} else {
|
} else {
|
||||||
if (!this.galleryTab) {
|
if (!this.galleryTab) {
|
||||||
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
|
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
|
||||||
}
|
}
|
||||||
const newTab = new this.galleryTab.default(galleryTabOptions);
|
|
||||||
|
const newTab = new this.galleryTab.default({
|
||||||
|
// GalleryTabOptions
|
||||||
|
account: userContext.databaseAccount,
|
||||||
|
container: this,
|
||||||
|
junoClient: this.notebookManager?.junoClient,
|
||||||
|
notebookUrl,
|
||||||
|
galleryItem,
|
||||||
|
isFavorite,
|
||||||
|
// TabOptions
|
||||||
|
tabKind: ViewModels.CollectionTabKind.Gallery,
|
||||||
|
title: title,
|
||||||
|
tabPath: title,
|
||||||
|
documentClientUtility: null,
|
||||||
|
isActive: ko.observable(false),
|
||||||
|
hashLocation: hashLocation,
|
||||||
|
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||||
|
isTabsContentExpanded: ko.observable(true),
|
||||||
|
onLoadStartKey: null,
|
||||||
|
});
|
||||||
|
|
||||||
this.tabsManager.activateNewTab(newTab);
|
this.tabsManager.activateNewTab(newTab);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3046,17 +3026,4 @@ 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,36 +36,7 @@ export class CommandBarComponentButtonFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newCollectionBtn = CommandBarComponentButtonFactory.createNewCollectionGroup(container);
|
const newCollectionBtn = CommandBarComponentButtonFactory.createNewCollectionGroup(container);
|
||||||
const buttons: CommandButtonComponentProps[] = [];
|
const buttons: CommandButtonComponentProps[] = [newCollectionBtn];
|
||||||
|
|
||||||
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) {
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ export class NotificationConsoleComponent extends React.Component<
|
|||||||
<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)}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="notificationConsoleHeader"
|
className="notificationConsoleHeader"
|
||||||
id="notificationConsoleHeader"
|
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -170,7 +169,6 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="notificationConsoleHeader"
|
className="notificationConsoleHeader"
|
||||||
id="notificationConsoleHeader"
|
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
onKeyDown={[Function]}
|
onKeyDown={[Function]}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
.mongoQueryComponent {
|
||||||
|
margin-left: 10px;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
label:before {
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.queryInput {
|
||||||
|
border: 1px solid black;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Dispatch } from "redux";
|
||||||
|
import MonacoEditor from "@nteract/monaco-editor";
|
||||||
|
import { PrimaryButton } from "office-ui-fabric-react";
|
||||||
|
import { ChoiceGroup, IChoiceGroupOption } from "office-ui-fabric-react/lib/ChoiceGroup";
|
||||||
|
import Outputs from "@nteract/stateful-components/lib/outputs";
|
||||||
|
import { KernelOutputError, StreamText } from "@nteract/outputs";
|
||||||
|
import TransformMedia from "@nteract/stateful-components/lib/outputs/transform-media";
|
||||||
|
import { actions, selectors, AppState, ContentRef, KernelRef } from "@nteract/core";
|
||||||
|
import loadTransform from "../NotebookComponent/loadTransform";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
|
import "./MongoQueryComponent.less";
|
||||||
|
interface MongoQueryComponentPureProps {
|
||||||
|
contentRef: ContentRef;
|
||||||
|
kernelRef: KernelRef;
|
||||||
|
databaseId: string;
|
||||||
|
collectionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MongoQueryComponentDispatchProps {
|
||||||
|
runCell: (contentRef: ContentRef, cellId: string) => void;
|
||||||
|
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void;
|
||||||
|
updateCell: (text: string, id: string, contentRef: ContentRef) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutputType = "rich" | "json";
|
||||||
|
|
||||||
|
interface MongoQueryComponentState {
|
||||||
|
query: string;
|
||||||
|
outputType: OutputType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: IChoiceGroupOption[] = [
|
||||||
|
{ key: "rich", text: "Rich Output" },
|
||||||
|
{ key: "json", text: "Json Output" },
|
||||||
|
];
|
||||||
|
|
||||||
|
type MongoQueryComponentProps = MongoQueryComponentPureProps & StateProps & MongoQueryComponentDispatchProps;
|
||||||
|
export class MongoQueryComponent extends React.Component<MongoQueryComponentProps, MongoQueryComponentState> {
|
||||||
|
constructor(props: MongoQueryComponentProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
query: this.props.inputValue,
|
||||||
|
outputType: "rich",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
loadTransform(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onExecute = () => {
|
||||||
|
const query = JSON.parse(this.state.query);
|
||||||
|
query["database"] = this.props.databaseId;
|
||||||
|
query["collection"] = this.props.collectionId;
|
||||||
|
query["outputType"] = this.state.outputType;
|
||||||
|
|
||||||
|
this.props.updateCell(JSON.stringify(query), this.props.firstCellId, this.props.contentRef);
|
||||||
|
this.props.runCell(this.props.contentRef, this.props.firstCellId);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onOutputTypeChange = (
|
||||||
|
e: React.FormEvent<HTMLElement | HTMLInputElement>,
|
||||||
|
option: IChoiceGroupOption
|
||||||
|
): void => {
|
||||||
|
const outputType = option.key as OutputType;
|
||||||
|
this.setState({ outputType }, () => this.onInputChange(this.props.inputValue));
|
||||||
|
};
|
||||||
|
|
||||||
|
private onInputChange = (text: string) => {
|
||||||
|
this.setState({ query: text });
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): JSX.Element {
|
||||||
|
const { firstCellId: id, contentRef } = this.props;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mongoQueryComponent">
|
||||||
|
<div className="queryInput">
|
||||||
|
<MonacoEditor
|
||||||
|
id={this.props.firstCellId}
|
||||||
|
contentRef={this.props.contentRef}
|
||||||
|
theme={""}
|
||||||
|
language="json"
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
value={this.state.query}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<PrimaryButton text="Run" onClick={this.onExecute} disabled={!this.props.firstCellId} />
|
||||||
|
<ChoiceGroup
|
||||||
|
selectedKey={this.state.outputType}
|
||||||
|
options={options}
|
||||||
|
onChange={this.onOutputTypeChange}
|
||||||
|
label="Output Type"
|
||||||
|
styles={{ root: { marginTop: 0 } }}
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
|
<Outputs id={id} contentRef={contentRef}>
|
||||||
|
<TransformMedia output_type={"display_data"} id={id} contentRef={contentRef} />
|
||||||
|
<TransformMedia output_type={"execute_result"} id={id} contentRef={contentRef} />
|
||||||
|
<KernelOutputError />
|
||||||
|
<StreamText />
|
||||||
|
</Outputs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
|
firstCellId: string;
|
||||||
|
inputValue: string;
|
||||||
|
}
|
||||||
|
interface InitialProps {
|
||||||
|
contentRef: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redux
|
||||||
|
const makeMapStateToProps = (state: AppState, initialProps: InitialProps) => {
|
||||||
|
const { contentRef } = initialProps;
|
||||||
|
const mapStateToProps = (state: AppState) => {
|
||||||
|
let firstCellId;
|
||||||
|
let inputValue = "";
|
||||||
|
const content = selectors.content(state, { contentRef });
|
||||||
|
if (content?.type === "notebook") {
|
||||||
|
const cellOrder = selectors.notebook.cellOrder(content.model);
|
||||||
|
if (cellOrder.size > 0) {
|
||||||
|
firstCellId = cellOrder.first() as string;
|
||||||
|
const cell = selectors.notebook.cellById(content.model, { id: firstCellId });
|
||||||
|
|
||||||
|
// Parse to extract filter and output type
|
||||||
|
const cellValue = cell.get("source", "");
|
||||||
|
if (cellValue) {
|
||||||
|
try {
|
||||||
|
const filterValue = JSON.parse(cellValue).filter;
|
||||||
|
if (filterValue) {
|
||||||
|
inputValue = filterValue;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not parse", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
firstCellId,
|
||||||
|
inputValue,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: MongoQueryComponentProps) => {
|
||||||
|
const mapDispatchToProps = (dispatch: Dispatch) => {
|
||||||
|
return {
|
||||||
|
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
|
||||||
|
return dispatch(
|
||||||
|
actions.addTransform({
|
||||||
|
mediaType: transform.MIMETYPE,
|
||||||
|
component: transform,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
runCell: (contentRef: ContentRef, cellId: string) => {
|
||||||
|
return dispatch(
|
||||||
|
actions.executeCell({
|
||||||
|
contentRef,
|
||||||
|
id: cellId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
updateCell: (text: string, id: string, contentRef: ContentRef) => {
|
||||||
|
dispatch(actions.updateCellSource({ id, contentRef, value: text }));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return mapDispatchToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(makeMapStateToProps, makeMapDispatchToProps)(MongoQueryComponent);
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||||
|
import {
|
||||||
|
NotebookComponentBootstrapper,
|
||||||
|
NotebookComponentBootstrapperOptions,
|
||||||
|
} from "../NotebookComponent/NotebookComponentBootstrapper";
|
||||||
|
import MongoQueryComponent from "../MongoQueryComponent/MongoQueryComponent";
|
||||||
|
import { actions, createContentRef, createKernelRef, IContent, KernelRef } from "@nteract/core";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { Notebook } from "@nteract/commutable";
|
||||||
|
|
||||||
|
export class MongoQueryComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
|
||||||
|
public parameters: unknown;
|
||||||
|
private kernelRef: KernelRef;
|
||||||
|
|
||||||
|
constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) {
|
||||||
|
super(options);
|
||||||
|
|
||||||
|
if (!this.contentRef) {
|
||||||
|
this.contentRef = createContentRef();
|
||||||
|
this.kernelRef = createKernelRef();
|
||||||
|
|
||||||
|
const notebook: Notebook = {
|
||||||
|
cells: [
|
||||||
|
{
|
||||||
|
cell_type: "code",
|
||||||
|
metadata: {},
|
||||||
|
execution_count: 0,
|
||||||
|
outputs: [],
|
||||||
|
source: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
metadata: {
|
||||||
|
kernelspec: {
|
||||||
|
displayName: "Mongo",
|
||||||
|
language: "mongocli",
|
||||||
|
name: "mongo",
|
||||||
|
},
|
||||||
|
language_info: {
|
||||||
|
file_extension: "ipynb",
|
||||||
|
mimetype: "application/json",
|
||||||
|
name: "mongo",
|
||||||
|
version: "1.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nbformat: 4,
|
||||||
|
nbformat_minor: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
const model: IContent<"notebook"> = {
|
||||||
|
name: "mongo-query-component-notebook.ipynb",
|
||||||
|
path: "mongo-query-component-notebook.ipynb",
|
||||||
|
type: "notebook",
|
||||||
|
writable: true,
|
||||||
|
created: "",
|
||||||
|
last_modified: "",
|
||||||
|
mimetype: "application/x-ipynb+json",
|
||||||
|
content: notebook,
|
||||||
|
format: "json",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Request fetching notebook content
|
||||||
|
this.getStore().dispatch(
|
||||||
|
actions.fetchContentFulfilled({
|
||||||
|
filepath: model.path,
|
||||||
|
model,
|
||||||
|
kernelRef: this.kernelRef,
|
||||||
|
contentRef: this.contentRef,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderComponent(): JSX.Element {
|
||||||
|
const props = {
|
||||||
|
contentRef: this.contentRef,
|
||||||
|
kernelRef: this.kernelRef,
|
||||||
|
databaseId: this.databaseId,
|
||||||
|
collectionId: this.collectionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Provider store={this.getStore()}>
|
||||||
|
<MongoQueryComponent {...props} />;
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() && this.container.isEnableMongoCapabilityPresent()) {
|
if (this.container.isPreferredApiMongoDB()) {
|
||||||
createMongoWildcardIndex = this.shouldCreateMongoWildcardIndex();
|
createMongoWildcardIndex = this.shouldCreateMongoWildcardIndex();
|
||||||
} else if (this.showIndexingOptionsForSharedThroughput()) {
|
} else if (this.showIndexingOptionsForSharedThroughput()) {
|
||||||
if (this.useIndexingForSharedThroughput()) {
|
if (this.useIndexingForSharedThroughput()) {
|
||||||
|
|||||||
142
src/Explorer/Panes/DeleteCollectionConfirmationPane.test.ts
Normal file
142
src/Explorer/Panes/DeleteCollectionConfirmationPane.test.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
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.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.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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
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,7 +1,9 @@
|
|||||||
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";
|
||||||
@@ -48,7 +50,18 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase
|
|||||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||||
paneTitle: this.title(),
|
paneTitle: this.title(),
|
||||||
});
|
});
|
||||||
return deleteCollection(selectedCollection.databaseId, selectedCollection.id()).then(
|
let promise: Promise<any>;
|
||||||
|
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();
|
||||||
|
|||||||
@@ -1,186 +0,0 @@
|
|||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -52,71 +52,81 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
|
|||||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||||
paneTitle: this.title(),
|
paneTitle: this.title(),
|
||||||
});
|
});
|
||||||
return Q(
|
// TODO: Should not be a Q promise anymore, but the Cassandra code requires it
|
||||||
deleteDatabase(selectedDatabase.id()).then(
|
let promise: Q.Promise<any>;
|
||||||
() => {
|
if (this.container.isPreferredApiCassandra()) {
|
||||||
this.isExecuting(false);
|
promise = (<CassandraAPIDataClient>this.container.tableDataClient).deleteTableOrKeyspace(
|
||||||
this.close();
|
this.container.databaseAccount().properties.cassandraEndpoint,
|
||||||
this.container.refreshAllDatabases();
|
this.container.databaseAccount().id,
|
||||||
this.container.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
|
`DROP KEYSPACE ${selectedDatabase.id()};`,
|
||||||
this.container.selectedNode(null);
|
this.container
|
||||||
selectedDatabase
|
);
|
||||||
.collections()
|
} else {
|
||||||
.forEach((collection: ViewModels.Collection) =>
|
promise = Q(deleteDatabase(selectedDatabase.id()));
|
||||||
this.container.tabsManager.closeTabsByComparator(
|
}
|
||||||
(tab) =>
|
return promise.then(
|
||||||
tab.node?.id() === collection.id() &&
|
() => {
|
||||||
(tab.node as ViewModels.Collection).databaseId === collection.databaseId
|
this.isExecuting(false);
|
||||||
)
|
this.close();
|
||||||
);
|
this.container.refreshAllDatabases();
|
||||||
this.resetData();
|
this.container.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
|
||||||
TelemetryProcessor.traceSuccess(
|
this.container.selectedNode(null);
|
||||||
Action.DeleteDatabase,
|
selectedDatabase
|
||||||
{
|
.collections()
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
.forEach((collection: ViewModels.Collection) =>
|
||||||
defaultExperience: this.container.defaultExperience(),
|
this.container.tabsManager.closeTabsByComparator(
|
||||||
databaseId: selectedDatabase.id(),
|
(tab) =>
|
||||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
tab.node?.id() === collection.id() &&
|
||||||
paneTitle: this.title(),
|
(tab.node as ViewModels.Collection).databaseId === collection.databaseId
|
||||||
},
|
)
|
||||||
startKey
|
);
|
||||||
|
this.resetData();
|
||||||
|
TelemetryProcessor.traceSuccess(
|
||||||
|
Action.DeleteDatabase,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
databaseId: selectedDatabase.id(),
|
||||||
|
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||||
|
paneTitle: this.title(),
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.shouldRecordFeedback()) {
|
||||||
|
let deleteFeedback = new DeleteFeedback(
|
||||||
|
this.container.databaseAccount().id,
|
||||||
|
this.container.databaseAccount().name,
|
||||||
|
DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience()),
|
||||||
|
this.databaseDeleteFeedback()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.shouldRecordFeedback()) {
|
TelemetryProcessor.trace(Action.DeleteDatabase, ActionModifiers.Mark, {
|
||||||
let deleteFeedback = new DeleteFeedback(
|
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
|
||||||
this.container.databaseAccount().id,
|
});
|
||||||
this.container.databaseAccount().name,
|
|
||||||
DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience()),
|
|
||||||
this.databaseDeleteFeedback()
|
|
||||||
);
|
|
||||||
|
|
||||||
TelemetryProcessor.trace(Action.DeleteDatabase, ActionModifiers.Mark, {
|
this.databaseDeleteFeedback("");
|
||||||
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.databaseDeleteFeedback("");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
this.isExecuting(false);
|
|
||||||
const errorMessage = getErrorMessage(error);
|
|
||||||
this.formErrors(errorMessage);
|
|
||||||
this.formErrorsDetails(errorMessage);
|
|
||||||
TelemetryProcessor.traceFailure(
|
|
||||||
Action.DeleteDatabase,
|
|
||||||
{
|
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
|
||||||
defaultExperience: this.container.defaultExperience(),
|
|
||||||
databaseId: selectedDatabase.id(),
|
|
||||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
|
||||||
paneTitle: this.title(),
|
|
||||||
error: errorMessage,
|
|
||||||
errorStack: getErrorStack(error),
|
|
||||||
},
|
|
||||||
startKey
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
|
(error: any) => {
|
||||||
|
this.isExecuting(false);
|
||||||
|
const errorMessage = getErrorMessage(error);
|
||||||
|
this.formErrors(errorMessage);
|
||||||
|
this.formErrorsDetails(errorMessage);
|
||||||
|
TelemetryProcessor.traceFailure(
|
||||||
|
Action.DeleteDatabase,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
databaseId: selectedDatabase.id(),
|
||||||
|
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||||
|
paneTitle: this.title(),
|
||||||
|
error: errorMessage,
|
||||||
|
errorStack: getErrorStack(error),
|
||||||
|
},
|
||||||
|
startKey
|
||||||
|
);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
@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;
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
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";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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,11 +10,7 @@ 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, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
import { handleError, getErrorMessage } 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>;
|
||||||
@@ -70,7 +66,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.createFormError,
|
onError: this.createFormErrorForLargeImageSelection,
|
||||||
clearFormError: this.clearFormError,
|
clearFormError: this.clearFormError,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -144,21 +140,10 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
this.isExecuting = true;
|
this.isExecuting = true;
|
||||||
this.triggerRender();
|
this.triggerRender();
|
||||||
|
|
||||||
let startKey: number;
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
startKey = traceStart(Action.NotebooksGalleryPublish, {
|
if (!this.name || !this.description || !this.author) {
|
||||||
databaseAccountName: this.container.databaseAccount()?.name,
|
throw new Error("Name, description, and author are required");
|
||||||
defaultExperience: this.container.defaultExperience(),
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const response = await this.junoClient.publishNotebook(
|
const response = await this.junoClient.publishNotebook(
|
||||||
this.name,
|
this.name,
|
||||||
@@ -172,43 +157,17 @@ 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 ${FileSystemUtil.stripExtension(this.name, "ipynb")} to gallery`;
|
this.formError = `Failed to publish ${this.name} to gallery`;
|
||||||
this.formErrorDetail = `${errorMessage}`;
|
this.formErrorDetail = `${errorMessage}`;
|
||||||
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", this.formError);
|
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", this.formError);
|
||||||
return;
|
return;
|
||||||
@@ -221,7 +180,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private createFormError = (formError: string, formErrorDetail: string, area: string): void => {
|
private createFormErrorForLargeImageSelection = (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.CustomImage,
|
type: ImageTypes.Url,
|
||||||
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 =
|
||||||
"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 notebook has your data. Please make sure you delete any sensitive data/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,7 +120,6 @@ 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 });
|
||||||
@@ -141,23 +140,17 @@ 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.CustomImage,
|
defaultSelectedKey: ImageTypes.Url,
|
||||||
ariaLabel: "Cover image",
|
ariaLabel: "Cover image",
|
||||||
options: options.map((value: string) => ({ text: value, key: value })),
|
options: [
|
||||||
|
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 {
|
||||||
@@ -179,12 +172,11 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
this.nameProps = {
|
this.nameProps = {
|
||||||
label: "Name",
|
label: "Name",
|
||||||
ariaLabel: "Name",
|
ariaLabel: "Name",
|
||||||
defaultValue: FileSystemUtil.stripExtension(this.props.notebookName, "ipynb"),
|
defaultValue: this.props.notebookName,
|
||||||
required: true,
|
required: true,
|
||||||
onChange: (event, newValue) => {
|
onChange: (event, newValue) => {
|
||||||
const notebookName = newValue + ".ipynb";
|
this.props.onChangeName(newValue);
|
||||||
this.props.onChangeName(notebookName);
|
this.setState({ notebookName: newValue });
|
||||||
this.setState({ notebookName });
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -301,16 +293,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: undefined,
|
downloads: 0,
|
||||||
favorites: undefined,
|
favorites: 0,
|
||||||
views: undefined,
|
views: 0,
|
||||||
newCellId: undefined,
|
newCellId: undefined,
|
||||||
policyViolations: undefined,
|
policyViolations: undefined,
|
||||||
pendingScanJobIds: undefined,
|
pendingScanJobIds: undefined,
|
||||||
}}
|
}}
|
||||||
isFavorite={undefined}
|
isFavorite={false}
|
||||||
showDownload={false}
|
showDownload={true}
|
||||||
showDelete={false}
|
showDelete={true}
|
||||||
onClick={undefined}
|
onClick={undefined}
|
||||||
onTagClick={undefined}
|
onTagClick={undefined}
|
||||||
onFavoriteClick={undefined}
|
onFavoriteClick={undefined}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,71 +0,0 @@
|
|||||||
// 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>
|
||||||
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 notebook has your data. Please make sure you delete any sensitive data/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"
|
defaultValue="SampleNotebook.ipynb"
|
||||||
label="Name"
|
label="Name"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
required={true}
|
required={true}
|
||||||
@@ -52,29 +52,36 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
|||||||
<StackItem>
|
<StackItem>
|
||||||
<StyledWithResponsiveMode
|
<StyledWithResponsiveMode
|
||||||
ariaLabel="Cover image"
|
ariaLabel="Cover image"
|
||||||
defaultSelectedKey="Custom Image"
|
defaultSelectedKey="URL"
|
||||||
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": "URL",
|
"key": "Take Screenshot",
|
||||||
"text": "URL",
|
"text": "Take Screenshot",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"key": "Use First Display Output",
|
||||||
|
"text": "Use First Display Output",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<input
|
<StyledTextFieldBase
|
||||||
accept="image/*"
|
ariaLabel="Cover image url"
|
||||||
id="selectImageFile"
|
label="Cover image url"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
type="file"
|
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
@@ -89,8 +96,8 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
|||||||
"author": "CosmosDB",
|
"author": "CosmosDB",
|
||||||
"created": "2020-07-17T00:00:00Z",
|
"created": "2020-07-17T00:00:00Z",
|
||||||
"description": "",
|
"description": "",
|
||||||
"downloads": undefined,
|
"downloads": 0,
|
||||||
"favorites": undefined,
|
"favorites": 0,
|
||||||
"gitSha": undefined,
|
"gitSha": undefined,
|
||||||
"id": undefined,
|
"id": undefined,
|
||||||
"isSample": false,
|
"isSample": false,
|
||||||
@@ -102,11 +109,12 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
|||||||
"",
|
"",
|
||||||
],
|
],
|
||||||
"thumbnailUrl": undefined,
|
"thumbnailUrl": undefined,
|
||||||
"views": undefined,
|
"views": 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
showDelete={false}
|
isFavorite={false}
|
||||||
showDownload={false}
|
showDelete={true}
|
||||||
|
showDownload={true}
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ 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[];
|
||||||
@@ -346,7 +345,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)
|
this.createOrDeleteQuery(cassandraEndpoint, resourceId, createKeyspaceQuery, explorer)
|
||||||
.then(
|
.then(
|
||||||
(data: any) => {
|
(data: any) => {
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
@@ -392,7 +391,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)
|
this.createOrDeleteQuery(cassandraEndpoint, resourceId, createTableQuery, explorer)
|
||||||
.then(
|
.then(
|
||||||
(data: any) => {
|
(data: any) => {
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
@@ -417,6 +416,41 @@ 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);
|
||||||
@@ -517,7 +551,12 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private createOrDeleteQuery(cassandraEndpoint: string, resourceId: string, query: string): Q.Promise<any> {
|
private createOrDeleteQuery(
|
||||||
|
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 =
|
||||||
@@ -527,7 +566,7 @@ export class CassandraAPIDataClient extends TableDataClient {
|
|||||||
$.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
$.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
|
||||||
type: "POST",
|
type: "POST",
|
||||||
data: {
|
data: {
|
||||||
accountName: userContext.databaseAccount?.name,
|
accountName: explorer.databaseAccount() && explorer.databaseAccount().name,
|
||||||
cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint),
|
cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint),
|
||||||
resourceId: resourceId,
|
resourceId: resourceId,
|
||||||
query: query,
|
query: query,
|
||||||
|
|||||||
@@ -387,6 +387,8 @@ 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,7 +11,6 @@ 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;
|
||||||
@@ -22,46 +21,27 @@ 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.galleryAndNotebookViewerComponentProps = {
|
this.container = options.container;
|
||||||
|
const props: 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: options.selectedTab,
|
selectedTab: GalleryViewerTab.OfficialSamples,
|
||||||
sortBy: SortBy.MostViewed,
|
sortBy: SortBy.MostViewed,
|
||||||
searchText: undefined,
|
searchText: undefined,
|
||||||
};
|
};
|
||||||
this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter(
|
|
||||||
this.galleryAndNotebookViewerComponentProps
|
this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter(props);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public reset(options: GalleryTabOptions) {
|
protected getContainer(): Explorer {
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/Explorer/Tabs/MongoDocumentsTabV2.html
Normal file
1
src/Explorer/Tabs/MongoDocumentsTabV2.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<div data-bind="react:mongoQueryComponentAdapter" style="height: 100%"></div>
|
||||||
49
src/Explorer/Tabs/MongoDocumentsTabV2.ts
Normal file
49
src/Explorer/Tabs/MongoDocumentsTabV2.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import * as Q from "q";
|
||||||
|
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
|
||||||
|
import { MongoQueryComponentAdapter } from "../Notebook/MongoQueryComponent/MongoQueryComponentAdapter";
|
||||||
|
|
||||||
|
export default class MongoDocumentsTabV2 extends NotebookTabBase {
|
||||||
|
private mongoQueryComponentAdapter: MongoQueryComponentAdapter;
|
||||||
|
|
||||||
|
constructor(options: NotebookTabBaseOptions) {
|
||||||
|
super(options);
|
||||||
|
this.mongoQueryComponentAdapter = new MongoQueryComponentAdapter(
|
||||||
|
{
|
||||||
|
contentRef: undefined,
|
||||||
|
notebookClient: NotebookTabBase.clientManager,
|
||||||
|
},
|
||||||
|
options.collection?.databaseId,
|
||||||
|
options.collection?.id()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onCloseTabButtonClick(): Q.Promise<void> {
|
||||||
|
super.onCloseTabButtonClick();
|
||||||
|
|
||||||
|
// const cleanup = () => {
|
||||||
|
// this.notebookComponentAdapter.notebookShutdown();
|
||||||
|
// this.isActive(false);
|
||||||
|
// super.onCloseTabButtonClick();
|
||||||
|
// };
|
||||||
|
|
||||||
|
// if (this.notebookComponentAdapter.isContentDirty()) {
|
||||||
|
// this.container.showOkCancelModalDialog(
|
||||||
|
// "Close without saving?",
|
||||||
|
// `File has unsaved changes, close without saving?`,
|
||||||
|
// "Close",
|
||||||
|
// cleanup,
|
||||||
|
// "Cancel",
|
||||||
|
// undefined
|
||||||
|
// );
|
||||||
|
// return Q.resolve(null);
|
||||||
|
// } else {
|
||||||
|
// cleanup();
|
||||||
|
// return Q.resolve(null);
|
||||||
|
// }
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected buildCommandBarOptions(): void {
|
||||||
|
this.updateNavbarWithTabsButtons();
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/Explorer/Tabs/NotebookTabBase.ts
Normal file
50
src/Explorer/Tabs/NotebookTabBase.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
import TabsBase from "./TabsBase";
|
||||||
|
|
||||||
|
import { NotebookClientV2 } from "../Notebook/NotebookClientV2";
|
||||||
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import Explorer from "../Explorer";
|
||||||
|
import { ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
import { Areas } from "../../Common/Constants";
|
||||||
|
|
||||||
|
export interface NotebookTabBaseOptions extends ViewModels.TabOptions {
|
||||||
|
container: Explorer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every notebook-based tab inherits from this class. It holds the static reference to a notebook client (singleton)
|
||||||
|
*/
|
||||||
|
export default class NotebookTabBase extends TabsBase {
|
||||||
|
protected static clientManager: NotebookClientV2;
|
||||||
|
protected container: Explorer;
|
||||||
|
|
||||||
|
constructor(options: NotebookTabBaseOptions) {
|
||||||
|
super(options);
|
||||||
|
|
||||||
|
this.container = options.container;
|
||||||
|
|
||||||
|
if (!NotebookTabBase.clientManager) {
|
||||||
|
NotebookTabBase.clientManager = new NotebookClientV2({
|
||||||
|
connectionInfo: this.container.notebookServerInfo(),
|
||||||
|
databaseAccountName: this.container.databaseAccount().name,
|
||||||
|
defaultExperience: this.container.defaultExperience(),
|
||||||
|
contentProvider: this.container.notebookManager?.notebookContentProvider,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override base behavior
|
||||||
|
*/
|
||||||
|
protected getContainer(): Explorer {
|
||||||
|
return this.container;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected traceTelemetry(actionType: number): void {
|
||||||
|
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
||||||
|
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
||||||
|
defaultExperience: this.container.defaultExperience && this.container.defaultExperience(),
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,7 @@ import * as _ from "underscore";
|
|||||||
import * as Q from "q";
|
import * as Q from "q";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
|
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import TabsBase from "./TabsBase";
|
|
||||||
|
|
||||||
import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg";
|
import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.svg";
|
||||||
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
|
import CutIcon from "../../../images/notebook/Notebook-cut.svg";
|
||||||
@@ -17,33 +15,27 @@ import SaveIcon from "../../../images/save-cosmos.svg";
|
|||||||
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
|
import ClearAllOutputsIcon from "../../../images/notebook/Notebook-clear-all-outputs.svg";
|
||||||
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 { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
|
import { 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";
|
||||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
|
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
|
||||||
import { NotebookConfigurationUtils } from "../../Utils/NotebookConfigurationUtils";
|
import { NotebookConfigurationUtils } from "../../Utils/NotebookConfigurationUtils";
|
||||||
import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2";
|
import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2";
|
||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import Explorer from "../Explorer";
|
|
||||||
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { toJS, stringifyNotebook } from "@nteract/commutable";
|
import { toJS, stringifyNotebook } from "@nteract/commutable";
|
||||||
import { appInsights } from "../../Shared/appInsights";
|
import { appInsights } from "../../Shared/appInsights";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
|
||||||
|
|
||||||
export interface NotebookTabOptions extends ViewModels.TabOptions {
|
export interface NotebookTabOptions extends NotebookTabBaseOptions {
|
||||||
account: DataModels.DatabaseAccount;
|
|
||||||
masterKey: string;
|
|
||||||
container: Explorer;
|
|
||||||
notebookContentItem: NotebookContentItem;
|
notebookContentItem: NotebookContentItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class NotebookTabV2 extends TabsBase {
|
export default class NotebookTabV2 extends NotebookTabBase {
|
||||||
private static clientManager: NotebookClientV2;
|
|
||||||
private container: Explorer;
|
|
||||||
public notebookPath: ko.Observable<string>;
|
public notebookPath: ko.Observable<string>;
|
||||||
private selectedSparkPool: ko.Observable<string>;
|
private selectedSparkPool: ko.Observable<string>;
|
||||||
private notebookComponentAdapter: NotebookComponentAdapter;
|
private notebookComponentAdapter: NotebookComponentAdapter;
|
||||||
@@ -52,16 +44,6 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
super(options);
|
super(options);
|
||||||
|
|
||||||
this.container = options.container;
|
this.container = options.container;
|
||||||
|
|
||||||
if (!NotebookTabV2.clientManager) {
|
|
||||||
NotebookTabV2.clientManager = new NotebookClientV2({
|
|
||||||
connectionInfo: this.container.notebookServerInfo(),
|
|
||||||
databaseAccountName: this.container.databaseAccount().name,
|
|
||||||
defaultExperience: this.container.defaultExperience(),
|
|
||||||
contentProvider: this.container.notebookManager?.notebookContentProvider,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notebookPath = ko.observable(options.notebookContentItem.path);
|
this.notebookPath = ko.observable(options.notebookContentItem.path);
|
||||||
|
|
||||||
this.container.notebookServerInfo.subscribe((newValue: DataModels.NotebookWorkspaceConnectionInfo) => {
|
this.container.notebookServerInfo.subscribe((newValue: DataModels.NotebookWorkspaceConnectionInfo) => {
|
||||||
@@ -71,7 +53,7 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
this.notebookComponentAdapter = new NotebookComponentAdapter({
|
this.notebookComponentAdapter = new NotebookComponentAdapter({
|
||||||
contentItem: options.notebookContentItem,
|
contentItem: options.notebookContentItem,
|
||||||
notebooksBasePath: this.container.getNotebookBasePath(),
|
notebooksBasePath: this.container.getNotebookBasePath(),
|
||||||
notebookClient: NotebookTabV2.clientManager,
|
notebookClient: NotebookTabBase.clientManager,
|
||||||
onUpdateKernelInfo: this.onKernelUpdate,
|
onUpdateKernelInfo: this.onKernelUpdate,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,10 +99,6 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
return await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName());
|
return await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public getContainer(): Explorer {
|
|
||||||
return this.container;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getTabsButtons(): CommandButtonComponentProps[] {
|
protected getTabsButtons(): CommandButtonComponentProps[] {
|
||||||
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
||||||
|
|
||||||
@@ -485,10 +463,6 @@ 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,
|
||||||
@@ -508,12 +482,4 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
|
|
||||||
this.container.copyNotebook(notebookContent.name, content);
|
this.container.copyNotebook(notebookContent.name, content);
|
||||||
};
|
};
|
||||||
|
|
||||||
private traceTelemetry(actionType: number) {
|
|
||||||
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
|
||||||
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
|
||||||
defaultExperience: this.container.defaultExperience && this.container.defaultExperience(),
|
|
||||||
dataExplorerArea: Areas.Notebook,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default class NotebookViewerTab extends TabsBase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getContainer(): Explorer {
|
protected getContainer(): Explorer {
|
||||||
return this.container;
|
return this.container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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";
|
||||||
@@ -10,27 +11,23 @@ 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 class SettingsTabV2 extends TabsBase {
|
export default 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.TabOptions) {
|
constructor(options: ViewModels.SettingsTabV2Options) {
|
||||||
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>(() => {
|
||||||
@@ -48,95 +45,49 @@ export class CollectionSettingsTabV2 extends SettingsTabV2 {
|
|||||||
public async onActivate(): Promise<void> {
|
public async onActivate(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.isExecuting(true);
|
this.isExecuting(true);
|
||||||
|
await this.currentCollection.loadOffer();
|
||||||
const collection: ViewModels.Collection = this.collection as ViewModels.Collection;
|
|
||||||
await collection.loadOffer();
|
|
||||||
// passed in options and set by parent as "Settings" by default
|
// passed in options and set by parent as "Settings" by default
|
||||||
this.tabTitle(collection.offer() ? "Settings" : "Scale & Settings");
|
this.tabTitle(this.currentCollection.offer() ? "Settings" : "Scale & Settings");
|
||||||
|
|
||||||
const data: DataModels.Notification = await collection.getPendingThroughputSplitNotification();
|
this.options.getPendingNotification.then(
|
||||||
this.notification = data;
|
(data: DataModels.Notification) => {
|
||||||
this.notificationRead(true);
|
this.notification = data;
|
||||||
} catch (error) {
|
this.notificationRead(true);
|
||||||
const errorMessage = getErrorMessage(error);
|
|
||||||
this.notification = undefined;
|
|
||||||
this.notificationRead(true);
|
|
||||||
traceFailure(
|
|
||||||
Action.Tab,
|
|
||||||
{
|
|
||||||
databaseAccountName: this.collection.container.databaseAccount().name,
|
|
||||||
databaseName: this.collection.databaseId,
|
|
||||||
collectionName: this.collection.id(),
|
|
||||||
defaultExperience: this.collection.container.defaultExperience(),
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle: this.tabTitle,
|
|
||||||
error: errorMessage,
|
|
||||||
errorStack: getErrorStack(error),
|
|
||||||
},
|
},
|
||||||
this.onLoadStartKey
|
(error) => {
|
||||||
|
const errorMessage = getErrorMessage(error);
|
||||||
|
this.notification = undefined;
|
||||||
|
this.notificationRead(true);
|
||||||
|
traceFailure(
|
||||||
|
Action.Tab,
|
||||||
|
{
|
||||||
|
databaseAccountName: this.options.collection.container.databaseAccount().name,
|
||||||
|
databaseName: this.options.collection.databaseId,
|
||||||
|
collectionName: this.options.collection.id(),
|
||||||
|
defaultExperience: this.options.collection.container.defaultExperience(),
|
||||||
|
dataExplorerArea: Constants.Areas.Tab,
|
||||||
|
tabTitle: this.tabTitle,
|
||||||
|
error: errorMessage,
|
||||||
|
errorStack: getErrorStack(error),
|
||||||
|
},
|
||||||
|
this.options.onLoadStartKey
|
||||||
|
);
|
||||||
|
logConsoleError(
|
||||||
|
`Error while fetching container settings for container ${this.options.collection.id()}: ${errorMessage}`
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
logConsoleError(`Error while fetching container settings for container ${this.collection.id()}: ${errorMessage}`);
|
|
||||||
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.CollectionSettingsV2);
|
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.SettingsV2);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getContainer() {
|
protected getContainer() {
|
||||||
return this._container;
|
return this._container;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import SparkMasterTabTemplate from "./SparkMasterTab.html";
|
|||||||
import NotebookV2TabTemplate from "./NotebookV2Tab.html";
|
import NotebookV2TabTemplate from "./NotebookV2Tab.html";
|
||||||
import TerminalTabTemplate from "./TerminalTab.html";
|
import TerminalTabTemplate from "./TerminalTab.html";
|
||||||
import MongoDocumentsTabTemplate from "./MongoDocumentsTab.html";
|
import MongoDocumentsTabTemplate from "./MongoDocumentsTab.html";
|
||||||
|
import MongoDocumentsTabV2Template from "./MongoDocumentsTabV2.html";
|
||||||
import MongoQueryTabTemplate from "./MongoQueryTab.html";
|
import MongoQueryTabTemplate from "./MongoQueryTab.html";
|
||||||
import MongoShellTabTemplate from "./MongoShellTab.html";
|
import MongoShellTabTemplate from "./MongoShellTab.html";
|
||||||
import QueryTabTemplate from "./QueryTab.html";
|
import QueryTabTemplate from "./QueryTab.html";
|
||||||
@@ -105,6 +106,15 @@ export class MongoQueryTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MongoDocumentsTabV2 {
|
||||||
|
constructor() {
|
||||||
|
return {
|
||||||
|
viewModel: TabComponent,
|
||||||
|
template: MongoDocumentsTabV2Template,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class MongoShellTab {
|
export class MongoShellTab {
|
||||||
constructor() {
|
constructor() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||||||
return Q();
|
return Q();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getContainer(): Explorer {
|
protected 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,11 +143,7 @@
|
|||||||
<!-- /ko -->
|
<!-- /ko -->
|
||||||
|
|
||||||
<!-- ko if: $data.tabKind === 20 -->
|
<!-- ko if: $data.tabKind === 20 -->
|
||||||
<collection-settings-tab-v2 params="{data: $data}"></collection-settings-tab-v2>
|
<settings-tab-v2 params="{data: $data}"></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 {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getContainer(): Explorer {
|
protected getContainer(): Explorer {
|
||||||
return this.container;
|
return this.container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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";
|
||||||
@@ -22,11 +23,12 @@ import ConflictsTab from "../Tabs/ConflictsTab";
|
|||||||
import DocumentsTab from "../Tabs/DocumentsTab";
|
import DocumentsTab from "../Tabs/DocumentsTab";
|
||||||
import GraphTab from "../Tabs/GraphTab";
|
import GraphTab from "../Tabs/GraphTab";
|
||||||
import MongoDocumentsTab from "../Tabs/MongoDocumentsTab";
|
import MongoDocumentsTab from "../Tabs/MongoDocumentsTab";
|
||||||
|
import MongoDocumentsTabV2 from "../Tabs/MongoDocumentsTabV2";
|
||||||
import MongoQueryTab from "../Tabs/MongoQueryTab";
|
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 { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
|
import SettingsTabV2 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";
|
||||||
@@ -253,9 +255,9 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public expandCollection(): void {
|
public expandCollection(): Q.Promise<any> {
|
||||||
if (this.isCollectionExpanded()) {
|
if (this.isCollectionExpanded()) {
|
||||||
return;
|
return Q();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isCollectionExpanded(true);
|
this.isCollectionExpanded(true);
|
||||||
@@ -267,6 +269,8 @@ 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() {
|
||||||
@@ -493,11 +497,11 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mongoDocumentsTabs: MongoDocumentsTab[] = this.container.tabsManager.getTabs(
|
const mongoDocumentsTabs: MongoDocumentsTabV2[] = this.container.tabsManager.getTabs(
|
||||||
ViewModels.CollectionTabKind.Documents,
|
ViewModels.CollectionTabKind.Documents,
|
||||||
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
|
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
|
||||||
) as MongoDocumentsTab[];
|
) as MongoDocumentsTabV2[];
|
||||||
let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0];
|
let mongoDocumentsTab: MongoDocumentsTabV2 = mongoDocumentsTabs && mongoDocumentsTabs[0];
|
||||||
|
|
||||||
if (mongoDocumentsTab) {
|
if (mongoDocumentsTab) {
|
||||||
this.container.tabsManager.activateTab(mongoDocumentsTab);
|
this.container.tabsManager.activateTab(mongoDocumentsTab);
|
||||||
@@ -512,9 +516,8 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
this.documentIds([]);
|
this.documentIds([]);
|
||||||
|
|
||||||
mongoDocumentsTab = new MongoDocumentsTab({
|
mongoDocumentsTab = new MongoDocumentsTabV2({
|
||||||
partitionKey: this.partitionKey,
|
container: this.container,
|
||||||
documentIds: this.documentIds,
|
|
||||||
tabKind: ViewModels.CollectionTabKind.Documents,
|
tabKind: ViewModels.CollectionTabKind.Documents,
|
||||||
title: "Documents",
|
title: "Documents",
|
||||||
tabPath: "",
|
tabPath: "",
|
||||||
@@ -544,12 +547,10 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
|
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
|
||||||
const matchingTabs = this.container.tabsManager.getTabs(
|
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
|
||||||
ViewModels.CollectionTabKind.CollectionSettingsV2,
|
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.SettingsV2, (tab) => {
|
||||||
(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,
|
||||||
@@ -571,20 +572,26 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
|
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
|
||||||
};
|
};
|
||||||
|
|
||||||
let settingsTabV2 = matchingTabs && (matchingTabs[0] as CollectionSettingsTabV2);
|
let settingsTabV2 = matchingTabs && (matchingTabs[0] as SettingsTabV2);
|
||||||
this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions);
|
this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions, pendingNotificationsPromise);
|
||||||
};
|
};
|
||||||
|
|
||||||
private launchSettingsTabV2 = (
|
private launchSettingsTabV2 = (
|
||||||
settingsTabV2: CollectionSettingsTabV2,
|
settingsTabV2: SettingsTabV2,
|
||||||
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);
|
||||||
settingsTabOptions.onLoadStartKey = startKey;
|
settingsTabV2Options.onLoadStartKey = startKey;
|
||||||
settingsTabOptions.tabKind = ViewModels.CollectionTabKind.CollectionSettingsV2;
|
settingsTabV2Options.tabKind = ViewModels.CollectionTabKind.SettingsV2;
|
||||||
settingsTabV2 = new CollectionSettingsTabV2(settingsTabOptions);
|
settingsTabV2 = new SettingsTabV2(settingsTabV2Options);
|
||||||
this.container.tabsManager.activateNewTab(settingsTabV2);
|
this.container.tabsManager.activateNewTab(settingsTabV2);
|
||||||
} else {
|
} else {
|
||||||
this.container.tabsManager.activateTab(settingsTabV2);
|
this.container.tabsManager.activateTab(settingsTabV2);
|
||||||
@@ -969,19 +976,23 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
this.uploadFiles(event.originalEvent.dataTransfer.files);
|
this.uploadFiles(event.originalEvent.dataTransfer.files);
|
||||||
}
|
}
|
||||||
|
|
||||||
public uploadFiles = (fileList: FileList): Promise<UploadDetails> => {
|
public onDeleteCollectionContextMenuClick(source: ViewModels.Collection, event: MouseEvent | KeyboardEvent) {
|
||||||
|
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 Promise.reject("No files specified");
|
return Q.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;
|
||||||
@@ -990,26 +1001,31 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressNotificationId);
|
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressNotificationId);
|
||||||
documentUploader.terminate();
|
documentUploader.terminate();
|
||||||
if (!!runtimeError) {
|
if (!!runtimeError) {
|
||||||
reject(runtimeError);
|
deferred.reject(runtimeError);
|
||||||
} else if (numSuccessful === 0) {
|
} else if (numSuccessful === 0) {
|
||||||
// all uploads failed
|
// all uploads failed
|
||||||
NotificationConsoleUtils.logConsoleError(`Failed to upload all documents to container ${this.id()}`);
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
|
ConsoleDataType.Error,
|
||||||
|
`Failed to upload all documents to container ${this.id()}`
|
||||||
|
);
|
||||||
} else if (numFailed > 0) {
|
} else if (numFailed > 0) {
|
||||||
NotificationConsoleUtils.logConsoleError(
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
|
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.logConsoleInfo(
|
NotificationConsoleUtils.logConsoleMessage(
|
||||||
|
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);
|
||||||
resolve(uploadDetails);
|
deferred.resolve(uploadDetails);
|
||||||
};
|
};
|
||||||
function onerror(reject: (reason: any) => void, event: ErrorEvent) {
|
documentUploader.onerror = (event: ErrorEvent): void => {
|
||||||
documentUploader.terminate();
|
documentUploader.terminate();
|
||||||
reject(event.error);
|
deferred.reject(event.error);
|
||||||
}
|
};
|
||||||
|
|
||||||
const uploaderMessage: StartUploadMessageParams = {
|
const uploaderMessage: StartUploadMessageParams = {
|
||||||
files: fileList,
|
files: fileList,
|
||||||
@@ -1024,68 +1040,42 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise<UploadDetails>((resolve, reject) => {
|
documentUploader.postMessage(uploaderMessage);
|
||||||
documentUploader.onmessage = onmessage.bind(null, resolve, reject);
|
inProgressNotificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||||
documentUploader.onerror = onerror.bind(null, reject);
|
ConsoleDataType.InProgress,
|
||||||
|
`Uploading and creating documents in container ${this.id()}`
|
||||||
|
);
|
||||||
|
|
||||||
documentUploader.postMessage(uploaderMessage);
|
return deferred.promise;
|
||||||
inProgressNotificationId = NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.InProgress,
|
|
||||||
`Uploading and creating documents in container ${this.id()}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public async getPendingThroughputSplitNotification(): Promise<DataModels.Notification> {
|
private _uploadFilesCors(files: FileList): Q.Promise<UploadDetails> {
|
||||||
if (!this.container) {
|
const deferred: Q.Deferred<UploadDetails> = Q.defer<UploadDetails>();
|
||||||
return undefined;
|
const promises: Array<Q.Promise<UploadDetailsRecord>> = [];
|
||||||
|
|
||||||
|
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 {
|
return deferred.promise;
|
||||||
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 undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _uploadFilesCors(files: FileList): Promise<UploadDetails> {
|
private _uploadFile(file: File): Q.Promise<UploadDetailsRecord> {
|
||||||
const data = await Promise.all(Array.from(files).map((file) => this._uploadFile(file)));
|
const deferred: Q.Deferred<UploadDetailsRecord> = Q.defer();
|
||||||
|
|
||||||
return { data };
|
|
||||||
}
|
|
||||||
|
|
||||||
private _uploadFile(file: File): Promise<UploadDetailsRecord> {
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
const onload = (resolve: (value: UploadDetailsRecord) => void, evt: any): void => {
|
reader.onload = (evt: any): void => {
|
||||||
const fileData: string = evt.target.result;
|
const fileData: string = evt.target.result;
|
||||||
this._createDocumentsFromFile(file.name, fileData).then((record) => resolve(record));
|
this._createDocumentsFromFile(file.name, fileData).then((record) => {
|
||||||
|
deferred.resolve(record);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onerror = (resolve: (value: UploadDetailsRecord) => void, evt: ProgressEvent): void => {
|
reader.onerror = (evt: ProgressEvent): void => {
|
||||||
resolve({
|
deferred.resolve({
|
||||||
fileName: file.name,
|
fileName: file.name,
|
||||||
numSucceeded: 0,
|
numSucceeded: 0,
|
||||||
numFailed: 1,
|
numFailed: 1,
|
||||||
@@ -1093,11 +1083,9 @@ export default class Collection implements ViewModels.Collection {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise<UploadDetailsRecord>((resolve) => {
|
reader.readAsText(file);
|
||||||
reader.onload = onload.bind(this, resolve);
|
|
||||||
reader.onerror = onerror.bind(this, resolve);
|
return deferred.promise;
|
||||||
reader.readAsText(file);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> {
|
private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> {
|
||||||
@@ -1131,6 +1119,48 @@ 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,6 +16,7 @@ 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";
|
||||||
|
|
||||||
@@ -58,17 +59,12 @@ export default class Database implements ViewModels.Database {
|
|||||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||||
});
|
});
|
||||||
|
|
||||||
const pendingNotificationsPromise: Promise<DataModels.Notification> = this.getPendingThroughputSplitNotification();
|
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
|
||||||
const useDatabaseSettingsTabV1: boolean = this.container.isFeatureEnabled(
|
const matchingTabs = this.container.tabsManager.getTabs(
|
||||||
Constants.Features.enableDatabaseSettingsTabV1
|
ViewModels.CollectionTabKind.DatabaseSettings,
|
||||||
|
(tab) => tab.node?.id() === this.id()
|
||||||
);
|
);
|
||||||
const tabKind: ViewModels.CollectionTabKind = useDatabaseSettingsTabV1
|
let settingsTab: DatabaseSettingsTab = matchingTabs && (matchingTabs[0] as DatabaseSettingsTab);
|
||||||
? 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,
|
||||||
@@ -79,11 +75,9 @@ export default class Database implements ViewModels.Database {
|
|||||||
});
|
});
|
||||||
pendingNotificationsPromise.then(
|
pendingNotificationsPromise.then(
|
||||||
(data: any) => {
|
(data: any) => {
|
||||||
const pendingNotification: DataModels.Notification = data?.[0];
|
const pendingNotification: DataModels.Notification = data && data[0];
|
||||||
const tabOptions: ViewModels.TabOptions = {
|
settingsTab = new DatabaseSettingsTab({
|
||||||
tabKind: useDatabaseSettingsTabV1
|
tabKind: ViewModels.CollectionTabKind.DatabaseSettings,
|
||||||
? ViewModels.CollectionTabKind.DatabaseSettings
|
|
||||||
: ViewModels.CollectionTabKind.DatabaseSettingsV2,
|
|
||||||
title: "Scale",
|
title: "Scale",
|
||||||
tabPath: "",
|
tabPath: "",
|
||||||
node: this,
|
node: this,
|
||||||
@@ -93,10 +87,8 @@ 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);
|
||||||
},
|
},
|
||||||
@@ -229,40 +221,47 @@ export default class Database implements ViewModels.Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPendingThroughputSplitNotification(): Promise<DataModels.Notification> {
|
private _getPendingThroughputSplitNotification(): Q.Promise<DataModels.Notification> {
|
||||||
if (!this.container) {
|
if (!this.container) {
|
||||||
return undefined;
|
return Q.resolve(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const deferred: Q.Deferred<DataModels.Notification> = Q.defer<DataModels.Notification>();
|
||||||
const notifications: DataModels.Notification[] = await fetchPortalNotifications();
|
fetchPortalNotifications().then(
|
||||||
if (!notifications || notifications.length === 0) {
|
(notifications) => {
|
||||||
return undefined;
|
if (!notifications || notifications.length === 0) {
|
||||||
}
|
deferred.resolve(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return _.find(notifications, (notification: DataModels.Notification) => {
|
const pendingNotification = _.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" &&
|
||||||
!notification.collectionName &&
|
!notification.collectionName &&
|
||||||
notification.databaseName === this.id() &&
|
notification.databaseName === this.id() &&
|
||||||
notification.description &&
|
notification.description &&
|
||||||
throughputUpdateRegExp.test(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.id(),
|
||||||
|
collectionName: this.id(),
|
||||||
|
}),
|
||||||
|
"Settings tree node"
|
||||||
);
|
);
|
||||||
});
|
deferred.resolve(undefined);
|
||||||
} catch (error) {
|
}
|
||||||
Logger.logError(
|
);
|
||||||
JSON.stringify({
|
|
||||||
error: getErrorMessage(error),
|
|
||||||
accountName: this.container && this.container.databaseAccount(),
|
|
||||||
databaseName: this.id(),
|
|
||||||
collectionName: this.id(),
|
|
||||||
}),
|
|
||||||
"Settings tree node"
|
|
||||||
);
|
|
||||||
|
|
||||||
return undefined;
|
return deferred.promise;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(): void {
|
public expandCollection(): Q.Promise<void> {
|
||||||
if (this.isCollectionExpanded()) {
|
if (this.isCollectionExpanded()) {
|
||||||
return;
|
return Q();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isCollectionExpanded(true);
|
this.isCollectionExpanded(true);
|
||||||
@@ -55,6 +55,8 @@ 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,13 +15,12 @@ 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, Source } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers } 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";
|
||||||
@@ -717,23 +716,6 @@ 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 ...");
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
@import "../../less/Common/Constants";
|
|
||||||
|
|
||||||
.standalone-gallery-root {
|
|
||||||
background: @GalleryBackgroundColor;
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
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 { configContext, initializeConfiguration } from "../ConfigContext";
|
import { initializeConfiguration } from "../ConfigContext";
|
||||||
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
|
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
|
||||||
import {
|
import {
|
||||||
GalleryAndNotebookViewerComponent,
|
GalleryAndNotebookViewerComponent,
|
||||||
@@ -25,7 +24,6 @@ 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,
|
||||||
@@ -33,7 +31,7 @@ const onInit = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const element = (
|
const element = (
|
||||||
<div className="standalone-gallery-root">
|
<>
|
||||||
<header>
|
<header>
|
||||||
<GalleryHeaderComponent />
|
<GalleryHeaderComponent />
|
||||||
</header>
|
</header>
|
||||||
@@ -54,7 +52,7 @@ const onInit = async () => {
|
|||||||
|
|
||||||
<GalleryAndNotebookViewerComponent {...props} />
|
<GalleryAndNotebookViewerComponent {...props} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
ReactDOM.render(element, document.getElementById("galleryContent"));
|
ReactDOM.render(element, document.getElementById("galleryContent"));
|
||||||
|
|||||||
@@ -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/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}/downloads`,
|
`${configContext.JUNO_ENDPOINT}/api/notebooks/${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/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}/favorite`,
|
`${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleSubscriptionId}/${sampleDatabaseAccount.name}/gallery/${id}/favorite`,
|
||||||
{
|
{
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -270,16 +270,13 @@ 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/gallery/${id}/unfavorite`, {
|
||||||
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}/unfavorite`,
|
method: "PATCH",
|
||||||
{
|
headers: {
|
||||||
method: "PATCH",
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
headers: {
|
[HttpHeaders.contentType]: "application/json",
|
||||||
[authorizationHeader.header]: authorizationHeader.token,
|
},
|
||||||
[HttpHeaders.contentType]: "application/json",
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getFavoriteNotebooks", async () => {
|
it("getFavoriteNotebooks", async () => {
|
||||||
@@ -292,15 +289,12 @@ 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/gallery/favorites`, {
|
||||||
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/favorites`,
|
headers: {
|
||||||
{
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
headers: {
|
[HttpHeaders.contentType]: "application/json",
|
||||||
[authorizationHeader.header]: authorizationHeader.token,
|
},
|
||||||
[HttpHeaders.contentType]: "application/json",
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getPublishedNotebooks", async () => {
|
it("getPublishedNotebooks", async () => {
|
||||||
@@ -314,7 +308,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/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/published`,
|
`${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleSubscriptionId}/gallery/published`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
[authorizationHeader.header]: authorizationHeader.token,
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
@@ -335,16 +329,13 @@ 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/gallery/${id}`, {
|
||||||
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}`,
|
method: "DELETE",
|
||||||
{
|
headers: {
|
||||||
method: "DELETE",
|
[authorizationHeader.header]: authorizationHeader.token,
|
||||||
headers: {
|
[HttpHeaders.contentType]: "application/json",
|
||||||
[authorizationHeader.header]: authorizationHeader.token,
|
},
|
||||||
[HttpHeaders.contentType]: "application/json",
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("publishNotebook", async () => {
|
it("publishNotebook", async () => {
|
||||||
@@ -373,7 +364,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/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery`,
|
`${configContext.JUNO_ENDPOINT}/api/notebooks/${sampleSubscriptionId}/${sampleDatabaseAccount.name}/gallery`,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -186,7 +186,10 @@ export class JunoClient {
|
|||||||
|
|
||||||
public async getPublicGalleryData(): Promise<IJunoResponse<IPublicGalleryData>> {
|
public async getPublicGalleryData(): Promise<IJunoResponse<IPublicGalleryData>> {
|
||||||
const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/public`;
|
const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/public`;
|
||||||
const response = await window.fetch(url, { headers: JunoClient.getHeaders() });
|
const response = await window.fetch(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: JunoClient.getHeaders(),
|
||||||
|
});
|
||||||
|
|
||||||
let data: IPublicGalleryData;
|
let data: IPublicGalleryData;
|
||||||
if (response.status === HttpStatusCodes.OK) {
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
@@ -219,7 +222,10 @@ export class JunoClient {
|
|||||||
|
|
||||||
public async isCodeOfConductAccepted(): Promise<IJunoResponse<boolean>> {
|
public async isCodeOfConductAccepted(): Promise<IJunoResponse<boolean>> {
|
||||||
const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/isCodeOfConductAccepted`;
|
const url = `${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/isCodeOfConductAccepted`;
|
||||||
const response = await window.fetch(url, { headers: JunoClient.getHeaders() });
|
const response = await window.fetch(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: JunoClient.getHeaders(),
|
||||||
|
});
|
||||||
|
|
||||||
let data: boolean;
|
let data: boolean;
|
||||||
if (response.status === HttpStatusCodes.OK) {
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
@@ -277,7 +283,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.getNotebooksSubscriptionIdAccountUrl()}/gallery/${id}/downloads`, {
|
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery/${id}/downloads`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
});
|
});
|
||||||
@@ -311,7 +317,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.getNotebooksSubscriptionIdAccountUrl()}/gallery/${id}/unfavorite`, {
|
const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/${id}/unfavorite`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
});
|
});
|
||||||
@@ -328,19 +334,19 @@ export class JunoClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getFavoriteNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
public async getFavoriteNotebooks(): Promise<IJunoResponse<IGalleryItem[]>> {
|
||||||
return await this.getNotebooks(`${this.getNotebooksSubscriptionIdAccountUrl()}/gallery/favorites`, {
|
return await this.getNotebooks(`${this.getNotebooksUrl()}/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.getNotebooksSubscriptionIdAccountUrl()}/gallery/published`, {
|
return await this.getNotebooks(`${this.getNotebooksUrl()}/${this.getSubscriptionId()}/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.getNotebooksSubscriptionIdAccountUrl()}/gallery/${id}`, {
|
const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
});
|
});
|
||||||
@@ -495,8 +501,12 @@ export class JunoClient {
|
|||||||
return userContext.subscriptionId;
|
return userContext.subscriptionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getNotebooksAccountUrl(): string {
|
||||||
|
return `${this.getNotebooksUrl()}/${this.getAccount()}`;
|
||||||
|
}
|
||||||
|
|
||||||
private getNotebooksSubscriptionIdAccountUrl(): string {
|
private getNotebooksSubscriptionIdAccountUrl(): string {
|
||||||
return `${this.getNotebooksUrl()}/subscriptions/${this.getSubscriptionId()}/databaseAccounts/${this.getAccount()}`;
|
return `${this.getNotebooksUrl()}/${this.getSubscriptionId()}/${this.getAccount()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAnalyticsUrl(): string {
|
private getAnalyticsUrl(): string {
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
"translations": {
|
|
||||||
"Common": {
|
|
||||||
"Save": "Save",
|
|
||||||
"Discard": "Discard",
|
|
||||||
"Refresh": "Refesh"
|
|
||||||
},
|
|
||||||
"SelfServeExample": {
|
|
||||||
"North Central US": "North Central US",
|
|
||||||
"West US": "West US",
|
|
||||||
"East US 2": "East US 2",
|
|
||||||
"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": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
15
src/Main.tsx
15
src/Main.tsx
@@ -14,7 +14,6 @@ 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";
|
||||||
@@ -65,9 +64,7 @@ import arrowLeftImg from "../images/imgarrowlefticon.svg";
|
|||||||
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
|
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
|
||||||
import { useConfig } from "./hooks/useConfig";
|
import { useConfig } from "./hooks/useConfig";
|
||||||
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
|
||||||
import { useSidePanel } from "./hooks/useSidePanel";
|
|
||||||
import { NotificationConsoleComponent } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
import { NotificationConsoleComponent } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { PanelContainerComponent } from "./Explorer/Panes/PanelContainerComponent";
|
|
||||||
|
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
|
|
||||||
@@ -76,15 +73,10 @@ const App: React.FunctionComponent = () => {
|
|||||||
const [notificationConsoleData, setNotificationConsoleData] = useState(undefined);
|
const [notificationConsoleData, setNotificationConsoleData] = useState(undefined);
|
||||||
//TODO: Refactor so we don't need to pass the id to remove a console data
|
//TODO: Refactor so we don't need to pass the id to remove a console data
|
||||||
const [inProgressConsoleDataIdToBeDeleted, setInProgressConsoleDataIdToBeDeleted] = useState("");
|
const [inProgressConsoleDataIdToBeDeleted, setInProgressConsoleDataIdToBeDeleted] = useState("");
|
||||||
|
|
||||||
const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel();
|
|
||||||
|
|
||||||
const explorerParams: ExplorerParams = {
|
const explorerParams: ExplorerParams = {
|
||||||
setIsNotificationConsoleExpanded,
|
setIsNotificationConsoleExpanded,
|
||||||
setNotificationConsoleData,
|
setNotificationConsoleData,
|
||||||
setInProgressConsoleDataIdToBeDeleted,
|
setInProgressConsoleDataIdToBeDeleted,
|
||||||
openSidePanel,
|
|
||||||
closeSidePanel,
|
|
||||||
};
|
};
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
useKnockoutExplorer(config, explorerParams);
|
useKnockoutExplorer(config, explorerParams);
|
||||||
@@ -317,13 +309,6 @@ 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,9 +288,11 @@ 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.expandCollection();
|
collection &&
|
||||||
const storedProcedure = collection && collection.findStoredProcedureWithId(sprocId);
|
collection.expandCollection().then(() => {
|
||||||
storedProcedure && storedProcedure.open();
|
const storedProcedure = collection && collection.findStoredProcedureWithId(sprocId);
|
||||||
|
storedProcedure && storedProcedure.open();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,9 +319,11 @@ 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.expandCollection();
|
collection &&
|
||||||
const trigger = collection && collection.findTriggerWithId(triggerId);
|
collection.expandCollection().then(() => {
|
||||||
trigger && trigger.open();
|
const trigger = collection && collection.findTriggerWithId(triggerId);
|
||||||
|
trigger && trigger.open();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,9 +350,11 @@ 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.expandCollection();
|
collection &&
|
||||||
const userDefinedFunction = collection && collection.findUserDefinedFunctionWithId(udfId);
|
collection.expandCollection().then(() => {
|
||||||
userDefinedFunction && userDefinedFunction.open();
|
const userDefinedFunction = collection && collection.findUserDefinedFunctionWithId(udfId);
|
||||||
|
userDefinedFunction && userDefinedFunction.open();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ interface Decorator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface InputOptionsBase {
|
interface InputOptionsBase {
|
||||||
labelTKey: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NumberInputOptions extends InputOptionsBase {
|
export interface NumberInputOptions extends InputOptionsBase {
|
||||||
@@ -19,17 +19,17 @@ export interface NumberInputOptions extends InputOptionsBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StringInputOptions extends InputOptionsBase {
|
export interface StringInputOptions extends InputOptionsBase {
|
||||||
placeholderTKey?: (() => Promise<string>) | string;
|
placeholder?: (() => Promise<string>) | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BooleanInputOptions extends InputOptionsBase {
|
export interface BooleanInputOptions extends InputOptionsBase {
|
||||||
trueLabelTKey: (() => Promise<string>) | string;
|
trueLabel: (() => Promise<string>) | string;
|
||||||
falseLabelTKey: (() => Promise<string>) | string;
|
falseLabel: (() => 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;
|
placeholder?: (() => Promise<string>) | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DescriptionDisplayOptions {
|
export interface DescriptionDisplayOptions {
|
||||||
@@ -48,7 +48,7 @@ const isNumberInputOptions = (inputOptions: InputOptions): inputOptions is Numbe
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isBooleanInputOptions = (inputOptions: InputOptions): inputOptions is BooleanInputOptions => {
|
const isBooleanInputOptions = (inputOptions: InputOptions): inputOptions is BooleanInputOptions => {
|
||||||
return "trueLabelTKey" in inputOptions;
|
return "trueLabel" in inputOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isChoiceInputOptions = (inputOptions: InputOptions): inputOptions is ChoiceInputOptions => {
|
const isChoiceInputOptions = (inputOptions: InputOptions): inputOptions is ChoiceInputOptions => {
|
||||||
@@ -92,7 +92,7 @@ export const PropertyInfo = (info: (() => Promise<Info>) | Info): PropertyDecora
|
|||||||
export const Values = (inputOptions: InputOptions): PropertyDecorator => {
|
export const Values = (inputOptions: InputOptions): PropertyDecorator => {
|
||||||
if (isNumberInputOptions(inputOptions)) {
|
if (isNumberInputOptions(inputOptions)) {
|
||||||
return addToMap(
|
return addToMap(
|
||||||
{ name: "labelTKey", value: inputOptions.labelTKey },
|
{ name: "label", value: inputOptions.label },
|
||||||
{ name: "min", value: inputOptions.min },
|
{ name: "min", value: inputOptions.min },
|
||||||
{ name: "max", value: inputOptions.max },
|
{ name: "max", value: inputOptions.max },
|
||||||
{ name: "step", value: inputOptions.step },
|
{ name: "step", value: inputOptions.step },
|
||||||
@@ -100,22 +100,22 @@ export const Values = (inputOptions: InputOptions): PropertyDecorator => {
|
|||||||
);
|
);
|
||||||
} else if (isBooleanInputOptions(inputOptions)) {
|
} else if (isBooleanInputOptions(inputOptions)) {
|
||||||
return addToMap(
|
return addToMap(
|
||||||
{ name: "labelTKey", value: inputOptions.labelTKey },
|
{ name: "label", value: inputOptions.label },
|
||||||
{ name: "trueLabelTKey", value: inputOptions.trueLabelTKey },
|
{ name: "trueLabel", value: inputOptions.trueLabel },
|
||||||
{ name: "falseLabelTKey", value: inputOptions.falseLabelTKey }
|
{ name: "falseLabel", value: inputOptions.falseLabel }
|
||||||
);
|
);
|
||||||
} else if (isChoiceInputOptions(inputOptions)) {
|
} else if (isChoiceInputOptions(inputOptions)) {
|
||||||
return addToMap(
|
return addToMap(
|
||||||
{ name: "labelTKey", value: inputOptions.labelTKey },
|
{ name: "label", value: inputOptions.label },
|
||||||
{ name: "placeholderTKey", value: inputOptions.placeholderTKey },
|
{ name: "placeholder", value: inputOptions.placeholder },
|
||||||
{ name: "choices", value: inputOptions.choices }
|
{ name: "choices", value: inputOptions.choices }
|
||||||
);
|
);
|
||||||
} else if (isDescriptionDisplayOptions(inputOptions)) {
|
} else if (isDescriptionDisplayOptions(inputOptions)) {
|
||||||
return addToMap({ name: "description", value: inputOptions.description });
|
return addToMap({ name: "description", value: inputOptions.description });
|
||||||
} else {
|
} else {
|
||||||
return addToMap(
|
return addToMap(
|
||||||
{ name: "labelTKey", value: inputOptions.labelTKey },
|
{ name: "label", value: inputOptions.label },
|
||||||
{ name: "placeholderTKey", value: inputOptions.placeholderTKey }
|
{ name: "placeholder", value: inputOptions.placeholder }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,22 +16,10 @@ export interface InitializeResponse {
|
|||||||
dbThroughput: number;
|
dbThroughput: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMaxCollectionThroughput = async (): Promise<number> => {
|
export const getMaxThroughput = async (): Promise<number> => {
|
||||||
return 10000;
|
return 10000;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMinCollectionThroughput = async (): Promise<number> => {
|
|
||||||
return 400;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMaxDatabaseThroughput = async (): Promise<number> => {
|
|
||||||
return 10000;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMinDatabaseThroughput = async (): Promise<number> => {
|
|
||||||
return 400;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const update = async (
|
export const update = async (
|
||||||
regions: Regions,
|
regions: Regions,
|
||||||
enableLogging: boolean,
|
enableLogging: boolean,
|
||||||
@@ -71,6 +59,6 @@ export const onRefreshSelfServeExample = async (): Promise<RefreshResult> => {
|
|||||||
const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded";
|
const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded";
|
||||||
return {
|
return {
|
||||||
isUpdateInProgress: isUpdateInProgress,
|
isUpdateInProgress: isUpdateInProgress,
|
||||||
notificationMessage: "RefreshMessage",
|
notificationMessage: "Self Serve Example successfully refreshing",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,16 +10,7 @@ import {
|
|||||||
SelfServeNotificationType,
|
SelfServeNotificationType,
|
||||||
SmartUiInput,
|
SmartUiInput,
|
||||||
} from "../SelfServeTypes";
|
} from "../SelfServeTypes";
|
||||||
import {
|
import { onRefreshSelfServeExample, getMaxThroughput, Regions, update, initialize } from "./SelfServeExample.rp";
|
||||||
onRefreshSelfServeExample,
|
|
||||||
Regions,
|
|
||||||
update,
|
|
||||||
initialize,
|
|
||||||
getMinDatabaseThroughput,
|
|
||||||
getMaxDatabaseThroughput,
|
|
||||||
getMinCollectionThroughput,
|
|
||||||
getMaxCollectionThroughput,
|
|
||||||
} from "./SelfServeExample.rp";
|
|
||||||
|
|
||||||
const regionDropdownItems: ChoiceItem[] = [
|
const regionDropdownItems: ChoiceItem[] = [
|
||||||
{ label: "North Central US", key: Regions.NorthCentralUS },
|
{ label: "North Central US", key: Regions.NorthCentralUS },
|
||||||
@@ -28,11 +19,11 @@ const regionDropdownItems: ChoiceItem[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const selfServeExampleInfo: Info = {
|
const selfServeExampleInfo: Info = {
|
||||||
messageTKey: "ClassInfo",
|
message: "This is a self serve class",
|
||||||
};
|
};
|
||||||
|
|
||||||
const regionDropdownInfo: Info = {
|
const regionDropdownInfo: Info = {
|
||||||
messageTKey: "RegionDropdownInfo",
|
message: "More regions can be added in the future.",
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: InputType): Map<string, SmartUiInput> => {
|
const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: InputType): Map<string, SmartUiInput> => {
|
||||||
@@ -59,7 +50,7 @@ const onEnableDbLevelThroughputChange = (
|
|||||||
|
|
||||||
const validate = (currentvalues: Map<string, SmartUiInput>): void => {
|
const validate = (currentvalues: Map<string, SmartUiInput>): void => {
|
||||||
if (!currentvalues.get("regions").value || !currentvalues.get("accountName").value) {
|
if (!currentvalues.get("regions").value || !currentvalues.get("accountName").value) {
|
||||||
throw new Error("ValidationError");
|
throw new Error("Regions and AccountName should not be empty.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,9 +66,6 @@ const validate = (currentvalues: Map<string, SmartUiInput>): void => {
|
|||||||
|
|
||||||
You can test this self serve UI by using the featureflag '?feature.selfServeType=example'
|
You can test this self serve UI by using the featureflag '?feature.selfServeType=example'
|
||||||
and plumb in similar feature flags for your own self serve class.
|
and plumb in similar feature flags for your own self serve class.
|
||||||
|
|
||||||
All string to be used should be present in the "src/Localization" folder, in the language specific json files. The
|
|
||||||
corresponding key should be given as the value for the fields like "label", the error message etc.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -129,7 +117,7 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
|||||||
let dbThroughput = currentValues.get("dbThroughput")?.value as number;
|
let dbThroughput = currentValues.get("dbThroughput")?.value as number;
|
||||||
dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined;
|
dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined;
|
||||||
await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput);
|
await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput);
|
||||||
return { message: "SubmissionMessage", type: SelfServeNotificationType.info };
|
return { message: "submitted successfully", type: SelfServeNotificationType.info };
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -173,10 +161,10 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
|||||||
*/
|
*/
|
||||||
@Values({
|
@Values({
|
||||||
description: {
|
description: {
|
||||||
textTKey: "DescriptionText",
|
text: "This class sets collection and database throughput.",
|
||||||
link: {
|
link: {
|
||||||
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
||||||
textTKey: "DecriptionLinkText",
|
text: "Click here for more information",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -205,26 +193,26 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
|||||||
any other value of "regions"
|
any other value of "regions"
|
||||||
*/
|
*/
|
||||||
@OnChange(onRegionsChange)
|
@OnChange(onRegionsChange)
|
||||||
@Values({ labelTKey: "Regions", choices: regionDropdownItems, placeholderTKey: "RegionsPlaceholder" })
|
@Values({ label: "Regions", choices: regionDropdownItems, placeholder: "Select a region" })
|
||||||
regions: ChoiceItem;
|
regions: ChoiceItem;
|
||||||
|
|
||||||
@Values({
|
@Values({
|
||||||
labelTKey: "Enable Logging",
|
label: "Enable Logging",
|
||||||
trueLabelTKey: "Enable",
|
trueLabel: "Enable",
|
||||||
falseLabelTKey: "Disable",
|
falseLabel: "Disable",
|
||||||
})
|
})
|
||||||
enableLogging: boolean;
|
enableLogging: boolean;
|
||||||
|
|
||||||
@Values({
|
@Values({
|
||||||
labelTKey: "Account Name",
|
label: "Account Name",
|
||||||
placeholderTKey: "AccountNamePlaceHolder",
|
placeholder: "Enter the account name",
|
||||||
})
|
})
|
||||||
accountName: string;
|
accountName: string;
|
||||||
|
|
||||||
@Values({
|
@Values({
|
||||||
labelTKey: "Collection Throughput",
|
label: "Collection Throughput",
|
||||||
min: getMinCollectionThroughput,
|
min: 400,
|
||||||
max: getMaxCollectionThroughput,
|
max: getMaxThroughput,
|
||||||
step: 100,
|
step: 100,
|
||||||
uiType: NumberUiType.Spinner,
|
uiType: NumberUiType.Spinner,
|
||||||
})
|
})
|
||||||
@@ -236,16 +224,16 @@ export default class SelfServeExample extends SelfServeBaseClass {
|
|||||||
*/
|
*/
|
||||||
@OnChange(onEnableDbLevelThroughputChange)
|
@OnChange(onEnableDbLevelThroughputChange)
|
||||||
@Values({
|
@Values({
|
||||||
labelTKey: "Enable DB level throughput",
|
label: "Enable DB level throughput",
|
||||||
trueLabelTKey: "Enable",
|
trueLabel: "Enable",
|
||||||
falseLabelTKey: "Disable",
|
falseLabel: "Disable",
|
||||||
})
|
})
|
||||||
enableDbLevelThroughput: boolean;
|
enableDbLevelThroughput: boolean;
|
||||||
|
|
||||||
@Values({
|
@Values({
|
||||||
labelTKey: "Database Throughput",
|
label: "Database Throughput",
|
||||||
min: getMinDatabaseThroughput,
|
min: 400,
|
||||||
max: getMaxDatabaseThroughput,
|
max: getMaxThroughput,
|
||||||
step: 100,
|
step: 100,
|
||||||
uiType: NumberUiType.Slider,
|
uiType: NumberUiType.Slider,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,17 +34,17 @@ describe("SelfServeComponent", () => {
|
|||||||
root: {
|
root: {
|
||||||
id: "root",
|
id: "root",
|
||||||
info: {
|
info: {
|
||||||
messageTKey: "Start at $24/mo per database",
|
message: "Start at $24/mo per database",
|
||||||
link: {
|
link: {
|
||||||
href: "https://aka.ms/azure-cosmos-db-pricing",
|
href: "https://aka.ms/azure-cosmos-db-pricing",
|
||||||
textTKey: "More Details",
|
text: "More Details",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "throughput",
|
id: "throughput",
|
||||||
input: {
|
input: {
|
||||||
labelTKey: "Throughput (input)",
|
label: "Throughput (input)",
|
||||||
dataFieldName: "throughput",
|
dataFieldName: "throughput",
|
||||||
type: "number",
|
type: "number",
|
||||||
min: 400,
|
min: 400,
|
||||||
@@ -57,7 +57,7 @@ describe("SelfServeComponent", () => {
|
|||||||
{
|
{
|
||||||
id: "containerId",
|
id: "containerId",
|
||||||
input: {
|
input: {
|
||||||
labelTKey: "Container id",
|
label: "Container id",
|
||||||
dataFieldName: "containerId",
|
dataFieldName: "containerId",
|
||||||
type: "string",
|
type: "string",
|
||||||
},
|
},
|
||||||
@@ -65,9 +65,9 @@ describe("SelfServeComponent", () => {
|
|||||||
{
|
{
|
||||||
id: "analyticalStore",
|
id: "analyticalStore",
|
||||||
input: {
|
input: {
|
||||||
labelTKey: "Analytical Store",
|
label: "Analytical Store",
|
||||||
trueLabelTKey: "Enabled",
|
trueLabel: "Enabled",
|
||||||
falseLabelTKey: "Disabled",
|
falseLabel: "Disabled",
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
dataFieldName: "analyticalStore",
|
dataFieldName: "analyticalStore",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
@@ -76,7 +76,7 @@ describe("SelfServeComponent", () => {
|
|||||||
{
|
{
|
||||||
id: "database",
|
id: "database",
|
||||||
input: {
|
input: {
|
||||||
labelTKey: "Database",
|
label: "Database",
|
||||||
dataFieldName: "database",
|
dataFieldName: "database",
|
||||||
type: "object",
|
type: "object",
|
||||||
choices: [
|
choices: [
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ import {
|
|||||||
} from "./SelfServeTypes";
|
} from "./SelfServeTypes";
|
||||||
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
||||||
import { getMessageBarType } from "./SelfServeUtils";
|
import { getMessageBarType } from "./SelfServeUtils";
|
||||||
import { Translation } from "react-i18next";
|
|
||||||
import { TFunction } from "i18next";
|
|
||||||
import "../i18n";
|
|
||||||
|
|
||||||
export interface SelfServeComponentProps {
|
export interface SelfServeComponentProps {
|
||||||
descriptor: SelfServeDescriptor;
|
descriptor: SelfServeDescriptor;
|
||||||
@@ -46,8 +43,6 @@ export interface SelfServeComponentState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
|
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
|
||||||
private smartUiGeneratorClassName: string;
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
this.performRefresh();
|
this.performRefresh();
|
||||||
this.initializeSmartUiComponent();
|
this.initializeSmartUiComponent();
|
||||||
@@ -65,7 +60,6 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
notification: undefined,
|
notification: undefined,
|
||||||
refreshResult: undefined,
|
refreshResult: undefined,
|
||||||
};
|
};
|
||||||
this.smartUiGeneratorClassName = this.props.descriptor.root.id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onError = (hasErrors: boolean): void => {
|
private onError = (hasErrors: boolean): void => {
|
||||||
@@ -153,8 +147,8 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
currentValues: Map<string, SmartUiInput>,
|
currentValues: Map<string, SmartUiInput>,
|
||||||
baselineValues: Map<string, SmartUiInput>
|
baselineValues: Map<string, SmartUiInput>
|
||||||
): Promise<AnyDisplay> => {
|
): Promise<AnyDisplay> => {
|
||||||
input.labelTKey = await this.getResolvedValue(input.labelTKey);
|
input.label = await this.getResolvedValue(input.label);
|
||||||
input.placeholderTKey = await this.getResolvedValue(input.placeholderTKey);
|
input.placeholder = await this.getResolvedValue(input.placeholder);
|
||||||
|
|
||||||
switch (input.type) {
|
switch (input.type) {
|
||||||
case "string": {
|
case "string": {
|
||||||
@@ -183,8 +177,8 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
}
|
}
|
||||||
case "boolean": {
|
case "boolean": {
|
||||||
const booleanInput = input as BooleanInput;
|
const booleanInput = input as BooleanInput;
|
||||||
booleanInput.trueLabelTKey = await this.getResolvedValue(booleanInput.trueLabelTKey);
|
booleanInput.trueLabel = await this.getResolvedValue(booleanInput.trueLabel);
|
||||||
booleanInput.falseLabelTKey = await this.getResolvedValue(booleanInput.falseLabelTKey);
|
booleanInput.falseLabel = await this.getResolvedValue(booleanInput.falseLabel);
|
||||||
return booleanInput;
|
return booleanInput;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@@ -220,7 +214,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
onSavePromise.catch((error) => {
|
onSavePromise.catch((error) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
notification: {
|
notification: {
|
||||||
message: `${error.message}`,
|
message: `Error: ${error.message}`,
|
||||||
type: SelfServeNotificationType.error,
|
type: SelfServeNotificationType.error,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -279,15 +273,11 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
this.setState({ isInitializing: false });
|
this.setState({ isInitializing: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
public getCommonTranslation = (translationFunction: TFunction, key: string): string => {
|
private getCommandBarItems = (): ICommandBarItemProps[] => {
|
||||||
return translationFunction(`Common.${key}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
private getCommandBarItems = (translate: TFunction): ICommandBarItemProps[] => {
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: "save",
|
key: "save",
|
||||||
text: this.getCommonTranslation(translate, "Save"),
|
text: "Save",
|
||||||
iconProps: { iconName: "Save" },
|
iconProps: { iconName: "Save" },
|
||||||
split: true,
|
split: true,
|
||||||
disabled: this.isSaveButtonDisabled(),
|
disabled: this.isSaveButtonDisabled(),
|
||||||
@@ -295,7 +285,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "discard",
|
key: "discard",
|
||||||
text: this.getCommonTranslation(translate, "Discard"),
|
text: "Discard",
|
||||||
iconProps: { iconName: "Undo" },
|
iconProps: { iconName: "Undo" },
|
||||||
split: true,
|
split: true,
|
||||||
disabled: this.isDiscardButtonDisabled(),
|
disabled: this.isDiscardButtonDisabled(),
|
||||||
@@ -305,7 +295,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "refresh",
|
key: "refresh",
|
||||||
text: this.getCommonTranslation(translate, "Refresh"),
|
text: "Refresh",
|
||||||
disabled: this.state.isInitializing,
|
disabled: this.state.isInitializing,
|
||||||
iconProps: { iconName: "Refresh" },
|
iconProps: { iconName: "Refresh" },
|
||||||
split: true,
|
split: true,
|
||||||
@@ -316,66 +306,47 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
private getNotificationMessageTranslation = (translationFunction: TFunction, messageKey: string): string => {
|
|
||||||
const translation = translationFunction(messageKey);
|
|
||||||
if (translation === `${this.smartUiGeneratorClassName}.${messageKey}`) {
|
|
||||||
return messageKey;
|
|
||||||
}
|
|
||||||
return translation;
|
|
||||||
};
|
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
const containerStackTokens: IStackTokens = { childrenGap: 5 };
|
const containerStackTokens: IStackTokens = { childrenGap: 5 };
|
||||||
if (this.state.compileErrorMessage) {
|
if (this.state.compileErrorMessage) {
|
||||||
return <MessageBar messageBarType={MessageBarType.error}>{this.state.compileErrorMessage}</MessageBar>;
|
return <MessageBar messageBarType={MessageBarType.error}>{this.state.compileErrorMessage}</MessageBar>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Translation>
|
<div style={{ overflowX: "auto" }}>
|
||||||
{(translate) => {
|
<Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}>
|
||||||
const getTranslation = (key: string): string => {
|
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems()} />
|
||||||
return translate(`${this.smartUiGeneratorClassName}.${key}`);
|
{this.state.isInitializing ? (
|
||||||
};
|
<Spinner
|
||||||
|
size={SpinnerSize.large}
|
||||||
return (
|
styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }}
|
||||||
<div style={{ overflowX: "auto" }}>
|
/>
|
||||||
<Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}>
|
) : (
|
||||||
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems(translate)} />
|
<>
|
||||||
{this.state.isInitializing ? (
|
{this.state.refreshResult?.isUpdateInProgress && (
|
||||||
<Spinner
|
<MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}>
|
||||||
size={SpinnerSize.large}
|
{this.state.refreshResult.notificationMessage}
|
||||||
styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }}
|
</MessageBar>
|
||||||
/>
|
)}
|
||||||
) : (
|
{this.state.notification && (
|
||||||
<>
|
<MessageBar
|
||||||
{this.state.refreshResult?.isUpdateInProgress && (
|
messageBarType={getMessageBarType(this.state.notification.type)}
|
||||||
<MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}>
|
styles={{ root: { width: 400 } }}
|
||||||
{getTranslation(this.state.refreshResult.notificationMessage)}
|
onDismiss={() => this.setState({ notification: undefined })}
|
||||||
</MessageBar>
|
>
|
||||||
)}
|
{this.state.notification.message}
|
||||||
{this.state.notification && (
|
</MessageBar>
|
||||||
<MessageBar
|
)}
|
||||||
messageBarType={getMessageBarType(this.state.notification.type)}
|
<SmartUiComponent
|
||||||
styles={{ root: { width: 400 } }}
|
disabled={this.state.refreshResult?.isUpdateInProgress}
|
||||||
onDismiss={() => this.setState({ notification: undefined })}
|
descriptor={this.state.root as SmartUiDescriptor}
|
||||||
>
|
currentValues={this.state.currentValues}
|
||||||
{this.getNotificationMessageTranslation(getTranslation, this.state.notification.message)}
|
onInputChange={this.onInputChange}
|
||||||
</MessageBar>
|
onError={this.onError}
|
||||||
)}
|
/>
|
||||||
<SmartUiComponent
|
</>
|
||||||
disabled={this.state.refreshResult?.isUpdateInProgress}
|
)}
|
||||||
descriptor={this.state.root as SmartUiDescriptor}
|
</Stack>
|
||||||
currentValues={this.state.currentValues}
|
</div>
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onError={this.onError}
|
|
||||||
getTranslation={getTranslation}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Translation>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ interface BaseInput {
|
|||||||
dataFieldName: string;
|
dataFieldName: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
type: InputTypeValue;
|
type: InputTypeValue;
|
||||||
labelTKey?: (() => Promise<string>) | string;
|
label?: (() => Promise<string>) | string;
|
||||||
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>;
|
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>;
|
||||||
placeholderTKey?: (() => Promise<string>) | string;
|
placeholder?: (() => Promise<string>) | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NumberInput extends BaseInput {
|
export interface NumberInput extends BaseInput {
|
||||||
@@ -16,8 +16,8 @@ export interface NumberInput extends BaseInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BooleanInput extends BaseInput {
|
export interface BooleanInput extends BaseInput {
|
||||||
trueLabelTKey: (() => Promise<string>) | string;
|
trueLabel: (() => Promise<string>) | string;
|
||||||
falseLabelTKey: (() => Promise<string>) | string;
|
falseLabel: (() => Promise<string>) | string;
|
||||||
defaultValue?: boolean;
|
defaultValue?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,18 +92,18 @@ export type ChoiceItem = { label: string; key: string };
|
|||||||
export type InputType = number | string | boolean | ChoiceItem;
|
export type InputType = number | string | boolean | ChoiceItem;
|
||||||
|
|
||||||
export interface Info {
|
export interface Info {
|
||||||
messageTKey: string;
|
message: string;
|
||||||
link?: {
|
link?: {
|
||||||
href: string;
|
href: string;
|
||||||
textTKey: string;
|
text: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Description {
|
export interface Description {
|
||||||
textTKey: string;
|
text: string;
|
||||||
link?: {
|
link?: {
|
||||||
href: string;
|
href: string;
|
||||||
textTKey: string;
|
text: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ describe("SelfServeUtils", () => {
|
|||||||
id: "dbThroughput",
|
id: "dbThroughput",
|
||||||
dataFieldName: "dbThroughput",
|
dataFieldName: "dbThroughput",
|
||||||
type: "number",
|
type: "number",
|
||||||
labelTKey: "Database Throughput",
|
label: "Database Throughput",
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 5,
|
max: 5,
|
||||||
step: 1,
|
step: 1,
|
||||||
@@ -71,7 +71,7 @@ describe("SelfServeUtils", () => {
|
|||||||
id: "collThroughput",
|
id: "collThroughput",
|
||||||
dataFieldName: "collThroughput",
|
dataFieldName: "collThroughput",
|
||||||
type: "number",
|
type: "number",
|
||||||
labelTKey: "Coll Throughput",
|
label: "Coll Throughput",
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 5,
|
max: 5,
|
||||||
step: 1,
|
step: 1,
|
||||||
@@ -84,7 +84,7 @@ describe("SelfServeUtils", () => {
|
|||||||
id: "invalidThroughput",
|
id: "invalidThroughput",
|
||||||
dataFieldName: "invalidThroughput",
|
dataFieldName: "invalidThroughput",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
labelTKey: "Invalid Coll Throughput",
|
label: "Invalid Coll Throughput",
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 5,
|
max: 5,
|
||||||
step: 1,
|
step: 1,
|
||||||
@@ -98,8 +98,8 @@ describe("SelfServeUtils", () => {
|
|||||||
id: "collName",
|
id: "collName",
|
||||||
dataFieldName: "collName",
|
dataFieldName: "collName",
|
||||||
type: "string",
|
type: "string",
|
||||||
labelTKey: "Coll Name",
|
label: "Coll Name",
|
||||||
placeholderTKey: "placeholder text",
|
placeholder: "placeholder text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -108,9 +108,9 @@ describe("SelfServeUtils", () => {
|
|||||||
id: "enableLogging",
|
id: "enableLogging",
|
||||||
dataFieldName: "enableLogging",
|
dataFieldName: "enableLogging",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
labelTKey: "Enable Logging",
|
label: "Enable Logging",
|
||||||
trueLabelTKey: "Enable",
|
trueLabel: "Enable",
|
||||||
falseLabelTKey: "Disable",
|
falseLabel: "Disable",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -119,8 +119,8 @@ describe("SelfServeUtils", () => {
|
|||||||
id: "invalidEnableLogging",
|
id: "invalidEnableLogging",
|
||||||
dataFieldName: "invalidEnableLogging",
|
dataFieldName: "invalidEnableLogging",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
labelTKey: "Invalid Enable Logging",
|
label: "Invalid Enable Logging",
|
||||||
placeholderTKey: "placeholder text",
|
placeholder: "placeholder text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@@ -129,7 +129,7 @@ describe("SelfServeUtils", () => {
|
|||||||
id: "regions",
|
id: "regions",
|
||||||
dataFieldName: "regions",
|
dataFieldName: "regions",
|
||||||
type: "object",
|
type: "object",
|
||||||
labelTKey: "Regions",
|
label: "Regions",
|
||||||
choices: [
|
choices: [
|
||||||
{ label: "South West US", key: "SWUS" },
|
{ label: "South West US", key: "SWUS" },
|
||||||
{ label: "North Central US", key: "NCUS" },
|
{ label: "North Central US", key: "NCUS" },
|
||||||
@@ -143,14 +143,14 @@ describe("SelfServeUtils", () => {
|
|||||||
id: "invalidRegions",
|
id: "invalidRegions",
|
||||||
dataFieldName: "invalidRegions",
|
dataFieldName: "invalidRegions",
|
||||||
type: "object",
|
type: "object",
|
||||||
labelTKey: "Invalid Regions",
|
label: "Invalid Regions",
|
||||||
placeholderTKey: "placeholder text",
|
placeholder: "placeholder text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
const expectedDescriptor = {
|
const expectedDescriptor = {
|
||||||
root: {
|
root: {
|
||||||
id: "TestClass",
|
id: "root",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "dbThroughput",
|
id: "dbThroughput",
|
||||||
@@ -158,7 +158,7 @@ describe("SelfServeUtils", () => {
|
|||||||
id: "dbThroughput",
|
id: "dbThroughput",
|
||||||
dataFieldName: "dbThroughput",
|
dataFieldName: "dbThroughput",
|
||||||
type: "number",
|
type: "number",
|
||||||
labelTKey: "Database Throughput",
|
label: "Database Throughput",
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 5,
|
max: 5,
|
||||||
step: 1,
|
step: 1,
|
||||||
@@ -172,7 +172,7 @@ describe("SelfServeUtils", () => {
|
|||||||
id: "collThroughput",
|
id: "collThroughput",
|
||||||
dataFieldName: "collThroughput",
|
dataFieldName: "collThroughput",
|
||||||
type: "number",
|
type: "number",
|
||||||
labelTKey: "Coll Throughput",
|
label: "Coll Throughput",
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 5,
|
max: 5,
|
||||||
step: 1,
|
step: 1,
|
||||||
@@ -186,7 +186,7 @@ describe("SelfServeUtils", () => {
|
|||||||
id: "invalidThroughput",
|
id: "invalidThroughput",
|
||||||
dataFieldName: "invalidThroughput",
|
dataFieldName: "invalidThroughput",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
labelTKey: "Invalid Coll Throughput",
|
label: "Invalid Coll Throughput",
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 5,
|
max: 5,
|
||||||
step: 1,
|
step: 1,
|
||||||
@@ -201,8 +201,8 @@ describe("SelfServeUtils", () => {
|
|||||||
id: "collName",
|
id: "collName",
|
||||||
dataFieldName: "collName",
|
dataFieldName: "collName",
|
||||||
type: "string",
|
type: "string",
|
||||||
labelTKey: "Coll Name",
|
label: "Coll Name",
|
||||||
placeholderTKey: "placeholder text",
|
placeholder: "placeholder text",
|
||||||
},
|
},
|
||||||
children: [] as Node[],
|
children: [] as Node[],
|
||||||
},
|
},
|
||||||
@@ -212,9 +212,9 @@ describe("SelfServeUtils", () => {
|
|||||||
id: "enableLogging",
|
id: "enableLogging",
|
||||||
dataFieldName: "enableLogging",
|
dataFieldName: "enableLogging",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
labelTKey: "Enable Logging",
|
label: "Enable Logging",
|
||||||
trueLabelTKey: "Enable",
|
trueLabel: "Enable",
|
||||||
falseLabelTKey: "Disable",
|
falseLabel: "Disable",
|
||||||
},
|
},
|
||||||
children: [] as Node[],
|
children: [] as Node[],
|
||||||
},
|
},
|
||||||
@@ -224,8 +224,8 @@ describe("SelfServeUtils", () => {
|
|||||||
id: "invalidEnableLogging",
|
id: "invalidEnableLogging",
|
||||||
dataFieldName: "invalidEnableLogging",
|
dataFieldName: "invalidEnableLogging",
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
labelTKey: "Invalid Enable Logging",
|
label: "Invalid Enable Logging",
|
||||||
placeholderTKey: "placeholder text",
|
placeholder: "placeholder text",
|
||||||
errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidEnableLogging'.",
|
errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidEnableLogging'.",
|
||||||
},
|
},
|
||||||
children: [] as Node[],
|
children: [] as Node[],
|
||||||
@@ -236,7 +236,7 @@ describe("SelfServeUtils", () => {
|
|||||||
id: "regions",
|
id: "regions",
|
||||||
dataFieldName: "regions",
|
dataFieldName: "regions",
|
||||||
type: "object",
|
type: "object",
|
||||||
labelTKey: "Regions",
|
label: "Regions",
|
||||||
choices: [
|
choices: [
|
||||||
{ label: "South West US", key: "SWUS" },
|
{ label: "South West US", key: "SWUS" },
|
||||||
{ label: "North Central US", key: "NCUS" },
|
{ label: "North Central US", key: "NCUS" },
|
||||||
@@ -251,8 +251,8 @@ describe("SelfServeUtils", () => {
|
|||||||
id: "invalidRegions",
|
id: "invalidRegions",
|
||||||
dataFieldName: "invalidRegions",
|
dataFieldName: "invalidRegions",
|
||||||
type: "object",
|
type: "object",
|
||||||
labelTKey: "Invalid Regions",
|
label: "Invalid Regions",
|
||||||
placeholderTKey: "placeholder text",
|
placeholder: "placeholder text",
|
||||||
errorMessage: "label and choices are required for Choice input 'invalidRegions'.",
|
errorMessage: "label and choices are required for Choice input 'invalidRegions'.",
|
||||||
},
|
},
|
||||||
children: [] as Node[],
|
children: [] as Node[],
|
||||||
@@ -270,7 +270,7 @@ describe("SelfServeUtils", () => {
|
|||||||
"invalidRegions",
|
"invalidRegions",
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const descriptor = mapToSmartUiDescriptor("TestClass", context);
|
const descriptor = mapToSmartUiDescriptor(context);
|
||||||
expect(descriptor).toEqual(expectedDescriptor);
|
expect(descriptor).toEqual(expectedDescriptor);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ export interface DecoratorProperties {
|
|||||||
id: string;
|
id: string;
|
||||||
info?: (() => Promise<Info>) | Info;
|
info?: (() => Promise<Info>) | Info;
|
||||||
type?: InputTypeValue;
|
type?: InputTypeValue;
|
||||||
labelTKey?: (() => Promise<string>) | string;
|
label?: (() => Promise<string>) | string;
|
||||||
placeholderTKey?: (() => Promise<string>) | string;
|
placeholder?: (() => Promise<string>) | string;
|
||||||
dataFieldName?: string;
|
dataFieldName?: string;
|
||||||
min?: (() => Promise<number>) | number;
|
min?: (() => Promise<number>) | number;
|
||||||
max?: (() => Promise<number>) | number;
|
max?: (() => Promise<number>) | number;
|
||||||
step?: (() => Promise<number>) | number;
|
step?: (() => Promise<number>) | number;
|
||||||
trueLabelTKey?: (() => Promise<string>) | string;
|
trueLabel?: (() => Promise<string>) | string;
|
||||||
falseLabelTKey?: (() => Promise<string>) | string;
|
falseLabel?: (() => Promise<string>) | string;
|
||||||
choices?: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
|
choices?: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
|
||||||
uiType?: string;
|
uiType?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
@@ -100,21 +100,18 @@ export const updateContextWithDecorator = <T extends keyof DecoratorProperties,
|
|||||||
|
|
||||||
export const buildSmartUiDescriptor = (className: string, target: unknown): void => {
|
export const buildSmartUiDescriptor = (className: string, target: unknown): void => {
|
||||||
const context = Reflect.getMetadata(className, target) as Map<string, DecoratorProperties>;
|
const context = Reflect.getMetadata(className, target) as Map<string, DecoratorProperties>;
|
||||||
const smartUiDescriptor = mapToSmartUiDescriptor(className, context);
|
const smartUiDescriptor = mapToSmartUiDescriptor(context);
|
||||||
Reflect.defineMetadata(className, smartUiDescriptor, target);
|
Reflect.defineMetadata(className, smartUiDescriptor, target);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapToSmartUiDescriptor = (
|
export const mapToSmartUiDescriptor = (context: Map<string, DecoratorProperties>): SelfServeDescriptor => {
|
||||||
className: string,
|
|
||||||
context: Map<string, DecoratorProperties>
|
|
||||||
): SelfServeDescriptor => {
|
|
||||||
const root = context.get("root");
|
const root = context.get("root");
|
||||||
context.delete("root");
|
context.delete("root");
|
||||||
const inputNames: string[] = [];
|
const inputNames: string[] = [];
|
||||||
|
|
||||||
const smartUiDescriptor: SelfServeDescriptor = {
|
const smartUiDescriptor: SelfServeDescriptor = {
|
||||||
root: {
|
root: {
|
||||||
id: className,
|
id: "root",
|
||||||
info: root?.info,
|
info: root?.info,
|
||||||
children: [],
|
children: [],
|
||||||
},
|
},
|
||||||
@@ -150,7 +147,7 @@ const addToDescriptor = (
|
|||||||
const getInput = (value: DecoratorProperties): AnyDisplay => {
|
const getInput = (value: DecoratorProperties): AnyDisplay => {
|
||||||
switch (value.type) {
|
switch (value.type) {
|
||||||
case "number":
|
case "number":
|
||||||
if (!value.labelTKey || !value.step || !value.uiType || !value.min || !value.max) {
|
if (!value.label || !value.step || !value.uiType || !value.min || !value.max) {
|
||||||
value.errorMessage = `label, step, min, max and uiType are required for number input '${value.id}'.`;
|
value.errorMessage = `label, step, min, max and uiType are required for number input '${value.id}'.`;
|
||||||
}
|
}
|
||||||
return value as NumberInput;
|
return value as NumberInput;
|
||||||
@@ -158,17 +155,17 @@ const getInput = (value: DecoratorProperties): AnyDisplay => {
|
|||||||
if (value.description) {
|
if (value.description) {
|
||||||
return value as DescriptionDisplay;
|
return value as DescriptionDisplay;
|
||||||
}
|
}
|
||||||
if (!value.labelTKey) {
|
if (!value.label) {
|
||||||
value.errorMessage = `label is required for string input '${value.id}'.`;
|
value.errorMessage = `label is required for string input '${value.id}'.`;
|
||||||
}
|
}
|
||||||
return value as StringInput;
|
return value as StringInput;
|
||||||
case "boolean":
|
case "boolean":
|
||||||
if (!value.labelTKey || !value.trueLabelTKey || !value.falseLabelTKey) {
|
if (!value.label || !value.trueLabel || !value.falseLabel) {
|
||||||
value.errorMessage = `label, truelabel and falselabel are required for boolean input '${value.id}'.`;
|
value.errorMessage = `label, truelabel and falselabel are required for boolean input '${value.id}'.`;
|
||||||
}
|
}
|
||||||
return value as BooleanInput;
|
return value as BooleanInput;
|
||||||
default:
|
default:
|
||||||
if (!value.labelTKey || !value.choices) {
|
if (!value.label || !value.choices) {
|
||||||
value.errorMessage = `label and choices are required for Choice input '${value.id}'.`;
|
value.errorMessage = `label and choices are required for Choice input '${value.id}'.`;
|
||||||
}
|
}
|
||||||
return value as ChoiceInput;
|
return value as ChoiceInput;
|
||||||
|
|||||||
@@ -1,78 +1,33 @@
|
|||||||
import { RefreshResult } from "../SelfServeTypes";
|
import { RefreshResult } from "../SelfServeTypes";
|
||||||
import { userContext } from "../../UserContext";
|
|
||||||
import { armRequest } from "../../Utils/arm/request";
|
|
||||||
import { configContext } from "../../ConfigContext";
|
|
||||||
import { SqlxServiceResource, UpdateDedicatedGatewayRequestParameters } from "./SqlxTypes";
|
|
||||||
|
|
||||||
const apiVersion = "2020-06-01-preview";
|
|
||||||
|
|
||||||
export enum ResourceStatus {
|
|
||||||
Running,
|
|
||||||
Creating,
|
|
||||||
Updating,
|
|
||||||
Deleting,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DedicatedGatewayResponse {
|
export interface DedicatedGatewayResponse {
|
||||||
sku: string;
|
sku: string;
|
||||||
instances: number;
|
instances: number;
|
||||||
status: string;
|
|
||||||
endpoint: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPath = (subscriptionId: string, resourceGroup: string, name: string): string => {
|
export const getRegionSpecificMinInstances = async (): Promise<number> => {
|
||||||
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/sqlx`;
|
// TODO: write RP call to get min number of instances needed for this region
|
||||||
|
throw new Error("getRegionSpecificMinInstances not implemented");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateDedicatedGatewayResource = async (sku: string, instances: number): Promise<void> => {
|
export const getRegionSpecificMaxInstances = async (): Promise<number> => {
|
||||||
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
|
// TODO: write RP call to get max number of instances needed for this region
|
||||||
const body: UpdateDedicatedGatewayRequestParameters = {
|
throw new Error("getRegionSpecificMaxInstances not implemented");
|
||||||
properties: {
|
|
||||||
instanceSize: sku,
|
|
||||||
instanceCount: instances,
|
|
||||||
serviceType: "Sqlx",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteDedicatedGatewayResource = async (): Promise<void> => {
|
export const updateDedicatedGatewayProvisioning = async (sku: string, instances: number): Promise<void> => {
|
||||||
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
|
// TODO: write RP call to update dedicated gateway provisioning
|
||||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "DELETE", apiVersion });
|
throw new Error(
|
||||||
|
`updateDedicatedGatewayProvisioning not implemented. Parameters- sku: ${sku}, instances:${instances}`
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDedicatedGatewayResource = async (): Promise<SqlxServiceResource> => {
|
export const initializeDedicatedGatewayProvisioning = async (): Promise<DedicatedGatewayResponse> => {
|
||||||
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
|
// TODO: write RP call to initialize UI for dedicated gateway provisioning
|
||||||
return armRequest<SqlxServiceResource>({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion });
|
throw new Error("initializeDedicatedGatewayProvisioning not implemented");
|
||||||
};
|
|
||||||
|
|
||||||
export const getCurrentProvisioningState = async (): Promise<DedicatedGatewayResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await getDedicatedGatewayResource();
|
|
||||||
return {
|
|
||||||
sku: response.properties.instanceSize,
|
|
||||||
instances: response.properties.instanceCount,
|
|
||||||
status: response.properties.status,
|
|
||||||
endpoint: response.properties.sqlxEndPoint,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
return { sku: undefined, instances: undefined, status: undefined, endpoint: undefined };
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const refreshDedicatedGatewayProvisioning = async (): Promise<RefreshResult> => {
|
export const refreshDedicatedGatewayProvisioning = async (): Promise<RefreshResult> => {
|
||||||
try {
|
// TODO: write RP call to check if dedicated gateway update has gone through
|
||||||
const response = await getDedicatedGatewayResource();
|
throw new Error("refreshDedicatedGatewayProvisioning not implemented");
|
||||||
if (response.properties.status === ResourceStatus.Running.toString()) {
|
|
||||||
return { isUpdateInProgress: false, notificationMessage: undefined };
|
|
||||||
} else if (response.properties.status === ResourceStatus.Creating.toString()) {
|
|
||||||
return { isUpdateInProgress: true, notificationMessage: "CreateMessage" };
|
|
||||||
} else if (response.properties.status === ResourceStatus.Deleting.toString()) {
|
|
||||||
return { isUpdateInProgress: true, notificationMessage: "DeleteMessage" };
|
|
||||||
} else {
|
|
||||||
return { isUpdateInProgress: true, notificationMessage: "UpdateMessage" };
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return { isUpdateInProgress: false, notificationMessage: undefined };
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,18 +6,9 @@ import {
|
|||||||
RefreshResult,
|
RefreshResult,
|
||||||
SelfServeBaseClass,
|
SelfServeBaseClass,
|
||||||
SelfServeNotification,
|
SelfServeNotification,
|
||||||
SelfServeNotificationType,
|
|
||||||
SmartUiInput,
|
SmartUiInput,
|
||||||
} from "../SelfServeTypes";
|
} from "../SelfServeTypes";
|
||||||
import {
|
import { refreshDedicatedGatewayProvisioning } from "./SqlX.rp";
|
||||||
ResourceStatus,
|
|
||||||
refreshDedicatedGatewayProvisioning,
|
|
||||||
updateDedicatedGatewayResource,
|
|
||||||
deleteDedicatedGatewayResource,
|
|
||||||
getCurrentProvisioningState,
|
|
||||||
} from "./SqlX.rp";
|
|
||||||
|
|
||||||
let disableAttributesOnDedicatedGatewayChange = false;
|
|
||||||
|
|
||||||
const onEnableDedicatedGatewayChange = (
|
const onEnableDedicatedGatewayChange = (
|
||||||
currentState: Map<string, SmartUiInput>,
|
currentState: Map<string, SmartUiInput>,
|
||||||
@@ -25,38 +16,31 @@ const onEnableDedicatedGatewayChange = (
|
|||||||
): Map<string, SmartUiInput> => {
|
): Map<string, SmartUiInput> => {
|
||||||
const sku = currentState.get("sku");
|
const sku = currentState.get("sku");
|
||||||
const instances = currentState.get("instances");
|
const instances = currentState.get("instances");
|
||||||
const hideAttributes = newValue === undefined || !(newValue as boolean);
|
const isSkuHidden = newValue === undefined || !(newValue as boolean);
|
||||||
currentState.set("enableDedicatedGateway", { value: newValue });
|
currentState.set("enableDedicatedGateway", { value: newValue });
|
||||||
currentState.set("sku", {
|
currentState.set("sku", { value: sku.value, hidden: isSkuHidden });
|
||||||
value: sku.value,
|
currentState.set("instances", { value: instances.value, hidden: isSkuHidden });
|
||||||
hidden: hideAttributes,
|
|
||||||
disabled: disableAttributesOnDedicatedGatewayChange,
|
|
||||||
});
|
|
||||||
currentState.set("instances", {
|
|
||||||
value: instances.value,
|
|
||||||
hidden: hideAttributes,
|
|
||||||
disabled: disableAttributesOnDedicatedGatewayChange,
|
|
||||||
});
|
|
||||||
return currentState;
|
return currentState;
|
||||||
};
|
};
|
||||||
|
|
||||||
const skuDropDownItems: ChoiceItem[] = [
|
|
||||||
{ label: "CosmosD4s", key: "Cosmos.D4s" },
|
|
||||||
{ label: "CosmosD8s", key: "Cosmos.D8s" },
|
|
||||||
{ label: "CosmosD16s", key: "Cosmos.D16s" },
|
|
||||||
{ label: "CosmosD32s", key: "Cosmos.D32s" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const getSkus = async (): Promise<ChoiceItem[]> => {
|
const getSkus = async (): Promise<ChoiceItem[]> => {
|
||||||
return skuDropDownItems;
|
// TODO: get SKUs from getRegionSpecificSkus() RP call and return array of {label:..., key:...}.
|
||||||
|
throw new Error("getSkus not implemented.");
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInstancesMin = async (): Promise<number> => {
|
const getInstancesMin = async (): Promise<number> => {
|
||||||
return 1;
|
// TODO: get SKUs from getRegionSpecificSkus() RP call and return array of {label:..., key:...}.
|
||||||
|
throw new Error("getInstancesMin not implemented.");
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInstancesMax = async (): Promise<number> => {
|
const getInstancesMax = async (): Promise<number> => {
|
||||||
return 5;
|
// TODO: get SKUs from getRegionSpecificSkus() RP call and return array of {label:..., key:...}.
|
||||||
|
throw new Error("getInstancesMax not implemented.");
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = (currentValues: Map<string, SmartUiInput>): void => {
|
||||||
|
// TODO: add cusom validation logic to be called before Saving the data.
|
||||||
|
throw new Error(`validate not implemented. No. of properties to validate: ${currentValues.size}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@IsDisplayable()
|
@IsDisplayable()
|
||||||
@@ -66,80 +50,22 @@ export default class SqlX extends SelfServeBaseClass {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<SelfServeNotification> => {
|
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<SelfServeNotification> => {
|
||||||
const response = await getCurrentProvisioningState();
|
validate(currentValues);
|
||||||
|
// TODO: add pre processing logic before calling the updateDedicatedGatewayProvisioning() RP call.
|
||||||
// null implies the resource has not been provisioned.
|
throw new Error(`onSave not implemented. No. of properties to save: ${currentValues.size}`);
|
||||||
if (response.status !== undefined && response.status !== ResourceStatus.Running.toString()) {
|
|
||||||
switch (response.status) {
|
|
||||||
case ResourceStatus.Creating.toString():
|
|
||||||
return { message: "CreateMessage", type: SelfServeNotificationType.error };
|
|
||||||
case ResourceStatus.Updating.toString():
|
|
||||||
return { message: "UpdateMessage", type: SelfServeNotificationType.error };
|
|
||||||
case ResourceStatus.Deleting.toString():
|
|
||||||
return { message: "DeleteMessage", type: SelfServeNotificationType.error };
|
|
||||||
default:
|
|
||||||
return { message: "CannotSave", type: SelfServeNotificationType.error };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const enableDedicatedGateway = currentValues.get("enableDedicatedGateway")?.value as boolean;
|
|
||||||
|
|
||||||
if (response.status !== undefined) {
|
|
||||||
if (!enableDedicatedGateway) {
|
|
||||||
try {
|
|
||||||
await deleteDedicatedGatewayResource();
|
|
||||||
return { message: "DedicatedGateway resource will be deleted.", type: SelfServeNotificationType.info };
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
message: "Deleting Dedicated Gateway resource failed. DedicatedGateway will not be deleted.",
|
|
||||||
type: SelfServeNotificationType.error,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Check for scaling up/down/in/out
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (enableDedicatedGateway) {
|
|
||||||
const sku = currentValues.get("sku")?.value as string;
|
|
||||||
const instances = currentValues.get("instances").value as number;
|
|
||||||
try {
|
|
||||||
await updateDedicatedGatewayResource(sku, instances);
|
|
||||||
return { message: "Dedicated Gateway resource will be provisioned.", type: SelfServeNotificationType.info };
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
message: "Updating Dedicated Gateway resource failed. Dedicated Gateway will not be updated.",
|
|
||||||
type: SelfServeNotificationType.error,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { message: "No updates were applied at this time", type: SelfServeNotificationType.warning };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public initialize = async (): Promise<Map<string, SmartUiInput>> => {
|
public initialize = async (): Promise<Map<string, SmartUiInput>> => {
|
||||||
// Based on the RP call enableDedicatedGateway will be true if it has not yet been enabled and false if it has.
|
// TODO: get initialization data from initializeDedicatedGatewayProvisioning() RP call.
|
||||||
const defaults = new Map<string, SmartUiInput>();
|
throw new Error("onSave not implemented");
|
||||||
const enableDedicatedGateway = false;
|
|
||||||
defaults.set("enableDedicatedGateway", { value: enableDedicatedGateway, hidden: false, disabled: false });
|
|
||||||
defaults.set("sku", { value: "Cosmos.D4s", hidden: !enableDedicatedGateway, disabled: false });
|
|
||||||
defaults.set("instances", { value: await getInstancesMin(), hidden: !enableDedicatedGateway, disabled: false });
|
|
||||||
const response = await getCurrentProvisioningState();
|
|
||||||
if (response.status !== undefined) {
|
|
||||||
disableAttributesOnDedicatedGatewayChange = true;
|
|
||||||
defaults.set("enableDedicatedGateway", { value: true, hidden: false, disabled: false });
|
|
||||||
defaults.set("sku", { value: response.sku, hidden: false, disabled: true });
|
|
||||||
defaults.set("instances", { value: response.instances, hidden: false, disabled: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaults;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@Values({
|
@Values({
|
||||||
description: {
|
description: {
|
||||||
textTKey: "DedicatedGatewayDescription",
|
text: "Provisioning dedicated gateways for SqlX accounts.",
|
||||||
link: {
|
link: {
|
||||||
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
|
||||||
textTKey: "LearnAboutDedicatedGateway",
|
text: "Learn more about dedicated gateway.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -147,21 +73,21 @@ export default class SqlX extends SelfServeBaseClass {
|
|||||||
|
|
||||||
@OnChange(onEnableDedicatedGatewayChange)
|
@OnChange(onEnableDedicatedGatewayChange)
|
||||||
@Values({
|
@Values({
|
||||||
labelTKey: "DedicatedGateway",
|
label: "Dedicated Gateway",
|
||||||
trueLabelTKey: "Enable",
|
trueLabel: "Enable",
|
||||||
falseLabelTKey: "Disable",
|
falseLabel: "Disable",
|
||||||
})
|
})
|
||||||
enableDedicatedGateway: boolean;
|
enableDedicatedGateway: boolean;
|
||||||
|
|
||||||
@Values({
|
@Values({
|
||||||
labelTKey: "SKUs",
|
label: "SKUs",
|
||||||
choices: getSkus,
|
choices: getSkus,
|
||||||
placeholderTKey: "Select SKUs",
|
placeholder: "Select SKUs",
|
||||||
})
|
})
|
||||||
sku: ChoiceItem;
|
sku: ChoiceItem;
|
||||||
|
|
||||||
@Values({
|
@Values({
|
||||||
labelTKey: "NumberOfInstances",
|
label: "Number of instances",
|
||||||
min: getInstancesMin,
|
min: getInstancesMin,
|
||||||
max: getInstancesMax,
|
max: getInstancesMax,
|
||||||
step: 1,
|
step: 1,
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
export type SqlxServiceResource = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
properties: SqlxServiceProps;
|
|
||||||
locations: SqlxServiceLocations;
|
|
||||||
};
|
|
||||||
export type SqlxServiceProps = {
|
|
||||||
serviceType: string;
|
|
||||||
creationTime: string;
|
|
||||||
status: string;
|
|
||||||
instanceSize: string;
|
|
||||||
instanceCount: number;
|
|
||||||
sqlxEndPoint: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SqlxServiceLocations = {
|
|
||||||
location: string;
|
|
||||||
status: string;
|
|
||||||
sqlxEndpoint: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UpdateDedicatedGatewayRequestParameters = {
|
|
||||||
properties: UpdateDedicatedGatewayRequestProperties;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UpdateDedicatedGatewayRequestProperties = {
|
|
||||||
instanceSize: string;
|
|
||||||
instanceCount: number;
|
|
||||||
serviceType: string;
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user