Compare commits

..

1 Commits

Author SHA1 Message Date
Laurent Nguyen
acdd05a79b New MongoQueryTab and component running nteract 2021-01-27 18:38:12 +05:30
119 changed files with 4744 additions and 7745 deletions

View File

@@ -87,7 +87,7 @@ src/Explorer/DataSamples/ContainerSampleGenerator.test.ts
src/Explorer/DataSamples/ContainerSampleGenerator.ts
src/Explorer/DataSamples/DataSamplesUtil.test.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.ts
src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.test.ts

View File

@@ -156,7 +156,6 @@ jobs:
run: |
npm ci
npm start &
node utils/cleanupDBs.js
npm run wait-for-server
npm run test:e2e
shell: bash
@@ -166,8 +165,6 @@ jobs:
PORTAL_RUNNER_RESOURCE_GROUP: ${{ secrets.PORTAL_RUNNER_RESOURCE_GROUP }}
PORTAL_RUNNER_DATABASE_ACCOUNT: ${{ secrets.PORTAL_RUNNER_DATABASE_ACCOUNT }}
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_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}

View File

@@ -1,4 +1,3 @@
{
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com",
"ENABLE_GALLERY_PUBLISH": true
"JUNO_ENDPOINT": "https://tools-staging.cosmos.azure.com"
}

View File

@@ -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

View File

@@ -57,13 +57,6 @@
@FocusColor: #605e5c;
@GalleryBackgroundColor: #fdfdfd;
//Icons
@InfoIconColor: #0072c6;
@WarningIconColor: #db7500;
@ErrorIconColor: #b91f26;
/******************************************************************************
METRICS
/******************************************************************************/

View File

@@ -1523,21 +1523,6 @@ p {
.tooltipVisible();
}
.inputTooltip {
.inputTooltip();
}
.inputTooltip .inputTooltipText {
top: -68px;
.inputTooltipText();
}
.inputTooltip .inputTooltipText::after {
border-width: @MediumSpace @MediumSpace 0 @MediumSpace;
top: 55px;
.inputTooltipTextAfter();
}
.infoTooltip a {
color: @AccentHigh;
}
@@ -3043,45 +3028,3 @@ settings-pane {
.collapsibleSection :hover {
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

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,10 @@
"@azure/identity": "1.2.1",
"@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12",
"@jupyterlab/services": "6.0.2",
"@jupyterlab/terminal": "3.0.3",
"@jupyterlab/services": "6.0.0-rc.2",
"@jupyterlab/terminal": "3.0.0-rc.2",
"@microsoft/applicationinsights-web": "2.5.9",
"@nteract/commutable": "7.4.2",
"@nteract/commutable": "7.3.2",
"@nteract/connected-components": "6.8.2",
"@nteract/core": "15.1.0",
"@nteract/data-explorer": "8.0.3",
@@ -64,9 +64,6 @@
"eslint-plugin-react": "7.20.0",
"hasher": "1.2.0",
"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",
"is-ci": "2.0.0",
"jquery": "3.5.1",
@@ -89,7 +86,6 @@
"react-dnd-html5-backend": "9.4.0",
"react-dom": "16.13.1",
"react-hotkeys": "2.0.0",
"react-i18next": "11.8.5",
"react-notification-system": "0.2.17",
"react-redux": "7.1.3",
"redux": "4.0.4",
@@ -128,7 +124,7 @@
"@types/prop-types": "15.5.8",
"@types/puppeteer": "3.0.1",
"@types/q": "1.5.1",
"@types/react": "17.0.0",
"@types/react": "16.9.56",
"@types/react-dom": "17.0.0",
"@types/react-notification-system": "0.2.39",
"@types/react-redux": "7.1.7",

View File

@@ -119,9 +119,7 @@ export class Features {
public static readonly enableSchema = "enableschema";
public static readonly enableSDKoperations = "enablesdkoperations";
public static readonly showMinRUSurvey = "showminrusurvey";
public static readonly enableDatabaseSettingsTabV1 = "enabledbsettingsv1";
public static readonly selfServeType = "selfservetype";
public static readonly enableKOPanel = "enablekopanel";
}
// flight names returned from the portal are always lowercase
@@ -130,7 +128,6 @@ export class Flights {
public static readonly MongoIndexEditor = "mongoindexeditor";
public static readonly MongoIndexing = "mongoindexing";
public static readonly AutoscaleTest = "autoscaletest";
public static readonly GalleryPublish = "gallerypublish";
}
export class AfecFeatures {

View File

@@ -76,7 +76,7 @@ export const getCollectionUsageSizeInKB = async (databaseName: string, container
return dataUsageSizeInKb + indexUsageSizeInKb;
} catch (error) {
handleError(error, "getCollectionUsageSize");
return undefined;
throw error;
}
};

View File

@@ -26,7 +26,6 @@ export interface ConfigContext {
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
hostedExplorerURL: string;
armAPIVersion?: string;
ENABLE_GALLERY_PUBLISH?: boolean;
}
// Default configuration
@@ -80,11 +79,7 @@ if (process.env.NODE_ENV === "development") {
export async function initializeConfiguration(): Promise<ConfigContext> {
try {
const response = await fetch("./config.json", {
headers: {
"If-None-Match": "", // disable client side cache
},
});
const response = await fetch("./config.json");
if (response.status === 200) {
try {
const { allowedParentFrameOrigins, ...externalConfig } = await response.json();

View File

@@ -5,6 +5,7 @@ import {
TriggerDefinition,
UserDefinedFunctionDefinition,
} from "@azure/cosmos";
import Q from "q";
import { CommandButtonComponentProps } from "../Explorer/Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer/Explorer";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
@@ -91,7 +92,6 @@ export interface Database extends TreeNode {
onDeleteDatabaseContextMenuClick(source: Database, event: MouseEvent | KeyboardEvent): void;
onSettingsClick: () => void;
loadOffer(): Promise<void>;
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
}
export interface CollectionBase extends TreeNode {
@@ -109,7 +109,7 @@ export interface CollectionBase extends TreeNode {
onDocumentDBDocumentsClick(): void;
onNewQueryClick(source: any, event: MouseEvent, queryText?: string): void;
expandCollection(): void;
expandCollection(): Q.Promise<any>;
collapseCollection(): void;
getDatabase(): Database;
}
@@ -138,6 +138,7 @@ export interface Collection extends CollectionBase {
openTab(): void;
onSettingsClick: () => Promise<void>;
onDeleteCollectionContextMenuClick(source: Collection, event: MouseEvent): void;
onNewGraphClick(): 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;
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
uploadFiles(fileList: FileList): Promise<UploadDetails>;
uploadFiles(fileList: FileList): Q.Promise<UploadDetails>;
getLabel(): string;
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
}
/**
@@ -293,6 +293,10 @@ export interface DocumentsTabOptions extends TabOptions {
resourceTokenPartitionKey?: string;
}
export interface SettingsTabV2Options extends TabOptions {
getPendingNotification: Q.Promise<DataModels.Notification>;
}
export interface ConflictsTabOptions extends TabOptions {
partitionKey: DataModels.PartitionKey;
conflictIds: ko.ObservableArray<ConflictId>;
@@ -359,8 +363,7 @@ export enum CollectionTabKind {
Gallery = 17,
NotebookViewer = 18,
Schema = 19,
CollectionSettingsV2 = 20,
DatabaseSettingsV2 = 21,
SettingsV2 = 20,
}
export enum TerminalKind {

View File

@@ -45,8 +45,7 @@ describe("Component Registerer", () => {
});
it("should register settings-tab-v2 component", () => {
expect(ko.components.isRegistered("database-settings-tab-v2")).toBe(true);
expect(ko.components.isRegistered("collection-settings-tab-v2")).toBe(true);
expect(ko.components.isRegistered("settings-tab-v2")).toBe(true);
});
it("should register query-tab component", () => {

View File

@@ -26,12 +26,12 @@ ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponent
ko.components.register("tabs-manager", TabsManagerKOComponent());
// 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("stored-procedure-tab", new TabComponents.StoredProcedureTab());
ko.components.register("trigger-tab", new TabComponents.TriggerTab());
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("tables-query-tab", new TabComponents.QueryTablesTab());
ko.components.register("graph-tab", new TabComponents.GraphTab());
@@ -45,7 +45,6 @@ ko.components.register("notebook-viewer-tab", new TabComponents.NotebookViewerTa
// Database Tabs
ko.components.register("database-settings-tab", new TabComponents.DatabaseSettingsTab());
ko.components.register("database-settings-tab-v2", new TabComponents.SettingsTabV2());
// Panes
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());

View File

@@ -112,7 +112,10 @@ export class ResourceTreeContextMenuButtonFactory {
items.push({
iconSrc: DeleteCollectionIcon,
onClick: () => container.openDeleteCollectionConfirmationPane(),
onClick: () => {
const selectedCollection: ViewModels.Collection = container.findSelectedCollection();
selectedCollection && selectedCollection.onDeleteCollectionContextMenuClick(selectedCollection, null);
},
label: container.deleteCollectionText(),
styleClass: "deleteCollectionMenuItem",
});

View File

@@ -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 { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
import { Link } from "office-ui-fabric-react/lib/Link";
import {
ChoiceGroup,
FontIcon,
IChoiceGroupProps,
IProgressIndicatorProps,
ProgressIndicator,
} from "office-ui-fabric-react";
import { ChoiceGroup, FontIcon, IChoiceGroupProps } from "office-ui-fabric-react";
export interface TextFieldProps extends ITextFieldProps {
label: string;
@@ -33,7 +27,6 @@ export interface DialogProps {
choiceGroupProps?: IChoiceGroupProps;
textFieldProps?: TextFieldProps;
linkProps?: LinkProps;
progressIndicatorProps?: IProgressIndicatorProps;
primaryButtonText: string;
secondaryButtonText: string;
onPrimaryButtonClick: () => void;
@@ -69,14 +62,13 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
showCloseButton: this.props.showCloseButton || false,
onDismiss: this.props.onDismiss,
},
modalProps: { isBlocking: this.props.isModal, isDarkOverlay: false },
modalProps: { isBlocking: this.props.isModal },
minWidth: DIALOG_MIN_WIDTH,
maxWidth: DIALOG_MAX_WIDTH,
};
const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps;
const textFieldProps: ITextFieldProps = this.props.textFieldProps;
const linkProps: LinkProps = this.props.linkProps;
const progressIndicatorProps: IProgressIndicatorProps = this.props.progressIndicatorProps;
const primaryButtonProps: IButtonProps = {
text: this.props.primaryButtonText,
disabled: this.props.primaryButtonDisabled || false,
@@ -99,7 +91,6 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
</Link>
)}
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
<DialogFooter>
<PrimaryButton {...primaryButtonProps} />
{secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}

View File

@@ -18,6 +18,7 @@ import * as React from "react";
import { IGalleryItem } from "../../../../Juno/JunoClient";
import { FileSystemUtil } from "../../../Notebook/FileSystemUtil";
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
import { StyleConstants } from "../../../../Common/Constants";
export interface GalleryCardComponentProps {
data: IGalleryItem;
@@ -37,7 +38,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
private static readonly cardImageHeight = 144;
public static readonly cardHeightToWidthRatio =
GalleryCardComponent.cardImageHeight / GalleryCardComponent.CARD_WIDTH;
private static readonly cardDescriptionMaxChars = 80;
private static readonly cardDescriptionMaxChars = 88;
private static readonly cardItemGapBig = 10;
private static readonly cardItemGapSmall = 8;
@@ -53,7 +54,6 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
return (
<Card
style={{ background: "white" }}
aria-label={cardTitle}
data-is-focusable="true"
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 } }}>
<Text variant="small" nowrap>
{this.props.data.tags ? (
this.props.data.tags.map((tag, index, array) => (
<span key={tag}>
<Link onClick={(event) => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link>
{index === array.length - 1 ? <></> : ", "}
</span>
))
) : (
<br />
)}
{this.props.data.tags?.map((tag, index, array) => (
<span key={tag}>
<Link onClick={(event) => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link>
{index === array.length - 1 ? <></> : ", "}
</span>
))}
</Text>
<Text
@@ -105,14 +101,13 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
</Text>
<Text variant="small" styles={{ root: { height: 36 } }}>
{this.renderTruncatedDescription()}
{this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars)}
</Text>
<span>
{this.props.data.views !== undefined && this.generateIconText("RedEye", this.props.data.views.toString())}
{this.props.data.downloads !== undefined &&
this.generateIconText("Download", this.props.data.downloads.toString())}
{this.props.data.favorites !== undefined &&
{this.generateIconText("RedEye", this.props.data.views.toString())}
{this.generateIconText("Download", this.props.data.downloads.toString())}
{this.props.isFavorite !== undefined &&
this.generateIconText("Heart", this.props.data.favorites.toString())}
</span>
</Card.Section>
@@ -132,7 +127,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
{this.props.isFavorite !== undefined &&
this.generateIconButtonWithTooltip(
this.props.isFavorite ? "HeartFill" : "Heart",
this.props.isFavorite ? "Unfavorite" : "Favorite",
this.props.isFavorite ? "Unlike" : "Like",
"left",
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 => {
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}
</Text>
);

View File

@@ -5,11 +5,6 @@ exports[`GalleryCardComponent renders 1`] = `
aria-label="name"
data-is-focusable="true"
onClick={[Function]}
style={
Object {
"background": "white",
}
}
tokens={
Object {
"childrenGap": 0,
@@ -93,7 +88,7 @@ exports[`GalleryCardComponent renders 1`] = `
styles={
Object {
"root": Object {
"color": "#605E5C",
"color": undefined,
"paddingRight": 8,
},
}
@@ -117,7 +112,7 @@ exports[`GalleryCardComponent renders 1`] = `
styles={
Object {
"root": Object {
"color": "#605E5C",
"color": undefined,
"paddingRight": 8,
},
}
@@ -141,7 +136,7 @@ exports[`GalleryCardComponent renders 1`] = `
styles={
Object {
"root": Object {
"color": "#605E5C",
"color": undefined,
"paddingRight": 8,
},
}
@@ -190,7 +185,7 @@ exports[`GalleryCardComponent renders 1`] = `
"gapSpace": 0,
}
}
content="Favorite"
content="Like"
id="TooltipHost-IconButton-Heart"
styles={
Object {
@@ -202,14 +197,14 @@ exports[`GalleryCardComponent renders 1`] = `
}
>
<CustomizedIconButton
ariaLabel="Favorite"
ariaLabel="Like"
iconProps={
Object {
"iconName": "Heart",
}
}
onClick={[Function]}
title="Favorite"
title="Like"
/>
</StyledTooltipHostBase>
<StyledTooltipHostBase

View File

@@ -2,9 +2,7 @@ import * as React from "react";
import { JunoClient } from "../../../Juno/JunoClient";
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react";
import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { handleError } from "../../../Common/ErrorHandlingUtils";
export interface CodeOfConductComponentProps {
junoClient: JunoClient;
@@ -16,11 +14,11 @@ interface CodeOfConductComponentState {
}
export class CodeOfConductComponent extends React.Component<CodeOfConductComponentProps, CodeOfConductComponentState> {
private viewCodeOfConductTraced: boolean;
private descriptionPara1: string;
private descriptionPara2: string;
private descriptionPara3: string;
private link1: { label: string; url: string };
private link2: { label: string; url: string };
constructor(props: CodeOfConductComponentProps) {
super(props);
@@ -29,34 +27,23 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
readCodeOfConduct: false,
};
this.descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct";
this.descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.";
this.descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the ";
this.link1 = { label: "code of conduct.", url: CodeOfConductEndpoints.codeOfConduct };
this.descriptionPara1 = "Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement";
this.descriptionPara2 =
"Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.";
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> {
const startKey = traceStart(Action.NotebooksGalleryAcceptCodeOfConduct);
try {
const response = await this.props.junoClient.acceptCodeOfConduct();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
}
traceSuccess(Action.NotebooksGalleryAcceptCodeOfConduct, startKey);
this.props.onAcceptCodeOfConduct(response.data);
} catch (error) {
traceFailure(
Action.NotebooksGalleryAcceptCodeOfConduct,
{
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
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 {
if (!this.viewCodeOfConductTraced) {
this.viewCodeOfConductTraced = true;
trace(Action.NotebooksGalleryViewCodeOfConduct);
}
return (
<Stack tokens={{ childrenGap: 20 }}>
<Stack.Item>
@@ -87,6 +69,10 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
<Link href={this.link1.url} target="_blank">
{this.link1.label}
</Link>
{" and "}
<Link href={this.link2.url} target="_blank">
{this.link2.label}
</Link>
</Text>
</Stack.Item>
@@ -101,7 +87,7 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
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}
/>
</Stack.Item>

View File

@@ -7,7 +7,6 @@ import Explorer from "../../Explorer";
export interface GalleryAndNotebookViewerComponentProps {
container?: Explorer;
isGalleryPublishEnabled: boolean;
junoClient: JunoClient;
notebookUrl?: string;
galleryItem?: IGalleryItem;
@@ -61,7 +60,6 @@ export class GalleryAndNotebookViewerComponent extends React.Component<
const props: GalleryViewerComponentProps = {
container: this.props.container,
isGalleryPublishEnabled: this.props.isGalleryPublishEnabled,
junoClient: this.props.junoClient,
selectedTab: this.state.selectedTab,
sortBy: this.state.sortBy,

View File

@@ -7,20 +7,14 @@ import {
} from "./GalleryAndNotebookViewerComponent";
export class GalleryAndNotebookViewerComponentAdapter implements ReactAdapter {
private key: string;
public parameters: ko.Observable<number>;
constructor(private props: GalleryAndNotebookViewerComponentProps) {
this.reset();
this.parameters = ko.observable<number>(Date.now());
}
public renderComponent(): JSX.Element {
return <GalleryAndNotebookViewerComponent key={this.key} {...this.props} />;
}
public reset(): void {
this.key = `GalleryAndNotebookViewerComponent-${Date.now()}`;
return <GalleryAndNotebookViewerComponent {...this.props} />;
}
public triggerRender(): void {

View File

@@ -6,16 +6,4 @@
overflow-y: auto;
width: 100%;
font-family: @DataExplorerFont;
background: @GalleryBackgroundColor;
}
.publicGalleryTabContainer {
position: relative;
height: 100vh;
}
.publicGalleryTabOverlayContent {
background: white;
padding: 20px;
margin: 10%;
}

View File

@@ -5,7 +5,6 @@ import { GalleryViewerComponent, GalleryViewerComponentProps, GalleryTab, SortBy
describe("GalleryViewerComponent", () => {
it("renders", () => {
const props: GalleryViewerComponentProps = {
isGalleryPublishEnabled: false,
junoClient: undefined,
selectedTab: GalleryTab.OfficialSamples,
sortBy: SortBy.MostViewed,

View File

@@ -9,14 +9,10 @@ import {
IPivotProps,
IRectangle,
Label,
Link,
List,
Overlay,
Pivot,
PivotItem,
SearchBox,
Spinner,
SpinnerSize,
Stack,
Text,
} from "office-ui-fabric-react";
@@ -31,12 +27,9 @@ import Explorer from "../../Explorer";
import { CodeOfConductComponent } from "./CodeOfConductComponent";
import { InfoComponent } from "./InfoComponent/InfoComponent";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
export interface GalleryViewerComponentProps {
container?: Explorer;
isGalleryPublishEnabled: boolean;
junoClient: JunoClient;
selectedTab: GalleryTab;
sortBy: SortBy;
@@ -71,8 +64,6 @@ interface GalleryViewerComponentState {
searchText: string;
dialogProps: DialogProps;
isCodeOfConductAccepted: boolean;
isFetchingPublishedNotebooks: boolean;
isFetchingFavouriteNotebooks: boolean;
}
interface GalleryTabInfo {
@@ -83,24 +74,18 @@ interface GalleryTabInfo {
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps, GalleryViewerComponentState> {
public static readonly OfficialSamplesTitle = "Official samples";
public static readonly PublicGalleryTitle = "Public gallery";
public static readonly FavoritesTitle = "My favorites";
public static readonly PublishedTitle = "My published work";
public static readonly FavoritesTitle = "Liked";
public static readonly PublishedTitle = "Your published work";
private static readonly rowsPerPage = 5;
private static readonly mostViewedText = "Most viewed";
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 readonly sortingOptions: IDropdownOption[];
private viewGalleryTraced: boolean;
private viewOfficialSamplesTraced: boolean;
private viewPublicGalleryTraced: boolean;
private viewFavoritesTraced: boolean;
private viewPublishedNotebooksTraced: boolean;
private sampleNotebooks: IGalleryItem[];
private publicNotebooks: IGalleryItem[];
private favoriteNotebooks: IGalleryItem[];
@@ -122,8 +107,6 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
searchText: props.searchText,
dialogProps: undefined,
isCodeOfConductAccepted: undefined,
isFetchingFavouriteNotebooks: true,
isFetchingPublishedNotebooks: true,
};
this.sortingOptions = [
@@ -154,11 +137,9 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}
public render(): JSX.Element {
this.traceViewGallery();
const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
if (this.props.isGalleryPublishEnabled) {
if (this.props.container?.isGalleryPublishEnabled()) {
tabs.push(
this.createPublicGalleryTab(
GalleryTab.PublicGallery,
@@ -166,11 +147,13 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
this.state.isCodeOfConductAccepted
)
);
}
if (this.props.container?.isGalleryPublishEnabled()) {
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 = {
@@ -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 => {
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 (
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}>
<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&apos;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 {
return {
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 => {
return {
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 => {
const { published, underReview, removed } = GalleryUtils.filterPublishedNotebooks(data);
const content = (
<Stack tokens={{ childrenGap: 20 }}>
<Stack tokens={{ childrenGap: 10 }}>
{published?.length > 0 &&
this.createPublishedNotebooksSectionContent(
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)
)}
{underReview?.length > 0 &&
@@ -363,33 +276,24 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
content: JSX.Element
): JSX.Element => {
return (
<Stack tokens={{ childrenGap: 10 }}>
{title && (
<Text styles={{ root: { fontWeight: FontWeights.semibold, marginLeft: 10, marginRight: 10 } }}>{title}</Text>
)}
{description && <Text styles={{ root: { marginLeft: 10, marginRight: 10 } }}>{description}</Text>}
<Stack tokens={{ childrenGap: 5 }}>
{title && <Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{title}</Text>}
{description && <Text>{description}</Text>}
{content}
</Stack>
);
};
private createPublicGalleryTabContent(data: IGalleryItem[], acceptedCodeOfConduct: boolean): JSX.Element {
return (
<div className="publicGalleryTabContainer">
{this.createSearchBarHeader(this.createCardsTabContent(data))}
{acceptedCodeOfConduct === false && (
<Overlay isDarkThemed>
<div className="publicGalleryTabOverlayContent">
<CodeOfConductComponent
junoClient={this.props.junoClient}
onAcceptCodeOfConduct={(result: boolean) => {
this.setState({ isCodeOfConductAccepted: result });
}}
/>
</div>
</Overlay>
)}
</div>
return acceptedCodeOfConduct === false ? (
<CodeOfConductComponent
junoClient={this.props.junoClient}
onAcceptCodeOfConduct={(result: boolean) => {
this.setState({ isCodeOfConductAccepted: result });
}}
/>
) : (
this.createSearchBarHeader(this.createCardsTabContent(data))
);
}
@@ -406,7 +310,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
<Stack.Item styles={{ root: { minWidth: 200 } }}>
<Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
</Stack.Item>
{this.props.isGalleryPublishEnabled && (
{(!this.props.container || this.props.container.isGalleryPublishEnabled()) && (
<Stack.Item>
<InfoComponent />
</Stack.Item>
@@ -418,7 +322,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}
private createCardsTabContent(data: IGalleryItem[]): JSX.Element {
return data ? (
return (
<FocusZone>
<List
items={data}
@@ -427,14 +331,12 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
onRenderCell={this.onRenderCell}
/>
</FocusZone>
) : (
<Spinner size={SpinnerSize.large} />
);
}
private createPolicyViolationsListContent(data: IGalleryItem[]): JSX.Element {
return (
<table style={{ margin: 10 }}>
<table>
<tbody>
<tr>
<th>Name</th>
@@ -483,10 +385,6 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}
this.sampleNotebooks = response.data;
trace(Action.NotebooksGalleryOfficialSamplesCount, ActionModifiers.Mark, {
count: this.sampleNotebooks?.length,
});
} catch (error) {
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) {
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
}
trace(Action.NotebooksGalleryPublicGalleryCount, ActionModifiers.Mark, { count: this.publicNotebooks?.length });
} catch (error) {
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> {
if (!offline) {
try {
this.setState({ isFetchingFavouriteNotebooks: true });
const response = await this.props.junoClient.getFavoriteNotebooks();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when loading favorite notebooks`);
}
this.favoriteNotebooks = response.data;
trace(Action.NotebooksGalleryFavoritesCount, ActionModifiers.Mark, { count: this.favoriteNotebooks?.length });
} catch (error) {
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> {
if (!offline) {
try {
this.setState({ isFetchingPublishedNotebooks: true });
const response = await this.props.junoClient.getPublishedNotebooks();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when loading published notebooks`);
}
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) {
handleError(error, "GalleryViewerComponent/loadPublishedNotebooks", "Failed to load published notebooks");
} finally {
this.setState({ isFetchingPublishedNotebooks: false });
}
}

View File

@@ -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>
</StackItem>
<StackItem>
<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>
</StackItem>
<StackItem>
<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
href="https://aka.ms/cosmos-code-of-conduct"
target="_blank"
>
code of conduct.
code of conduct
</StyledLinkBase>
and
<StyledLinkBase
href="https://aka.ms/ms-privacy-policy"
target="_blank"
>
privacy statement
</StyledLinkBase>
</Text>
</StackItem>
<StackItem>
<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]}
styles={
Object {

View File

@@ -77,11 +77,24 @@ exports[`GalleryViewerComponent renders 1`] = `
selectedKey={0}
/>
</StackItem>
<StackItem>
<InfoComponent />
</StackItem>
</Stack>
<StackItem>
<StyledSpinnerBase
size={3}
/>
<FocusZone
direction={2}
isCircularNavigation={false}
shouldRaiseClicks={true}
>
<List
getPageSpecification={[Function]}
onRenderCell={[Function]}
renderedWindowsAhead={3}
renderedWindowsBehind={2}
startIndex={0}
/>
</FocusZone>
</StackItem>
</Stack>
</PivotItem>

View File

@@ -31,26 +31,6 @@ export interface 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 {
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
@@ -69,7 +49,19 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
</Text>
</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 && (
<Stack.Item>

View File

@@ -3,11 +3,14 @@
*/
import { Notebook } from "@nteract/commutable";
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 { contents } from "rx-jupyter";
import * as Logger from "../../../Common/Logger";
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
@@ -18,9 +21,7 @@ import Explorer from "../../Explorer";
import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
import { DialogHost } from "../../../Utils/GalleryUtils";
import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
export interface NotebookViewerComponentProps {
container?: Explorer;
@@ -79,12 +80,6 @@ export class NotebookViewerComponent
}
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 {
const response = await fetch(this.props.notebookUrl);
if (!response.ok) {
@@ -92,16 +87,6 @@ export class NotebookViewerComponent
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();
this.removeNotebookViewerLink(notebook, this.props.galleryItem?.newCellId);
this.notebookComponentBootstrapper.setContent("json", notebook);
@@ -116,18 +101,6 @@ export class NotebookViewerComponent
SessionStorageUtility.setEntry(this.props.galleryItem?.id, "true");
}
} 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 });
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
showOkCancelModalDialog(
title: string,
@@ -239,10 +186,8 @@ export class NotebookViewerComponent
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
progressIndicatorProps?: IProgressIndicatorProps,
choiceGroupProps?: IChoiceGroupProps,
textFieldProps?: TextFieldProps,
primaryButtonDisabled?: boolean
textFieldProps?: TextFieldProps
): void {
this.setState({
dialogProps: {
@@ -260,10 +205,8 @@ export class NotebookViewerComponent
this.setState({ dialogProps: undefined });
onCancel && onCancel();
},
progressIndicatorProps,
choiceGroupProps,
textFieldProps,
primaryButtonDisabled,
},
});
}

View File

@@ -2,7 +2,7 @@ import { shallow } from "enzyme";
import React from "react";
import { SettingsComponentProps, SettingsComponent, SettingsComponentState } from "./SettingsComponent";
import * as ViewModels from "../../../Contracts/ViewModels";
import { CollectionSettingsTabV2 } from "../../Tabs/SettingsTabV2";
import SettingsTabV2 from "../../Tabs/SettingsTabV2";
import { collection } from "./TestUtils";
import * as DataModels from "../../../Contracts/DataModels";
import ko from "knockout";
@@ -31,21 +31,25 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
}));
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types";
import Q from "q";
jest.mock("../../../Common/dataAccess/updateOffer", () => ({
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer),
}));
describe("SettingsComponent", () => {
const baseProps: SettingsComponentProps = {
settingsTab: new CollectionSettingsTabV2({
settingsTab: new SettingsTabV2({
collection: collection,
tabKind: ViewModels.CollectionTabKind.CollectionSettingsV2,
tabKind: ViewModels.CollectionTabKind.SettingsV2,
title: "Scale & Settings",
tabPath: "",
node: undefined,
hashLocation: "settings",
isActive: ko.observable(false),
onUpdateTabsButtons: undefined,
getPendingNotification: Q.Promise<DataModels.Notification>(() => {
return;
}),
}),
};
@@ -138,7 +142,6 @@ describe("SettingsComponent", () => {
readSettings: undefined,
onSettingsClick: undefined,
loadOffer: undefined,
getPendingThroughputSplitNotification: undefined,
} as ViewModels.Database;
newCollection.getDatabase = () => newDatabase;
newCollection.offer = ko.observable(undefined);

View File

@@ -11,7 +11,7 @@ import Explorer from "../../Explorer";
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { SettingsTabV2 } from "../../Tabs/SettingsTabV2";
import SettingsTab from "../../Tabs/SettingsTabV2";
import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
import {
@@ -58,7 +58,7 @@ interface ButtonV2 {
}
export interface SettingsComponentProps {
settingsTab: SettingsTabV2;
settingsTab: SettingsTab;
}
export interface SettingsComponentState {
@@ -116,10 +116,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
private discardSettingsChangesButton: ButtonV2;
private isAnalyticalStorageEnabled: boolean;
private isCollectionSettingsTab: boolean;
private collection: ViewModels.Collection;
private database: ViewModels.Database;
private offer: DataModels.Offer;
private container: Explorer;
private changeFeedPolicyVisible: boolean;
private isFixedContainer: boolean;
@@ -129,28 +126,20 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
constructor(props: SettingsComponentProps) {
super(props);
this.isCollectionSettingsTab = this.props.settingsTab.tabKind === ViewModels.CollectionTabKind.CollectionSettingsV2;
if (this.isCollectionSettingsTab) {
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
this.container = this.collection?.container;
this.offer = this.collection?.offer();
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
this.shouldShowIndexingPolicyEditor =
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB();
this.collection = this.props.settingsTab.collection as ViewModels.Collection;
this.container = this.collection?.container;
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
this.shouldShowIndexingPolicyEditor =
this.container && !this.container.isPreferredApiCassandra() && !this.container.isPreferredApiMongoDB();
this.changeFeedPolicyVisible = this.collection?.container.isFeatureEnabled(
Constants.Features.enableChangeFeedPolicy
);
this.changeFeedPolicyVisible = this.collection?.container.isFeatureEnabled(
Constants.Features.enableChangeFeedPolicy
);
// Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer =
this.container.isPreferredApiMongoDB() &&
(!this.collection?.partitionKey || this.collection?.partitionKey.systemKey);
} else {
this.database = this.props.settingsTab.database;
this.container = this.database?.container;
this.offer = this.database?.offer();
}
// Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer =
this.container.isPreferredApiMongoDB() &&
(!this.collection.partitionKey || this.collection.partitionKey.systemKey);
this.state = {
throughput: undefined,
@@ -217,21 +206,18 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
}
componentDidMount(): void {
if (this.isCollectionSettingsTab) {
this.refreshIndexTransformationProgress();
this.loadMongoIndexes();
}
this.refreshIndexTransformationProgress();
this.loadMongoIndexes();
this.setAutoPilotStates();
this.setBaseline();
if (this.props.settingsTab.isActive()) {
this.props.settingsTab.getContainer().onUpdateTabsButtons(this.getTabsButtons());
this.props.settingsTab.getSettingsTabContainer().onUpdateTabsButtons(this.getTabsButtons());
}
}
componentDidUpdate(): void {
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 => {
const autoscaleMaxThroughput = this.offer?.autoscaleMaxThroughput;
const autoscaleMaxThroughput = this.collection?.offer()?.autoscaleMaxThroughput;
if (autoscaleMaxThroughput && AutoPilotUtils.isValidAutoPilotThroughput(autoscaleMaxThroughput)) {
this.setState({
@@ -309,7 +295,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
!!this.collection.conflictResolutionPolicy();
public isOfferReplacePending = (): boolean => {
return this.offer?.offerReplacePending;
return this.collection?.offer()?.offerReplacePending;
};
public onSaveClick = async (): Promise<void> => {
@@ -323,10 +309,174 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
tabTitle: this.props.settingsTab.tabTitle(),
});
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
try {
await (this.isCollectionSettingsTab
? this.saveCollectionSettings(startKey)
: this.saveDatabaseSettings(startKey));
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.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) {
this.container.isRefreshingExplorer(false);
this.props.settingsTab.isExecutionError(true);
@@ -345,9 +495,8 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
},
startKey
);
} finally {
this.props.settingsTab.isExecuting(false);
}
this.props.settingsTab.isExecuting(false);
};
public onRevertClick = (): void => {
@@ -544,17 +693,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
};
public setBaseline = (): void => {
const offerThroughput = this.offer?.manualThroughput;
if (!this.isCollectionSettingsTab) {
this.setState({
throughput: offerThroughput,
throughputBaseline: offerThroughput,
});
return;
}
const defaultTtl = this.collection.defaultTtl();
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
? ChangeFeedPolicyState.On
: ChangeFeedPolicyState.Off;
@@ -672,225 +811,9 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
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 {
const scaleComponentProps: ScaleComponentProps = {
collection: this.collection,
database: this.database,
container: this.container,
isFixedContainer: this.isFixedContainer,
onThroughputChange: this.onThroughputChange,
@@ -907,16 +830,6 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
initialNotification: this.props.settingsTab.pendingNotification(),
};
if (!this.isCollectionSettingsTab) {
return (
<div className="settingsV2MainContainer">
<div className="settingsV2TabsContainer">
<ScaleComponent {...scaleComponentProps} />
</div>
</div>
);
}
const subSettingsComponentProps: SubSettingsComponentProps = {
collection: this.collection,
container: this.container,
@@ -986,7 +899,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
};
const tabs: SettingsV2TabInfo[] = [];
if (!hasDatabaseSharedThroughput(this.collection) && this.offer) {
if (!hasDatabaseSharedThroughput(this.collection) && this.collection.offer()) {
tabs.push({
tab: SettingsV2TabTypes.ScaleTab,
content: <ScaleComponent {...scaleComponentProps} />,

View File

@@ -375,7 +375,7 @@ export const getThroughputApplyShortDelayMessage = (
<Text styles={infoAndToolTipTextStyle} id="throughputApplyShortDelayMessage">
A request to increase the throughput is currently in progress. This operation will take some time to complete.
<br />
{collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `}
Database: {databaseName}, Container: {collectionName}{" "}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit)}
</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
complete. View the latest status in Notifications.
<br />
{collectionName ? `Database: ${databaseName}, Container: ${collectionName} ` : `Database: ${databaseName} `}
Database: {databaseName}, Container: {collectionName}{" "}
{getCurrentThroughput(isAutoscale, throughput, throughputUnit, requestedThroughput)}
</Text>
);

View File

@@ -18,7 +18,6 @@ describe("ScaleComponent", () => {
const baseProps: ScaleComponentProps = {
collection: collection,
database: undefined,
container: container,
isFixedContainer: false,
onThroughputChange: () => {

View File

@@ -21,7 +21,6 @@ import { configContext, Platform } from "../../../../ConfigContext";
export interface ScaleComponentProps {
collection: ViewModels.Collection;
database: ViewModels.Database;
container: Explorer;
isFixedContainer: boolean;
onThroughputChange: (newThroughput: number) => void;
@@ -40,16 +39,9 @@ export interface ScaleComponentProps {
export class ScaleComponent extends React.Component<ScaleComponentProps> {
private isEmulator: boolean;
private offer: DataModels.Offer;
private databaseId: string;
private collectionId: string;
constructor(props: ScaleComponentProps) {
super(props);
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 => {
@@ -95,7 +87,9 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
return SharedConstants.CollectionCreation.DefaultCollectionRUs400;
}
return this.offer?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400;
return (
this.props.collection.offer()?.minimumThroughput || SharedConstants.CollectionCreation.DefaultCollectionRUs400
);
};
public getThroughputTitle = (): string => {
@@ -121,14 +115,15 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
return this.getLongDelayMessage();
}
if (this.offer?.offerReplacePending) {
const throughput = this.offer.manualThroughput || this.offer.autoscaleMaxThroughput;
const offer = this.props.collection?.offer();
if (offer?.offerReplacePending) {
const throughput = offer.manualThroughput || offer.autoscaleMaxThroughput;
return getThroughputApplyShortDelayMessage(
this.props.isAutoPilotSelected,
throughput,
throughputUnit,
this.databaseId,
this.collectionId
this.props.collection.databaseId,
this.props.collection.id()
);
}
@@ -140,7 +135,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
this.canThroughputExceedMaximumValue() &&
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
if (throughputExceedsBackendLimits && !this.props.isFixedContainer) {
if (throughputExceedsBackendLimits && !!this.props.collection.partitionKey && !this.props.isFixedContainer) {
return updateThroughputBeyondLimitWarningMessage;
}
@@ -159,8 +154,8 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
this.props.wasAutopilotOriginallySet,
throughput,
throughputUnit,
this.databaseId,
this.collectionId,
this.props.collection.databaseId,
this.props.collection.id(),
targetThroughput
);
}
@@ -170,15 +165,15 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
private getThroughputInputComponent = (): JSX.Element => (
<ThroughputInputAutoPilotV3Component
databaseAccount={this.props.container.databaseAccount()}
databaseName={this.databaseId}
collectionName={this.collectionId}
databaseName={this.props.collection.databaseId}
collectionName={this.props.collection.id()}
serverId={this.props.container.serverId()}
throughput={this.props.throughput}
throughputBaseline={this.props.throughputBaseline}
onThroughputChange={this.props.onThroughputChange}
minimum={this.getMinRUs()}
maximum={this.getMaxRUs()}
isEnabled={!!this.props.database || !hasDatabaseSharedThroughput(this.props.collection)}
isEnabled={!hasDatabaseSharedThroughput(this.props.collection)}
canExceedMaximumValue={this.canThroughputExceedMaximumValue()}
label={this.getThroughputTitle()}
isEmulator={this.isEmulator}
@@ -194,7 +189,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
onScaleSaveableChange={this.props.onScaleSaveableChange}
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
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() && (
<Stack {...subComponentStackProps}>
{this.getThroughputInputComponent()}
{!this.props.database && this.getStorageCapacityTitle()}
{this.getStorageCapacityTitle()}
</Stack>
)}

View File

@@ -40,7 +40,6 @@ import { userContext } from "../../../../../UserContext";
import { SubscriptionType } from "../../../../../Contracts/SubscriptionType";
import { usageInGB, calculateEstimateNumber } from "../../../../../Utils/PricingUtils";
import { Features } from "../../../../../Common/Constants";
import { minAutoPilotThroughput } from "../../../../../Utils/AutoPilotUtils";
import * as TelemetryProcessor from "../../../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../../../Shared/Telemetry/TelemetryConstants";
@@ -542,7 +541,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
step={AutoPilotUtils.autoPilotIncrementStep}
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
onChange={this.onAutoPilotThroughputChange}
min={minAutoPilotThroughput}
/>
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
{this.minRUperGBSurvey()}
@@ -581,7 +579,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
: this.props.throughput?.toString()
}
onChange={this.onThroughputChange}
min={this.props.minimum}
/>
{this.state.exceedFreeTierThroughput && (
<MessageBar

View File

@@ -142,7 +142,6 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
id="autopilotInput"
key="auto pilot throughput input"
label="Max RU/s"
min={4000}
onChange={[Function]}
required={true}
step={1000}
@@ -261,7 +260,6 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
disabled={false}
id="throughputInput"
key="provisioned throughput input"
min={10000}
onChange={[Function]}
required={true}
step={100}
@@ -535,7 +533,6 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
disabled={false}
id="throughputInput"
key="provisioned throughput input"
min={10000}
onChange={[Function]}
required={true}
step={100}

View File

@@ -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.
<br />
Database: test, Container: test
Database:
test
, Container:
test
, Current autoscale throughput: 100 - 1000 RU/s, Target autoscale throughput: 600 - 6000 RU/s
</Text>
</StyledMessageBarBase>

View File

@@ -46,7 +46,6 @@ describe("SettingsUtils", () => {
readSettings: undefined,
onSettingsClick: undefined,
loadOffer: undefined,
getPendingThroughputSplitNotification: undefined,
} as ViewModels.Database;
};
newCollection.offer(undefined);

View File

@@ -804,7 +804,6 @@ exports[`SettingsComponent renders 1`] = `
},
"clickHostedAccountSwitch": [Function],
"clickHostedDirectorySwitch": [Function],
"closeSidePanel": undefined,
"collapsedResourceTreeWidth": 36,
"collectionCreationDefaults": Object {
"storage": "100",
@@ -1022,7 +1021,6 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function],
"openSidePanel": undefined,
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
@@ -2085,7 +2083,6 @@ exports[`SettingsComponent renders 1`] = `
},
"clickHostedAccountSwitch": [Function],
"clickHostedDirectorySwitch": [Function],
"closeSidePanel": undefined,
"collapsedResourceTreeWidth": 36,
"collectionCreationDefaults": Object {
"storage": "100",
@@ -2303,7 +2300,6 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function],
"openSidePanel": undefined,
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
@@ -3379,7 +3375,6 @@ exports[`SettingsComponent renders 1`] = `
},
"clickHostedAccountSwitch": [Function],
"clickHostedDirectorySwitch": [Function],
"closeSidePanel": undefined,
"collapsedResourceTreeWidth": 36,
"collectionCreationDefaults": Object {
"storage": "100",
@@ -3597,7 +3592,6 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function],
"openSidePanel": undefined,
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],
@@ -4660,7 +4654,6 @@ exports[`SettingsComponent renders 1`] = `
},
"clickHostedAccountSwitch": [Function],
"clickHostedDirectorySwitch": [Function],
"closeSidePanel": undefined,
"collapsedResourceTreeWidth": 36,
"collectionCreationDefaults": Object {
"storage": "100",
@@ -4878,7 +4871,6 @@ exports[`SettingsComponent renders 1`] = `
"onRefreshResourcesClick": [Function],
"onSwitchToConnectionString": [Function],
"onToggleKeyDown": [Function],
"openSidePanel": undefined,
"provideFeedbackEmail": [Function],
"queriesClient": QueriesClient {
"container": [Circular],

View File

@@ -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.
<br />
Database: sampleDb, Container: sampleCollection
Database:
sampleDb
, Container:
sampleCollection
, Current manual throughput: 1000 RU/s
</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.
<br />
Database: sampleDb, Container: sampleCollection
Database:
sampleDb
, Container:
sampleCollection
, Current manual throughput: 1000 RU/s, Target manual throughput: 2000
</Text>
<Text

View File

@@ -8,10 +8,10 @@ describe("SmartUiComponent", () => {
root: {
id: "root",
info: {
messageTKey: "Start at $24/mo per database",
message: "Start at $24/mo per database",
link: {
href: "https://aka.ms/azure-cosmos-db-pricing",
textTKey: "More Details",
text: "More Details",
},
},
children: [
@@ -21,10 +21,10 @@ describe("SmartUiComponent", () => {
dataFieldName: "description",
type: "string",
description: {
textTKey: "this is an example description text.",
text: "this is an example description text.",
link: {
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",
input: {
labelTKey: "Throughput (input)",
label: "Throughput (input)",
dataFieldName: "throughput",
type: "number",
min: 400,
@@ -45,7 +45,7 @@ describe("SmartUiComponent", () => {
{
id: "throughput2",
input: {
labelTKey: "Throughput (Slider)",
label: "Throughput (Slider)",
dataFieldName: "throughput2",
type: "number",
min: 400,
@@ -58,7 +58,7 @@ describe("SmartUiComponent", () => {
{
id: "throughput3",
input: {
labelTKey: "Throughput (invalid)",
label: "Throughput (invalid)",
dataFieldName: "throughput3",
type: "boolean",
min: 400,
@@ -72,7 +72,7 @@ describe("SmartUiComponent", () => {
{
id: "containerId",
input: {
labelTKey: "Container id",
label: "Container id",
dataFieldName: "containerId",
type: "string",
},
@@ -80,9 +80,9 @@ describe("SmartUiComponent", () => {
{
id: "analyticalStore",
input: {
labelTKey: "Analytical Store",
trueLabelTKey: "Enabled",
falseLabelTKey: "Disabled",
label: "Analytical Store",
trueLabel: "Enabled",
falseLabel: "Disabled",
defaultValue: true,
dataFieldName: "analyticalStore",
type: "boolean",
@@ -91,7 +91,7 @@ describe("SmartUiComponent", () => {
{
id: "database",
input: {
labelTKey: "Database",
label: "Database",
dataFieldName: "database",
type: "object",
choices: [
@@ -117,9 +117,6 @@ describe("SmartUiComponent", () => {
onError={() => {
return;
}}
getTranslation={(key: string) => {
return key;
}}
/>
);
await new Promise((resolve) => setTimeout(resolve, 0));
@@ -148,9 +145,6 @@ describe("SmartUiComponent", () => {
onError={() => {
return;
}}
getTranslation={(key: string) => {
return key;
}}
/>
);
await new Promise((resolve) => setTimeout(resolve, 0));

View File

@@ -18,7 +18,6 @@ import {
NumberUiType,
SmartUiInput,
} from "../../../SelfServe/SelfServeTypes";
import { TFunction } from "i18next";
/**
* Generic UX renderer
@@ -35,8 +34,8 @@ interface BaseDisplay {
}
interface BaseInput extends BaseDisplay {
labelTKey: string;
placeholderTKey?: string;
label: string;
placeholder?: string;
errorMessage?: string;
}
@@ -52,8 +51,8 @@ interface NumberInput extends BaseInput {
}
interface BooleanInput extends BaseInput {
trueLabelTKey: string;
falseLabelTKey: string;
trueLabel: string;
falseLabel: string;
defaultValue?: boolean;
}
@@ -90,7 +89,6 @@ export interface SmartUiComponentProps {
onInputChange: (input: AnyDisplay, newValue: InputType) => void;
onError: (hasError: boolean) => void;
disabled: boolean;
getTranslation: TFunction;
}
interface SmartUiComponentState {
@@ -124,10 +122,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
private renderInfo(info: Info): JSX.Element {
return (
<MessageBar styles={{ root: { width: 400 } }}>
{this.props.getTranslation(info.messageTKey)}
{info.message}
{info.link && (
<Link href={info.link.href} target="_blank">
{this.props.getTranslation(info.link.textTKey)}
{info.link.text}
</Link>
)}
</MessageBar>
@@ -141,10 +139,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
<div className="stringInputContainer">
<TextField
id={`${input.dataFieldName}-textField-input`}
label={this.props.getTranslation(input.labelTKey)}
label={input.label}
type="text"
value={value || ""}
placeholder={this.props.getTranslation(input.placeholderTKey)}
placeholder={input.placeholder}
disabled={disabled}
onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
styles={{
@@ -167,10 +165,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
const description = input.description;
return (
<Text id={`${input.dataFieldName}-text-display`}>
{this.props.getTranslation(input.description.textTKey)}{" "}
{input.description.text}{" "}
{description.link && (
<Link target="_blank" href={input.description.link.href}>
{this.props.getTranslation(input.description.link.textTKey)}
{input.description.link.text}
</Link>
)}
</Text>
@@ -221,12 +219,12 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
};
private renderNumberInput(input: NumberInput): JSX.Element {
const { labelTKey, min, max, dataFieldName, step } = input;
const { label, min, max, dataFieldName, step } = input;
const props = {
label: this.props.getTranslation(labelTKey),
label: label,
min: min,
max: max,
ariaLabel: labelTKey,
ariaLabel: label,
step: step,
};
@@ -286,10 +284,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return (
<Toggle
id={`${input.dataFieldName}-toggle-input`}
label={this.props.getTranslation(input.labelTKey)}
label={input.label}
checked={value || false}
onText={this.props.getTranslation(input.trueLabelTKey)}
offText={this.props.getTranslation(input.falseLabelTKey)}
onText={input.trueLabel}
offText={input.falseLabel}
disabled={disabled}
onChange={(event, checked: boolean) => this.props.onInputChange(input, checked)}
styles={{ root: { width: 400 } }}
@@ -298,7 +296,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
}
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 disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
let selectedKey = value ? value : defaultKey;
@@ -308,14 +306,14 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return (
<Dropdown
id={`${input.dataFieldName}-dropdown-input`}
label={this.props.getTranslation(labelTKey)}
label={label}
selectedKey={selectedKey}
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
placeholder={this.props.getTranslation(placeholderTKey)}
placeholder={placeholder}
disabled={disabled}
options={choices.map((c) => ({
key: c.key,
text: this.props.getTranslation(c.label),
text: c.label,
}))}
styles={{
root: { width: 400 },

View File

@@ -1,4 +1,3 @@
import React from "react";
import * as ComponentRegisterer from "./ComponentRegisterer";
import * as Constants from "../Common/Constants";
import * as DataModels from "../Contracts/DataModels";
@@ -48,6 +47,7 @@ import { ExplorerMetrics } from "../Common/Constants";
import { ExplorerSettings } from "../Shared/ExplorerSettings";
import { FileSystemUtil } from "./Notebook/FileSystemUtil";
import { handleOpenAction } from "./OpenActions";
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
import { IGalleryItem } from "../Juno/JunoClient";
import { LoadQueryPane } from "./Panes/LoadQueryPane";
import * as Logger from "../Common/Logger";
@@ -91,8 +91,6 @@ import { appInsights } from "../Shared/appInsights";
import { SelfServeLoadingComponentAdapter } from "../SelfServe/SelfServeLoadingComponentAdapter";
import { SelfServeType } from "../SelfServe/SelfServeUtils";
import { SelfServeComponentAdapter } from "../SelfServe/SelfServeComponentAdapter";
import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@@ -112,8 +110,6 @@ export interface ExplorerParams {
setIsNotificationConsoleExpanded: (isExpanded: boolean) => void;
setNotificationConsoleData: (consoleData: ConsoleData) => void;
setInProgressConsoleDataIdToBeDeleted: (id: string) => void;
openSidePanel: (headerText: string, panelContent: JSX.Element) => void;
closeSidePanel: () => void;
}
export default class Explorer {
@@ -161,8 +157,6 @@ export default class Explorer {
// Panes
public contextPanes: ContextualPaneBase[];
private openSidePanel: (headerText: string, panelContent: JSX.Element) => void;
private closeSidePanel: () => void;
// Resource Tree
public databases: ko.ObservableArray<ViewModels.Database>;
@@ -284,8 +278,6 @@ export default class Explorer {
this.setIsNotificationConsoleExpanded = params?.setIsNotificationConsoleExpanded;
this.setNotificationConsoleData = params?.setNotificationConsoleData;
this.setInProgressConsoleDataIdToBeDeleted = params?.setInProgressConsoleDataIdToBeDeleted;
this.openSidePanel = params?.openSidePanel;
this.closeSidePanel = params?.closeSidePanel;
const startKey: number = TelemetryProcessor.traceStart(Action.InitializeDataExplorer, {
dataExplorerArea: Constants.Areas.ResourceTree,
@@ -431,8 +423,8 @@ export default class Explorer {
this.shouldShowShareDialogContents = ko.observable<boolean>(false);
this.shouldShowDataAccessExpiryDialog = ko.observable<boolean>(false);
this.shouldShowContextSwitchPrompt = ko.observable<boolean>(false);
this.isGalleryPublishEnabled = ko.computed<boolean>(
() => configContext.ENABLE_GALLERY_PUBLISH || this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
);
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
@@ -1897,9 +1889,6 @@ export default class Explorer {
if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) {
this.isMongoIndexingEnabled(true);
}
if (flights.indexOf(Constants.Flights.GalleryPublish) !== -1) {
this.isGalleryPublishEnabled = ko.computed<boolean>(() => true);
}
}
public findSelectedCollection(): ViewModels.Collection {
@@ -2260,7 +2249,7 @@ export default class Explorer {
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) {
await this.notebookManager.openPublishNotebookPane(
name,
@@ -2357,13 +2346,11 @@ export default class Explorer {
this.tabsManager.activateTab(notebookTab);
} else {
const options: NotebookTabOptions = {
account: userContext.databaseAccount,
tabKind: ViewModels.CollectionTabKind.NotebookV2,
node: null,
title: notebookContentItem.name,
tabPath: notebookContentItem.path,
collection: null,
masterKey: userContext.masterKey || "",
hashLocation: "notebooks",
isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true),
@@ -2821,36 +2808,10 @@ export default class Explorer {
}
}
public async openGallery(
selectedTab?: GalleryTab,
notebookUrl?: string,
galleryItem?: IGalleryItem,
isFavorite?: boolean
) {
public async openGallery(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) {
let title: 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(
ViewModels.CollectionTabKind.Gallery,
(tab) => tab.hashLocation() == hashLocation
@@ -2859,12 +2820,31 @@ export default class Explorer {
if (galleryTab) {
this.tabsManager.activateTab(galleryTab);
(galleryTab as any).reset(galleryTabOptions);
} else {
if (!this.galleryTab) {
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
}
const newTab = new this.galleryTab.default(galleryTabOptions);
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);
}
}
@@ -3046,17 +3026,4 @@ export default class Explorer {
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()}
/>
);
}
}

View File

@@ -36,36 +36,7 @@ export class CommandBarComponentButtonFactory {
}
const newCollectionBtn = CommandBarComponentButtonFactory.createNewCollectionGroup(container);
const buttons: CommandButtonComponentProps[] = [];
if (container.isFeatureEnabled && container.isFeatureEnabled("regionselectbutton")) {
const regions = [{ name: "West US" }, { name: "East US" }, { name: "North Europe" }];
buttons.push({
iconSrc: null,
onCommandClick: () => {},
commandButtonLabel: null,
hasPopup: false,
isDropdown: true,
dropdownPlaceholder: "West US",
dropdownSelectedKey: "West US",
dropdownWidth: 100,
children: regions.map(
(region) =>
({
iconSrc: null,
onCommandClick: () => {},
commandButtonLabel: region.name,
dropdownItemKey: region.name,
hasPopup: false,
disabled: false,
ariaLabel: "",
} as CommandButtonComponentProps)
),
ariaLabel: "",
});
}
buttons.push(newCollectionBtn);
const buttons: CommandButtonComponentProps[] = [newCollectionBtn];
const addSynapseLink = CommandBarComponentButtonFactory.createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) {

View File

@@ -114,7 +114,6 @@ export class NotificationConsoleComponent extends React.Component<
<div className="notificationConsoleContainer">
<div
className="notificationConsoleHeader"
id="notificationConsoleHeader"
ref={this.setElememntRef}
onClick={(event: React.MouseEvent<HTMLDivElement>) => this.expandCollapseConsole()}
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onExpandCollapseKeyPress(event)}

View File

@@ -6,7 +6,6 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
>
<div
className="notificationConsoleHeader"
id="notificationConsoleHeader"
onClick={[Function]}
onKeyDown={[Function]}
tabIndex={0}
@@ -170,7 +169,6 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
>
<div
className="notificationConsoleHeader"
id="notificationConsoleHeader"
onClick={[Function]}
onKeyDown={[Function]}
tabIndex={0}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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>
);
}
}

View File

@@ -818,7 +818,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
let indexingPolicy: DataModels.IndexingPolicy;
let createMongoWildcardIndex: boolean;
// todo - remove mongo indexing policy ticket # 616274
if (this.container.isPreferredApiMongoDB() && this.container.isEnableMongoCapabilityPresent()) {
if (this.container.isPreferredApiMongoDB()) {
createMongoWildcardIndex = this.shouldCreateMongoWildcardIndex();
} else if (this.showIndexingOptionsForSharedThroughput()) {
if (this.useIndexingForSharedThroughput()) {

View 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);
});
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -1,7 +1,9 @@
import * as ko from "knockout";
import Q from "q";
import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "../../Common/Constants";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
@@ -48,7 +50,18 @@ export default class DeleteCollectionConfirmationPane extends ContextualPaneBase
dataExplorerArea: Constants.Areas.ContextualPane,
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.close();

View File

@@ -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
);
}
}
}

View File

@@ -52,71 +52,81 @@ export default class DeleteDatabaseConfirmationPane extends ContextualPaneBase {
dataExplorerArea: Constants.Areas.ContextualPane,
paneTitle: this.title(),
});
return Q(
deleteDatabase(selectedDatabase.id()).then(
() => {
this.isExecuting(false);
this.close();
this.container.refreshAllDatabases();
this.container.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
this.container.selectedNode(null);
selectedDatabase
.collections()
.forEach((collection: ViewModels.Collection) =>
this.container.tabsManager.closeTabsByComparator(
(tab) =>
tab.node?.id() === collection.id() &&
(tab.node as ViewModels.Collection).databaseId === collection.databaseId
)
);
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
// TODO: Should not be a Q promise anymore, but the Cassandra code requires it
let promise: Q.Promise<any>;
if (this.container.isPreferredApiCassandra()) {
promise = (<CassandraAPIDataClient>this.container.tableDataClient).deleteTableOrKeyspace(
this.container.databaseAccount().properties.cassandraEndpoint,
this.container.databaseAccount().id,
`DROP KEYSPACE ${selectedDatabase.id()};`,
this.container
);
} else {
promise = Q(deleteDatabase(selectedDatabase.id()));
}
return promise.then(
() => {
this.isExecuting(false);
this.close();
this.container.refreshAllDatabases();
this.container.tabsManager.closeTabsByComparator((tab) => tab.node?.id() === selectedDatabase.id());
this.container.selectedNode(null);
selectedDatabase
.collections()
.forEach((collection: ViewModels.Collection) =>
this.container.tabsManager.closeTabsByComparator(
(tab) =>
tab.node?.id() === collection.id() &&
(tab.node as ViewModels.Collection).databaseId === collection.databaseId
)
);
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()) {
let deleteFeedback = new DeleteFeedback(
this.container.databaseAccount().id,
this.container.databaseAccount().name,
DefaultExperienceUtility.getApiKindFromDefaultExperience(this.container.defaultExperience()),
this.databaseDeleteFeedback()
);
TelemetryProcessor.trace(Action.DeleteDatabase, ActionModifiers.Mark, {
message: JSON.stringify(deleteFeedback, Object.getOwnPropertyNames(deleteFeedback)),
});
TelemetryProcessor.trace(Action.DeleteDatabase, ActionModifiers.Mark, {
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
);
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
);
}
);
}

View File

@@ -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;
}

View File

@@ -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();
});
});

View File

@@ -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";
};
}

View File

@@ -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>
);

View File

@@ -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>
);

View File

@@ -10,11 +10,7 @@ import { ImmutableNotebook } from "@nteract/commutable/src";
import { toJS } from "@nteract/commutable";
import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent";
import { HttpStatusCodes } from "../../Common/Constants";
import { handleError, getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { GalleryTab } from "../Controls/NotebookGallery/GalleryViewerComponent";
import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { FileSystemUtil } from "../Notebook/FileSystemUtil";
import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils";
export class PublishNotebookPaneAdapter implements ReactAdapter {
parameters: ko.Observable<number>;
@@ -70,7 +66,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
onChangeDescription: (newValue: string) => (this.description = newValue),
onChangeTags: (newValue: string) => (this.tags = newValue),
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
onError: this.createFormError,
onError: this.createFormErrorForLargeImageSelection,
clearFormError: this.clearFormError,
};
@@ -144,21 +140,10 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
this.isExecuting = true;
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 {
startKey = traceStart(Action.NotebooksGalleryPublish, {
databaseAccountName: this.container.databaseAccount()?.name,
defaultExperience: this.container.defaultExperience(),
});
if (!this.name || !this.description || !this.author) {
throw new Error("Name, description, and author are required");
}
const response = await this.junoClient.publishNotebook(
this.name,
@@ -172,43 +157,17 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
const data = response.data;
if (data) {
let isPublishPending = false;
if (data.pendingScanJobIds?.length > 0) {
isPublishPending = true;
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).`
);
} else {
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) {
traceFailure(
Action.NotebooksGalleryPublish,
{
databaseAccountName: this.container.databaseAccount()?.name,
defaultExperience: this.container.defaultExperience(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
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}`;
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", this.formError);
return;
@@ -221,7 +180,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
this.close();
}
private createFormError = (formError: string, formErrorDetail: string, area: string): void => {
private createFormErrorForLargeImageSelection = (formError: string, formErrorDetail: string, area: string): void => {
this.formError = formError;
this.formErrorDetail = formErrorDetail;
handleError(formErrorDetail, area, formError);

View File

@@ -14,7 +14,7 @@ export interface PublishNotebookPaneProps {
notebookAuthor: string;
notebookCreatedDate: string;
notebookObject: ImmutableNotebook;
notebookParentDomElement?: HTMLElement;
notebookParentDomElement: HTMLElement;
onChangeName: (newValue: string) => void;
onChangeDescription: (newValue: string) => void;
onChangeTags: (newValue: string) => void;
@@ -54,7 +54,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
super(props);
this.state = {
type: ImageTypes.CustomImage,
type: ImageTypes.Url,
notebookName: props.notebookName,
notebookDescription: "",
notebookTags: "",
@@ -110,7 +110,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
};
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.props.notebookName,
@@ -120,7 +120,6 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
this.thumbnailUrlProps = {
label: "Cover image url",
ariaLabel: "Cover image url",
required: true,
onChange: (event, newValue) => {
this.props.onChangeImageSrc(newValue);
this.setState({ imageSrc: newValue });
@@ -141,23 +140,17 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
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 = {
label: "Cover image",
defaultSelectedKey: ImageTypes.CustomImage,
defaultSelectedKey: ImageTypes.Url,
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) => {
this.setState({ imageSrc: undefined });
this.props.onChangeImageSrc(undefined);
this.props.clearFormError();
if (options.text === ImageTypes.TakeScreenshot) {
try {
@@ -179,12 +172,11 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
this.nameProps = {
label: "Name",
ariaLabel: "Name",
defaultValue: FileSystemUtil.stripExtension(this.props.notebookName, "ipynb"),
defaultValue: this.props.notebookName,
required: true,
onChange: (event, newValue) => {
const notebookName = newValue + ".ipynb";
this.props.onChangeName(notebookName);
this.setState({ notebookName });
this.props.onChangeName(newValue);
this.setState({ notebookName: newValue });
},
};
@@ -301,16 +293,16 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
thumbnailUrl: this.state.imageSrc,
created: this.props.notebookCreatedDate,
isSample: false,
downloads: undefined,
favorites: undefined,
views: undefined,
downloads: 0,
favorites: 0,
views: 0,
newCellId: undefined,
policyViolations: undefined,
pendingScanJobIds: undefined,
}}
isFavorite={undefined}
showDownload={false}
showDelete={false}
isFavorite={false}
showDownload={true}
showDelete={true}
onClick={undefined}
onTagClick={undefined}
onFavoriteClick={undefined}

View File

@@ -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>
`;

View File

@@ -14,7 +14,7 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
>
<StackItem>
<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>
</StackItem>
<StackItem>
@@ -25,7 +25,7 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
<StackItem>
<StyledTextFieldBase
ariaLabel="Name"
defaultValue="SampleNotebook"
defaultValue="SampleNotebook.ipynb"
label="Name"
onChange={[Function]}
required={true}
@@ -52,29 +52,36 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
<StackItem>
<StyledWithResponsiveMode
ariaLabel="Cover image"
defaultSelectedKey="Custom Image"
defaultSelectedKey="URL"
label="Cover image"
onChange={[Function]}
options={
Array [
Object {
"key": "URL",
"text": "URL",
},
Object {
"key": "Custom Image",
"text": "Custom Image",
},
Object {
"key": "URL",
"text": "URL",
"key": "Take Screenshot",
"text": "Take Screenshot",
},
Object {
"key": "Use First Display Output",
"text": "Use First Display Output",
},
]
}
/>
</StackItem>
<StackItem>
<input
accept="image/*"
id="selectImageFile"
<StyledTextFieldBase
ariaLabel="Cover image url"
label="Cover image url"
onChange={[Function]}
type="file"
/>
</StackItem>
<StackItem>
@@ -89,8 +96,8 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
"author": "CosmosDB",
"created": "2020-07-17T00:00:00Z",
"description": "",
"downloads": undefined,
"favorites": undefined,
"downloads": 0,
"favorites": 0,
"gitSha": undefined,
"id": undefined,
"isSample": false,
@@ -102,11 +109,12 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
"",
],
"thumbnailUrl": undefined,
"views": undefined,
"views": 0,
}
}
showDelete={false}
showDownload={false}
isFavorite={false}
showDelete={true}
showDownload={true}
/>
</StackItem>
</Stack>

View File

@@ -19,7 +19,6 @@ import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import { userContext } from "../../UserContext";
export interface CassandraTableKeys {
partitionKeys: CassandraTableKey[];
@@ -346,7 +345,7 @@ export class CassandraAPIDataClient extends TableDataClient {
ConsoleDataType.InProgress,
`Creating a new keyspace with query ${createKeyspaceQuery}`
);
this.createOrDeleteQuery(cassandraEndpoint, resourceId, createKeyspaceQuery)
this.createOrDeleteQuery(cassandraEndpoint, resourceId, createKeyspaceQuery, explorer)
.then(
(data: any) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -392,7 +391,7 @@ export class CassandraAPIDataClient extends TableDataClient {
ConsoleDataType.InProgress,
`Creating a new table with query ${createTableQuery}`
);
this.createOrDeleteQuery(cassandraEndpoint, resourceId, createTableQuery)
this.createOrDeleteQuery(cassandraEndpoint, resourceId, createTableQuery, explorer)
.then(
(data: any) => {
NotificationConsoleUtils.logConsoleMessage(
@@ -417,6 +416,41 @@ export class CassandraAPIDataClient extends TableDataClient {
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> {
if (!!collection.cassandraKeys) {
return Q.resolve(collection.cassandraKeys);
@@ -517,7 +551,12 @@ export class CassandraAPIDataClient extends TableDataClient {
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 authType = window.authType;
const apiEndpoint: string =
@@ -527,7 +566,7 @@ export class CassandraAPIDataClient extends TableDataClient {
$.ajax(`${configContext.BACKEND_ENDPOINT}/${apiEndpoint}`, {
type: "POST",
data: {
accountName: userContext.databaseAccount?.name,
accountName: explorer.databaseAccount() && explorer.databaseAccount().name,
cassandraEndpoint: this.trimCassandraEndpoint(cassandraEndpoint),
resourceId: resourceId,
query: query,

View File

@@ -387,6 +387,8 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
tabTitle: this.tabTitle(),
});
const headerOptions: RequestOptions = { initialHeaders: {} };
try {
const updateOfferParams: DataModels.UpdateOfferParams = {
databaseId: this.database.id(),

View File

@@ -11,7 +11,6 @@ interface GalleryTabOptions extends ViewModels.TabOptions {
account: DatabaseAccount;
container: Explorer;
junoClient: JunoClient;
selectedTab: GalleryViewerTab;
notebookUrl?: string;
galleryItem?: IGalleryItem;
isFavorite?: boolean;
@@ -22,46 +21,27 @@ interface GalleryTabOptions extends ViewModels.TabOptions {
*/
export default class GalleryTab extends TabsBase {
private container: Explorer;
private galleryAndNotebookViewerComponentProps: GalleryAndNotebookViewerComponentProps;
public galleryAndNotebookViewerComponentAdapter: GalleryAndNotebookViewerComponentAdapter;
constructor(options: GalleryTabOptions) {
super(options);
this.container = options.container;
this.galleryAndNotebookViewerComponentProps = {
this.container = options.container;
const props: GalleryAndNotebookViewerComponentProps = {
container: options.container,
isGalleryPublishEnabled: options.container.isGalleryPublishEnabled(),
junoClient: options.junoClient,
notebookUrl: options.notebookUrl,
galleryItem: options.galleryItem,
isFavorite: options.isFavorite,
selectedTab: options.selectedTab,
selectedTab: GalleryViewerTab.OfficialSamples,
sortBy: SortBy.MostViewed,
searchText: undefined,
};
this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter(
this.galleryAndNotebookViewerComponentProps
);
this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter(props);
}
public reset(options: GalleryTabOptions) {
this.container = options.container;
this.galleryAndNotebookViewerComponentProps.container = options.container;
this.galleryAndNotebookViewerComponentProps.junoClient = options.junoClient;
this.galleryAndNotebookViewerComponentProps.notebookUrl = options.notebookUrl;
this.galleryAndNotebookViewerComponentProps.galleryItem = options.galleryItem;
this.galleryAndNotebookViewerComponentProps.isFavorite = options.isFavorite;
this.galleryAndNotebookViewerComponentProps.selectedTab = options.selectedTab;
this.galleryAndNotebookViewerComponentProps.sortBy = SortBy.MostViewed;
this.galleryAndNotebookViewerComponentProps.searchText = undefined;
this.galleryAndNotebookViewerComponentAdapter.reset();
this.galleryAndNotebookViewerComponentAdapter.triggerRender();
}
public getContainer(): Explorer {
protected getContainer(): Explorer {
return this.container;
}
}

View File

@@ -0,0 +1 @@
<div data-bind="react:mongoQueryComponentAdapter" style="height: 100%"></div>

View 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();
}
}

View 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,
});
}
}

View File

@@ -2,9 +2,7 @@ import * as _ from "underscore";
import * as Q from "q";
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import * as DataModels from "../../Contracts/DataModels";
import TabsBase from "./TabsBase";
import NewCellIcon from "../../../images/notebook/Notebook-insert-cell.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 InterruptKernelIcon from "../../../images/notebook/Notebook-stop.svg";
import KillKernelIcon from "../../../images/notebook/Notebook-stop.svg";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import { Areas, ArmApiVersions } from "../../Common/Constants";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { ArmApiVersions } from "../../Common/Constants";
import { CommandBarComponentButtonFactory } from "../Menus/CommandBar/CommandBarComponentButtonFactory";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
import { NotebookConfigurationUtils } from "../../Utils/NotebookConfigurationUtils";
import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2";
import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2";
import { configContext } from "../../ConfigContext";
import Explorer from "../Explorer";
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { toJS, stringifyNotebook } from "@nteract/commutable";
import { appInsights } from "../../Shared/appInsights";
import { userContext } from "../../UserContext";
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
export interface NotebookTabOptions extends ViewModels.TabOptions {
account: DataModels.DatabaseAccount;
masterKey: string;
container: Explorer;
export interface NotebookTabOptions extends NotebookTabBaseOptions {
notebookContentItem: NotebookContentItem;
}
export default class NotebookTabV2 extends TabsBase {
private static clientManager: NotebookClientV2;
private container: Explorer;
export default class NotebookTabV2 extends NotebookTabBase {
public notebookPath: ko.Observable<string>;
private selectedSparkPool: ko.Observable<string>;
private notebookComponentAdapter: NotebookComponentAdapter;
@@ -52,16 +44,6 @@ export default class NotebookTabV2 extends TabsBase {
super(options);
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.container.notebookServerInfo.subscribe((newValue: DataModels.NotebookWorkspaceConnectionInfo) => {
@@ -71,7 +53,7 @@ export default class NotebookTabV2 extends TabsBase {
this.notebookComponentAdapter = new NotebookComponentAdapter({
contentItem: options.notebookContentItem,
notebooksBasePath: this.container.getNotebookBasePath(),
notebookClient: NotebookTabV2.clientManager,
notebookClient: NotebookTabBase.clientManager,
onUpdateKernelInfo: this.onKernelUpdate,
});
@@ -117,10 +99,6 @@ export default class NotebookTabV2 extends TabsBase {
return await this.configureServiceEndpoints(this.notebookComponentAdapter.getCurrentKernelName());
}
public getContainer(): Explorer {
return this.container;
}
protected getTabsButtons(): CommandButtonComponentProps[] {
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
@@ -485,10 +463,6 @@ export default class NotebookTabV2 extends TabsBase {
}
private publishToGallery = async () => {
TelemetryProcessor.trace(Action.NotebooksGalleryClickPublishToGallery, ActionModifiers.Mark, {
source: Source.CommandBarMenu,
});
const notebookContent = this.notebookComponentAdapter.getContent();
await this.container.publishNotebook(
notebookContent.name,
@@ -508,12 +482,4 @@ export default class NotebookTabV2 extends TabsBase {
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,
});
}
}

View File

@@ -58,7 +58,7 @@ export default class NotebookViewerTab extends TabsBase {
});
}
public getContainer(): Explorer {
protected getContainer(): Explorer {
return this.container;
}

View File

@@ -3,6 +3,7 @@ import * as DataModels from "../../Contracts/DataModels";
import TabsBase from "./TabsBase";
import { SettingsComponentAdapter } from "../Controls/Settings/SettingsComponentAdapter";
import { SettingsComponentProps } from "../Controls/Settings/SettingsComponent";
import Explorer from "../Explorer";
import { traceFailure } from "../../Shared/Telemetry/TelemetryProcessor";
import ko from "knockout";
import * as Constants from "../../Common/Constants";
@@ -10,27 +11,23 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
export class SettingsTabV2 extends TabsBase {
export default class SettingsTabV2 extends TabsBase {
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);
this.options = options;
this.tabId = "SettingsV2-" + this.tabId;
const props: SettingsComponentProps = {
settingsTab: this,
};
this.settingsComponentAdapter = new SettingsComponentAdapter(props);
}
}
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.currentCollection = this.collection as ViewModels.Collection;
this.notificationRead = ko.observable(false);
this.offerRead = ko.observable(false);
this.settingsComponentAdapter.parameters = ko.computed<boolean>(() => {
@@ -48,95 +45,49 @@ export class CollectionSettingsTabV2 extends SettingsTabV2 {
public async onActivate(): Promise<void> {
try {
this.isExecuting(true);
const collection: ViewModels.Collection = this.collection as ViewModels.Collection;
await collection.loadOffer();
await this.currentCollection.loadOffer();
// 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.notification = data;
this.notificationRead(true);
} catch (error) {
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.options.getPendingNotification.then(
(data: DataModels.Notification) => {
this.notification = data;
this.notificationRead(true);
},
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 {
this.offerRead(true);
this.isExecuting(false);
}
super.onActivate();
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.CollectionSettingsV2);
}
}
export class DatabaseSettingsTabV2 extends SettingsTabV2 {
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);
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.SettingsV2);
}
public getSettingsTabContainer(): Explorer {
return this.getContainer();
}
}

View File

@@ -29,7 +29,7 @@ export default class SparkMasterTab extends TabsBase {
this.sparkMasterSrc = ko.observable<string>(sparkMasterEndpoint && sparkMasterEndpoint.endpoint);
}
public getContainer() {
protected getContainer() {
return this._container;
}
}

View File

@@ -5,6 +5,7 @@ import SparkMasterTabTemplate from "./SparkMasterTab.html";
import NotebookV2TabTemplate from "./NotebookV2Tab.html";
import TerminalTabTemplate from "./TerminalTab.html";
import MongoDocumentsTabTemplate from "./MongoDocumentsTab.html";
import MongoDocumentsTabV2Template from "./MongoDocumentsTabV2.html";
import MongoQueryTabTemplate from "./MongoQueryTab.html";
import MongoShellTabTemplate from "./MongoShellTab.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 {
constructor() {
return {

View File

@@ -177,7 +177,7 @@ export default class TabsBase extends WaitsForTemplateViewModel {
return Q();
}
public getContainer(): Explorer {
protected getContainer(): Explorer {
return (this.collection && this.collection.container) || (this.database && this.database.container);
}

View File

@@ -143,11 +143,7 @@
<!-- /ko -->
<!-- ko if: $data.tabKind === 20 -->
<collection-settings-tab-v2 params="{data: $data}"></collection-settings-tab-v2>
<!-- /ko -->
<!-- ko if: $data.tabKind === 21 -->
<database-settings-tab-v2 params="{data: $data}"></database-settings-tab-v2>
<settings-tab-v2 params="{data: $data}"></settings-tab-v2>
<!-- /ko -->
</div>
<!-- /ko -->

View File

@@ -56,7 +56,7 @@ export default class TerminalTab extends TabsBase {
});
}
public getContainer(): Explorer {
protected getContainer(): Explorer {
return this.container;
}

View File

@@ -1,5 +1,6 @@
import { Resource, StoredProcedureDefinition, TriggerDefinition, UserDefinedFunctionDefinition } from "@azure/cosmos";
import * as ko from "knockout";
import Q from "q";
import * as _ from "underscore";
import UploadWorker from "worker-loader!../../workers/upload";
import { AuthType } from "../../AuthType";
@@ -22,11 +23,12 @@ import ConflictsTab from "../Tabs/ConflictsTab";
import DocumentsTab from "../Tabs/DocumentsTab";
import GraphTab from "../Tabs/GraphTab";
import MongoDocumentsTab from "../Tabs/MongoDocumentsTab";
import MongoDocumentsTabV2 from "../Tabs/MongoDocumentsTabV2";
import MongoQueryTab from "../Tabs/MongoQueryTab";
import MongoShellTab from "../Tabs/MongoShellTab";
import QueryTab from "../Tabs/QueryTab";
import QueryTablesTab from "../Tabs/QueryTablesTab";
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
import SettingsTabV2 from "../Tabs/SettingsTabV2";
import ConflictId from "./ConflictId";
import DocumentId from "./DocumentId";
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()) {
return;
return Q();
}
this.isCollectionExpanded(true);
@@ -267,6 +269,8 @@ export default class Collection implements ViewModels.Collection {
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
return Q.resolve();
}
public onDocumentDBDocumentsClick() {
@@ -493,11 +497,11 @@ export default class Collection implements ViewModels.Collection {
dataExplorerArea: Constants.Areas.ResourceTree,
});
const mongoDocumentsTabs: MongoDocumentsTab[] = this.container.tabsManager.getTabs(
const mongoDocumentsTabs: MongoDocumentsTabV2[] = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.Documents,
(tab) => tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id()
) as MongoDocumentsTab[];
let mongoDocumentsTab: MongoDocumentsTab = mongoDocumentsTabs && mongoDocumentsTabs[0];
) as MongoDocumentsTabV2[];
let mongoDocumentsTab: MongoDocumentsTabV2 = mongoDocumentsTabs && mongoDocumentsTabs[0];
if (mongoDocumentsTab) {
this.container.tabsManager.activateTab(mongoDocumentsTab);
@@ -512,9 +516,8 @@ export default class Collection implements ViewModels.Collection {
});
this.documentIds([]);
mongoDocumentsTab = new MongoDocumentsTab({
partitionKey: this.partitionKey,
documentIds: this.documentIds,
mongoDocumentsTab = new MongoDocumentsTabV2({
container: this.container,
tabKind: ViewModels.CollectionTabKind.Documents,
title: "Documents",
tabPath: "",
@@ -544,12 +547,10 @@ export default class Collection implements ViewModels.Collection {
});
const tabTitle = !this.offer() ? "Settings" : "Scale & Settings";
const matchingTabs = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.CollectionSettingsV2,
(tab) => {
return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id();
}
);
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
const matchingTabs = this.container.tabsManager.getTabs(ViewModels.CollectionTabKind.SettingsV2, (tab) => {
return tab.collection && tab.collection.databaseId === this.databaseId && tab.collection.id() === this.id();
});
const traceStartData = {
databaseAccountName: this.container.databaseAccount().name,
@@ -571,20 +572,26 @@ export default class Collection implements ViewModels.Collection {
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
};
let settingsTabV2 = matchingTabs && (matchingTabs[0] as CollectionSettingsTabV2);
this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions);
let settingsTabV2 = matchingTabs && (matchingTabs[0] as SettingsTabV2);
this.launchSettingsTabV2(settingsTabV2, traceStartData, settingsTabOptions, pendingNotificationsPromise);
};
private launchSettingsTabV2 = (
settingsTabV2: CollectionSettingsTabV2,
settingsTabV2: SettingsTabV2,
traceStartData: any,
settingsTabOptions: ViewModels.TabOptions
settingsTabOptions: ViewModels.TabOptions,
getPendingNotification: Q.Promise<DataModels.Notification>
): void => {
const settingsTabV2Options: ViewModels.SettingsTabV2Options = {
...settingsTabOptions,
getPendingNotification: getPendingNotification,
};
if (!settingsTabV2) {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, traceStartData);
settingsTabOptions.onLoadStartKey = startKey;
settingsTabOptions.tabKind = ViewModels.CollectionTabKind.CollectionSettingsV2;
settingsTabV2 = new CollectionSettingsTabV2(settingsTabOptions);
settingsTabV2Options.onLoadStartKey = startKey;
settingsTabV2Options.tabKind = ViewModels.CollectionTabKind.SettingsV2;
settingsTabV2 = new SettingsTabV2(settingsTabV2Options);
this.container.tabsManager.activateNewTab(settingsTabV2);
} else {
this.container.tabsManager.activateTab(settingsTabV2);
@@ -969,19 +976,23 @@ export default class Collection implements ViewModels.Collection {
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
if (configContext.platform === Platform.Hosted && window.authType === AuthType.AAD) {
return this._uploadFilesCors(fileList);
}
const documentUploader: Worker = new UploadWorker();
const deferred: Q.Deferred<UploadDetails> = Q.defer<UploadDetails>();
let inProgressNotificationId: string = "";
if (!fileList || fileList.length === 0) {
return Promise.reject("No files specified");
return Q.reject("No files specified");
}
const onmessage = (resolve: (value: UploadDetails) => void, reject: (reason: any) => void, event: MessageEvent) => {
documentUploader.onmessage = (event: MessageEvent) => {
const numSuccessful: number = event.data.numUploadsSuccessful;
const numFailed: number = event.data.numUploadsFailed;
const runtimeError: string = event.data.runtimeError;
@@ -990,26 +1001,31 @@ export default class Collection implements ViewModels.Collection {
NotificationConsoleUtils.clearInProgressMessageWithId(inProgressNotificationId);
documentUploader.terminate();
if (!!runtimeError) {
reject(runtimeError);
deferred.reject(runtimeError);
} else if (numSuccessful === 0) {
// 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) {
NotificationConsoleUtils.logConsoleError(
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Failed to upload ${numFailed} of ${numSuccessful + numFailed} documents to container ${this.id()}`
);
} else {
NotificationConsoleUtils.logConsoleInfo(
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info,
`Successfully uploaded all ${numSuccessful} documents to container ${this.id()}`
);
}
this._logUploadDetailsInConsole(uploadDetails);
resolve(uploadDetails);
deferred.resolve(uploadDetails);
};
function onerror(reject: (reason: any) => void, event: ErrorEvent) {
documentUploader.onerror = (event: ErrorEvent): void => {
documentUploader.terminate();
reject(event.error);
}
deferred.reject(event.error);
};
const uploaderMessage: StartUploadMessageParams = {
files: fileList,
@@ -1024,68 +1040,42 @@ export default class Collection implements ViewModels.Collection {
},
};
return new Promise<UploadDetails>((resolve, reject) => {
documentUploader.onmessage = onmessage.bind(null, resolve, reject);
documentUploader.onerror = onerror.bind(null, reject);
documentUploader.postMessage(uploaderMessage);
inProgressNotificationId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Uploading and creating documents in container ${this.id()}`
);
documentUploader.postMessage(uploaderMessage);
inProgressNotificationId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress,
`Uploading and creating documents in container ${this.id()}`
);
});
return deferred.promise;
};
public async getPendingThroughputSplitNotification(): Promise<DataModels.Notification> {
if (!this.container) {
return undefined;
private _uploadFilesCors(files: FileList): Q.Promise<UploadDetails> {
const deferred: Q.Deferred<UploadDetails> = Q.defer<UploadDetails>();
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 {
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;
}
return deferred.promise;
}
private async _uploadFilesCors(files: FileList): Promise<UploadDetails> {
const data = await Promise.all(Array.from(files).map((file) => this._uploadFile(file)));
private _uploadFile(file: File): Q.Promise<UploadDetailsRecord> {
const deferred: Q.Deferred<UploadDetailsRecord> = Q.defer();
return { data };
}
private _uploadFile(file: File): Promise<UploadDetailsRecord> {
const reader = new FileReader();
const onload = (resolve: (value: UploadDetailsRecord) => void, evt: any): void => {
reader.onload = (evt: any): void => {
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 => {
resolve({
reader.onerror = (evt: ProgressEvent): void => {
deferred.resolve({
fileName: file.name,
numSucceeded: 0,
numFailed: 1,
@@ -1093,11 +1083,9 @@ export default class Collection implements ViewModels.Collection {
});
};
return new Promise<UploadDetailsRecord>((resolve) => {
reader.onload = onload.bind(this, resolve);
reader.onerror = onerror.bind(this, resolve);
reader.readAsText(file);
});
reader.readAsText(file);
return deferred.promise;
}
private async _createDocumentsFromFile(fileName: string, documentContent: string): Promise<UploadDetailsRecord> {
@@ -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 {
const uploadDetailsRecords: UploadDetailsRecord[] = uploadDetails.data;
const numFiles: number = uploadDetailsRecords.length;

View File

@@ -1,11 +1,11 @@
import * as _ from "underscore";
import * as ko from "knockout";
import Q from "q";
import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import DatabaseSettingsTab from "../Tabs/DatabaseSettingsTab";
import { DatabaseSettingsTabV2 } from "../Tabs/SettingsTabV2";
import Collection from "./Collection";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
@@ -16,6 +16,7 @@ import { readCollections } from "../../Common/dataAccess/readCollections";
import { JunoClient, IJunoResponse } from "../../Juno/JunoClient";
import { userContext } from "../../UserContext";
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { fetchPortalNotifications } from "../../Common/PortalNotifications";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
@@ -58,17 +59,12 @@ export default class Database implements ViewModels.Database {
dataExplorerArea: Constants.Areas.ResourceTree,
});
const pendingNotificationsPromise: Promise<DataModels.Notification> = this.getPendingThroughputSplitNotification();
const useDatabaseSettingsTabV1: boolean = this.container.isFeatureEnabled(
Constants.Features.enableDatabaseSettingsTabV1
const pendingNotificationsPromise: Q.Promise<DataModels.Notification> = this._getPendingThroughputSplitNotification();
const matchingTabs = this.container.tabsManager.getTabs(
ViewModels.CollectionTabKind.DatabaseSettings,
(tab) => tab.node?.id() === this.id()
);
const tabKind: ViewModels.CollectionTabKind = useDatabaseSettingsTabV1
? ViewModels.CollectionTabKind.DatabaseSettings
: ViewModels.CollectionTabKind.DatabaseSettingsV2;
const matchingTabs = this.container.tabsManager.getTabs(tabKind, (tab) => tab.node?.id() === this.id());
let settingsTab: DatabaseSettingsTab | DatabaseSettingsTabV2 = useDatabaseSettingsTabV1
? (matchingTabs?.[0] as DatabaseSettingsTab)
: (matchingTabs?.[0] as DatabaseSettingsTabV2);
let settingsTab: DatabaseSettingsTab = matchingTabs && (matchingTabs[0] as DatabaseSettingsTab);
if (!settingsTab) {
const startKey: number = TelemetryProcessor.traceStart(Action.Tab, {
databaseAccountName: this.container.databaseAccount().name,
@@ -79,11 +75,9 @@ export default class Database implements ViewModels.Database {
});
pendingNotificationsPromise.then(
(data: any) => {
const pendingNotification: DataModels.Notification = data?.[0];
const tabOptions: ViewModels.TabOptions = {
tabKind: useDatabaseSettingsTabV1
? ViewModels.CollectionTabKind.DatabaseSettings
: ViewModels.CollectionTabKind.DatabaseSettingsV2,
const pendingNotification: DataModels.Notification = data && data[0];
settingsTab = new DatabaseSettingsTab({
tabKind: ViewModels.CollectionTabKind.DatabaseSettings,
title: "Scale",
tabPath: "",
node: this,
@@ -93,10 +87,8 @@ export default class Database implements ViewModels.Database {
isActive: ko.observable(false),
onLoadStartKey: startKey,
onUpdateTabsButtons: this.container.onUpdateTabsButtons,
};
settingsTab = useDatabaseSettingsTabV1
? new DatabaseSettingsTab(tabOptions)
: new DatabaseSettingsTabV2(tabOptions);
});
settingsTab.pendingNotification(pendingNotification);
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) {
return undefined;
return Q.resolve(undefined);
}
try {
const notifications: DataModels.Notification[] = await fetchPortalNotifications();
if (!notifications || notifications.length === 0) {
return undefined;
}
const deferred: Q.Deferred<DataModels.Notification> = Q.defer<DataModels.Notification>();
fetchPortalNotifications().then(
(notifications) => {
if (!notifications || notifications.length === 0) {
deferred.resolve(undefined);
return;
}
return _.find(notifications, (notification: DataModels.Notification) => {
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress");
return (
notification.kind === "message" &&
!notification.collectionName &&
notification.databaseName === this.id() &&
notification.description &&
throughputUpdateRegExp.test(notification.description)
const pendingNotification = _.find(notifications, (notification: DataModels.Notification) => {
const throughputUpdateRegExp: RegExp = new RegExp("Throughput update (.*) in progress");
return (
notification.kind === "message" &&
!notification.collectionName &&
notification.databaseName === 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.id(),
collectionName: this.id(),
}),
"Settings tree node"
);
});
} catch (error) {
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);
}
);
return undefined;
}
return deferred.promise;
}
private getDeltaCollections(

View File

@@ -41,9 +41,9 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
this.isCollectionExpanded = ko.observable<boolean>(true);
}
public expandCollection(): void {
public expandCollection(): Q.Promise<void> {
if (this.isCollectionExpanded()) {
return;
return Q();
}
this.isCollectionExpanded(true);
@@ -55,6 +55,8 @@ export default class ResourceTokenCollection implements ViewModels.CollectionBas
defaultExperience: this.container.defaultExperience(),
dataExplorerArea: Constants.Areas.ResourceTree,
});
return Q.resolve();
}
public collapseCollection() {

View File

@@ -15,13 +15,12 @@ import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import RefreshIcon from "../../../images/refresh-cosmos.svg";
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
import FileIcon from "../../../images/notebook/file-cosmos.svg";
import PublishIcon from "../../../images/notebook/publish_content.svg";
import { ArrayHashMap } from "../../Common/ArrayHashMap";
import { NotebookUtil } from "../Notebook/NotebookUtil";
import _ from "underscore";
import { IPinnedRepo } from "../../Juno/JunoClient";
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 * as GitHubUtils from "../../Utils/GitHubUtils";
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
if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
items = items.filter((item) => item.label !== "Copy to ...");

View File

@@ -1,5 +0,0 @@
@import "../../less/Common/Constants";
.standalone-gallery-root {
background: @GalleryBackgroundColor;
}

View File

@@ -1,10 +1,9 @@
import "bootstrap/dist/css/bootstrap.css";
import "./GalleryViewer.less";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import { Text, Link } from "office-ui-fabric-react";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { configContext, initializeConfiguration } from "../ConfigContext";
import { initializeConfiguration } from "../ConfigContext";
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
import {
GalleryAndNotebookViewerComponent,
@@ -25,7 +24,6 @@ const onInit = async () => {
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
const props: GalleryAndNotebookViewerComponentProps = {
isGalleryPublishEnabled: configContext.ENABLE_GALLERY_PUBLISH,
junoClient: new JunoClient(),
selectedTab: galleryViewerProps.selectedTab || GalleryTab.OfficialSamples,
sortBy: galleryViewerProps.sortBy || SortBy.MostViewed,
@@ -33,7 +31,7 @@ const onInit = async () => {
};
const element = (
<div className="standalone-gallery-root">
<>
<header>
<GalleryHeaderComponent />
</header>
@@ -54,7 +52,7 @@ const onInit = async () => {
<GalleryAndNotebookViewerComponent {...props} />
</div>
</div>
</>
);
ReactDOM.render(element, document.getElementById("galleryContent"));

View File

@@ -225,7 +225,7 @@ describe("Gallery", () => {
const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK);
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",
headers: {
@@ -248,7 +248,7 @@ describe("Gallery", () => {
const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK);
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",
headers: {
@@ -270,16 +270,13 @@ describe("Gallery", () => {
const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith(
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}/unfavorite`,
{
method: "PATCH",
headers: {
[authorizationHeader.header]: authorizationHeader.token,
[HttpHeaders.contentType]: "application/json",
},
}
);
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/unfavorite`, {
method: "PATCH",
headers: {
[authorizationHeader.header]: authorizationHeader.token,
[HttpHeaders.contentType]: "application/json",
},
});
});
it("getFavoriteNotebooks", async () => {
@@ -292,15 +289,12 @@ describe("Gallery", () => {
const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith(
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/favorites`,
{
headers: {
[authorizationHeader.header]: authorizationHeader.token,
[HttpHeaders.contentType]: "application/json",
},
}
);
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/favorites`, {
headers: {
[authorizationHeader.header]: authorizationHeader.token,
[HttpHeaders.contentType]: "application/json",
},
});
});
it("getPublishedNotebooks", async () => {
@@ -314,7 +308,7 @@ describe("Gallery", () => {
const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK);
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: {
[authorizationHeader.header]: authorizationHeader.token,
@@ -335,16 +329,13 @@ describe("Gallery", () => {
const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith(
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}`,
{
method: "DELETE",
headers: {
[authorizationHeader.header]: authorizationHeader.token,
[HttpHeaders.contentType]: "application/json",
},
}
);
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}`, {
method: "DELETE",
headers: {
[authorizationHeader.header]: authorizationHeader.token,
[HttpHeaders.contentType]: "application/json",
},
});
});
it("publishNotebook", async () => {
@@ -373,7 +364,7 @@ describe("Gallery", () => {
const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK);
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",
headers: {

View File

@@ -186,7 +186,10 @@ export class JunoClient {
public async getPublicGalleryData(): Promise<IJunoResponse<IPublicGalleryData>> {
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;
if (response.status === HttpStatusCodes.OK) {
@@ -219,7 +222,10 @@ export class JunoClient {
public async isCodeOfConductAccepted(): Promise<IJunoResponse<boolean>> {
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;
if (response.status === HttpStatusCodes.OK) {
@@ -277,7 +283,7 @@ export class JunoClient {
}
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",
headers: JunoClient.getHeaders(),
});
@@ -311,7 +317,7 @@ export class JunoClient {
}
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",
headers: JunoClient.getHeaders(),
});
@@ -328,19 +334,19 @@ export class JunoClient {
}
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(),
});
}
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(),
});
}
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",
headers: JunoClient.getHeaders(),
});
@@ -495,8 +501,12 @@ export class JunoClient {
return userContext.subscriptionId;
}
private getNotebooksAccountUrl(): string {
return `${this.getNotebooksUrl()}/${this.getAccount()}`;
}
private getNotebooksSubscriptionIdAccountUrl(): string {
return `${this.getNotebooksUrl()}/subscriptions/${this.getSubscriptionId()}/databaseAccounts/${this.getAccount()}`;
return `${this.getNotebooksUrl()}/${this.getSubscriptionId()}/${this.getAccount()}`;
}
private getAnalyticsUrl(): string {

View File

@@ -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": ""
}
}
}

View File

@@ -14,7 +14,6 @@ import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "./Explorer/Panes/PanelComponent.less";
import "../less/TableStyles/queryBuilder.less";
import "../externals/jquery.dataTables.min.css";
import "../less/TableStyles/fulldatatables.less";
@@ -65,9 +64,7 @@ import arrowLeftImg from "../images/imgarrowlefticon.svg";
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { useSidePanel } from "./hooks/useSidePanel";
import { NotificationConsoleComponent } from "./Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { PanelContainerComponent } from "./Explorer/Panes/PanelContainerComponent";
initializeIcons();
@@ -76,15 +73,10 @@ const App: React.FunctionComponent = () => {
const [notificationConsoleData, setNotificationConsoleData] = useState(undefined);
//TODO: Refactor so we don't need to pass the id to remove a console data
const [inProgressConsoleDataIdToBeDeleted, setInProgressConsoleDataIdToBeDeleted] = useState("");
const { isPanelOpen, panelContent, headerText, openSidePanel, closeSidePanel } = useSidePanel();
const explorerParams: ExplorerParams = {
setIsNotificationConsoleExpanded,
setNotificationConsoleData,
setInProgressConsoleDataIdToBeDeleted,
openSidePanel,
closeSidePanel,
};
const config = useConfig();
useKnockoutExplorer(config, explorerParams);
@@ -317,13 +309,6 @@ const App: React.FunctionComponent = () => {
</div>
</div>
{/* Global loader - End */}
<PanelContainerComponent
isOpen={isPanelOpen}
panelContent={panelContent}
headerText={headerText}
closePanel={closeSidePanel}
isConsoleExpanded={isNotificationConsoleExpanded}
/>
<div data-bind="react:uploadItemsPaneAdapter" />
<div data-bind='component: { name: "add-database-pane", params: {data: addDatabasePane} }' />
<div data-bind='component: { name: "add-collection-pane", params: { data: addCollectionPane} }' />

View File

@@ -288,9 +288,11 @@ export class TabRouteHandler {
private _openSprocTabForResource(databaseId: string, collectionId: string, sprocId: string): void {
this._executeActionHelper(() => {
const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId);
collection && collection.expandCollection();
const storedProcedure = collection && collection.findStoredProcedureWithId(sprocId);
storedProcedure && storedProcedure.open();
collection &&
collection.expandCollection().then(() => {
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 {
this._executeActionHelper(() => {
const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId);
collection && collection.expandCollection();
const trigger = collection && collection.findTriggerWithId(triggerId);
trigger && trigger.open();
collection &&
collection.expandCollection().then(() => {
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 {
this._executeActionHelper(() => {
const collection: ViewModels.Collection = this._findMatchingCollectionForResource(databaseId, collectionId);
collection && collection.expandCollection();
const userDefinedFunction = collection && collection.findUserDefinedFunctionWithId(udfId);
userDefinedFunction && userDefinedFunction.open();
collection &&
collection.expandCollection().then(() => {
const userDefinedFunction = collection && collection.findUserDefinedFunctionWithId(udfId);
userDefinedFunction && userDefinedFunction.open();
});
});
}

View File

@@ -8,7 +8,7 @@ interface Decorator {
}
interface InputOptionsBase {
labelTKey: string;
label: string;
}
export interface NumberInputOptions extends InputOptionsBase {
@@ -19,17 +19,17 @@ export interface NumberInputOptions extends InputOptionsBase {
}
export interface StringInputOptions extends InputOptionsBase {
placeholderTKey?: (() => Promise<string>) | string;
placeholder?: (() => Promise<string>) | string;
}
export interface BooleanInputOptions extends InputOptionsBase {
trueLabelTKey: (() => Promise<string>) | string;
falseLabelTKey: (() => Promise<string>) | string;
trueLabel: (() => Promise<string>) | string;
falseLabel: (() => Promise<string>) | string;
}
export interface ChoiceInputOptions extends InputOptionsBase {
choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
placeholderTKey?: (() => Promise<string>) | string;
placeholder?: (() => Promise<string>) | string;
}
export interface DescriptionDisplayOptions {
@@ -48,7 +48,7 @@ const isNumberInputOptions = (inputOptions: InputOptions): inputOptions is Numbe
};
const isBooleanInputOptions = (inputOptions: InputOptions): inputOptions is BooleanInputOptions => {
return "trueLabelTKey" in inputOptions;
return "trueLabel" in inputOptions;
};
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 => {
if (isNumberInputOptions(inputOptions)) {
return addToMap(
{ name: "labelTKey", value: inputOptions.labelTKey },
{ name: "label", value: inputOptions.label },
{ name: "min", value: inputOptions.min },
{ name: "max", value: inputOptions.max },
{ name: "step", value: inputOptions.step },
@@ -100,22 +100,22 @@ export const Values = (inputOptions: InputOptions): PropertyDecorator => {
);
} else if (isBooleanInputOptions(inputOptions)) {
return addToMap(
{ name: "labelTKey", value: inputOptions.labelTKey },
{ name: "trueLabelTKey", value: inputOptions.trueLabelTKey },
{ name: "falseLabelTKey", value: inputOptions.falseLabelTKey }
{ name: "label", value: inputOptions.label },
{ name: "trueLabel", value: inputOptions.trueLabel },
{ name: "falseLabel", value: inputOptions.falseLabel }
);
} else if (isChoiceInputOptions(inputOptions)) {
return addToMap(
{ name: "labelTKey", value: inputOptions.labelTKey },
{ name: "placeholderTKey", value: inputOptions.placeholderTKey },
{ name: "label", value: inputOptions.label },
{ name: "placeholder", value: inputOptions.placeholder },
{ name: "choices", value: inputOptions.choices }
);
} else if (isDescriptionDisplayOptions(inputOptions)) {
return addToMap({ name: "description", value: inputOptions.description });
} else {
return addToMap(
{ name: "labelTKey", value: inputOptions.labelTKey },
{ name: "placeholderTKey", value: inputOptions.placeholderTKey }
{ name: "label", value: inputOptions.label },
{ name: "placeholder", value: inputOptions.placeholder }
);
}
};

View File

@@ -16,22 +16,10 @@ export interface InitializeResponse {
dbThroughput: number;
}
export const getMaxCollectionThroughput = async (): Promise<number> => {
export const getMaxThroughput = async (): Promise<number> => {
return 10000;
};
export const getMinCollectionThroughput = async (): Promise<number> => {
return 400;
};
export const getMaxDatabaseThroughput = async (): Promise<number> => {
return 10000;
};
export const getMinDatabaseThroughput = async (): Promise<number> => {
return 400;
};
export const update = async (
regions: Regions,
enableLogging: boolean,
@@ -71,6 +59,6 @@ export const onRefreshSelfServeExample = async (): Promise<RefreshResult> => {
const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded";
return {
isUpdateInProgress: isUpdateInProgress,
notificationMessage: "RefreshMessage",
notificationMessage: "Self Serve Example successfully refreshing",
};
};

View File

@@ -10,16 +10,7 @@ import {
SelfServeNotificationType,
SmartUiInput,
} from "../SelfServeTypes";
import {
onRefreshSelfServeExample,
Regions,
update,
initialize,
getMinDatabaseThroughput,
getMaxDatabaseThroughput,
getMinCollectionThroughput,
getMaxCollectionThroughput,
} from "./SelfServeExample.rp";
import { onRefreshSelfServeExample, getMaxThroughput, Regions, update, initialize } from "./SelfServeExample.rp";
const regionDropdownItems: ChoiceItem[] = [
{ label: "North Central US", key: Regions.NorthCentralUS },
@@ -28,11 +19,11 @@ const regionDropdownItems: ChoiceItem[] = [
];
const selfServeExampleInfo: Info = {
messageTKey: "ClassInfo",
message: "This is a self serve class",
};
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> => {
@@ -59,7 +50,7 @@ const onEnableDbLevelThroughputChange = (
const validate = (currentvalues: Map<string, SmartUiInput>): void => {
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'
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;
dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined;
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({
description: {
textTKey: "DescriptionText",
text: "This class sets collection and database throughput.",
link: {
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"
*/
@OnChange(onRegionsChange)
@Values({ labelTKey: "Regions", choices: regionDropdownItems, placeholderTKey: "RegionsPlaceholder" })
@Values({ label: "Regions", choices: regionDropdownItems, placeholder: "Select a region" })
regions: ChoiceItem;
@Values({
labelTKey: "Enable Logging",
trueLabelTKey: "Enable",
falseLabelTKey: "Disable",
label: "Enable Logging",
trueLabel: "Enable",
falseLabel: "Disable",
})
enableLogging: boolean;
@Values({
labelTKey: "Account Name",
placeholderTKey: "AccountNamePlaceHolder",
label: "Account Name",
placeholder: "Enter the account name",
})
accountName: string;
@Values({
labelTKey: "Collection Throughput",
min: getMinCollectionThroughput,
max: getMaxCollectionThroughput,
label: "Collection Throughput",
min: 400,
max: getMaxThroughput,
step: 100,
uiType: NumberUiType.Spinner,
})
@@ -236,16 +224,16 @@ export default class SelfServeExample extends SelfServeBaseClass {
*/
@OnChange(onEnableDbLevelThroughputChange)
@Values({
labelTKey: "Enable DB level throughput",
trueLabelTKey: "Enable",
falseLabelTKey: "Disable",
label: "Enable DB level throughput",
trueLabel: "Enable",
falseLabel: "Disable",
})
enableDbLevelThroughput: boolean;
@Values({
labelTKey: "Database Throughput",
min: getMinDatabaseThroughput,
max: getMaxDatabaseThroughput,
label: "Database Throughput",
min: 400,
max: getMaxThroughput,
step: 100,
uiType: NumberUiType.Slider,
})

View File

@@ -34,17 +34,17 @@ describe("SelfServeComponent", () => {
root: {
id: "root",
info: {
messageTKey: "Start at $24/mo per database",
message: "Start at $24/mo per database",
link: {
href: "https://aka.ms/azure-cosmos-db-pricing",
textTKey: "More Details",
text: "More Details",
},
},
children: [
{
id: "throughput",
input: {
labelTKey: "Throughput (input)",
label: "Throughput (input)",
dataFieldName: "throughput",
type: "number",
min: 400,
@@ -57,7 +57,7 @@ describe("SelfServeComponent", () => {
{
id: "containerId",
input: {
labelTKey: "Container id",
label: "Container id",
dataFieldName: "containerId",
type: "string",
},
@@ -65,9 +65,9 @@ describe("SelfServeComponent", () => {
{
id: "analyticalStore",
input: {
labelTKey: "Analytical Store",
trueLabelTKey: "Enabled",
falseLabelTKey: "Disabled",
label: "Analytical Store",
trueLabel: "Enabled",
falseLabel: "Disabled",
defaultValue: true,
dataFieldName: "analyticalStore",
type: "boolean",
@@ -76,7 +76,7 @@ describe("SelfServeComponent", () => {
{
id: "database",
input: {
labelTKey: "Database",
label: "Database",
dataFieldName: "database",
type: "object",
choices: [

View File

@@ -26,9 +26,6 @@ import {
} from "./SelfServeTypes";
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
import { getMessageBarType } from "./SelfServeUtils";
import { Translation } from "react-i18next";
import { TFunction } from "i18next";
import "../i18n";
export interface SelfServeComponentProps {
descriptor: SelfServeDescriptor;
@@ -46,8 +43,6 @@ export interface SelfServeComponentState {
}
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
private smartUiGeneratorClassName: string;
componentDidMount(): void {
this.performRefresh();
this.initializeSmartUiComponent();
@@ -65,7 +60,6 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
notification: undefined,
refreshResult: undefined,
};
this.smartUiGeneratorClassName = this.props.descriptor.root.id;
}
private onError = (hasErrors: boolean): void => {
@@ -153,8 +147,8 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
currentValues: Map<string, SmartUiInput>,
baselineValues: Map<string, SmartUiInput>
): Promise<AnyDisplay> => {
input.labelTKey = await this.getResolvedValue(input.labelTKey);
input.placeholderTKey = await this.getResolvedValue(input.placeholderTKey);
input.label = await this.getResolvedValue(input.label);
input.placeholder = await this.getResolvedValue(input.placeholder);
switch (input.type) {
case "string": {
@@ -183,8 +177,8 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
}
case "boolean": {
const booleanInput = input as BooleanInput;
booleanInput.trueLabelTKey = await this.getResolvedValue(booleanInput.trueLabelTKey);
booleanInput.falseLabelTKey = await this.getResolvedValue(booleanInput.falseLabelTKey);
booleanInput.trueLabel = await this.getResolvedValue(booleanInput.trueLabel);
booleanInput.falseLabel = await this.getResolvedValue(booleanInput.falseLabel);
return booleanInput;
}
default: {
@@ -220,7 +214,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
onSavePromise.catch((error) => {
this.setState({
notification: {
message: `${error.message}`,
message: `Error: ${error.message}`,
type: SelfServeNotificationType.error,
},
});
@@ -279,15 +273,11 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
this.setState({ isInitializing: false });
};
public getCommonTranslation = (translationFunction: TFunction, key: string): string => {
return translationFunction(`Common.${key}`);
};
private getCommandBarItems = (translate: TFunction): ICommandBarItemProps[] => {
private getCommandBarItems = (): ICommandBarItemProps[] => {
return [
{
key: "save",
text: this.getCommonTranslation(translate, "Save"),
text: "Save",
iconProps: { iconName: "Save" },
split: true,
disabled: this.isSaveButtonDisabled(),
@@ -295,7 +285,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
},
{
key: "discard",
text: this.getCommonTranslation(translate, "Discard"),
text: "Discard",
iconProps: { iconName: "Undo" },
split: true,
disabled: this.isDiscardButtonDisabled(),
@@ -305,7 +295,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
},
{
key: "refresh",
text: this.getCommonTranslation(translate, "Refresh"),
text: "Refresh",
disabled: this.state.isInitializing,
iconProps: { iconName: "Refresh" },
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 {
const containerStackTokens: IStackTokens = { childrenGap: 5 };
if (this.state.compileErrorMessage) {
return <MessageBar messageBarType={MessageBarType.error}>{this.state.compileErrorMessage}</MessageBar>;
}
return (
<Translation>
{(translate) => {
const getTranslation = (key: string): string => {
return translate(`${this.smartUiGeneratorClassName}.${key}`);
};
return (
<div style={{ overflowX: "auto" }}>
<Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}>
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems(translate)} />
{this.state.isInitializing ? (
<Spinner
size={SpinnerSize.large}
styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }}
/>
) : (
<>
{this.state.refreshResult?.isUpdateInProgress && (
<MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}>
{getTranslation(this.state.refreshResult.notificationMessage)}
</MessageBar>
)}
{this.state.notification && (
<MessageBar
messageBarType={getMessageBarType(this.state.notification.type)}
styles={{ root: { width: 400 } }}
onDismiss={() => this.setState({ notification: undefined })}
>
{this.getNotificationMessageTranslation(getTranslation, this.state.notification.message)}
</MessageBar>
)}
<SmartUiComponent
disabled={this.state.refreshResult?.isUpdateInProgress}
descriptor={this.state.root as SmartUiDescriptor}
currentValues={this.state.currentValues}
onInputChange={this.onInputChange}
onError={this.onError}
getTranslation={getTranslation}
/>
</>
)}
</Stack>
</div>
);
}}
</Translation>
<div style={{ overflowX: "auto" }}>
<Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}>
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems()} />
{this.state.isInitializing ? (
<Spinner
size={SpinnerSize.large}
styles={{ root: { textAlign: "center", justifyContent: "center", width: "100%", height: "100%" } }}
/>
) : (
<>
{this.state.refreshResult?.isUpdateInProgress && (
<MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}>
{this.state.refreshResult.notificationMessage}
</MessageBar>
)}
{this.state.notification && (
<MessageBar
messageBarType={getMessageBarType(this.state.notification.type)}
styles={{ root: { width: 400 } }}
onDismiss={() => this.setState({ notification: undefined })}
>
{this.state.notification.message}
</MessageBar>
)}
<SmartUiComponent
disabled={this.state.refreshResult?.isUpdateInProgress}
descriptor={this.state.root as SmartUiDescriptor}
currentValues={this.state.currentValues}
onInputChange={this.onInputChange}
onError={this.onError}
/>
</>
)}
</Stack>
</div>
);
}
}

View File

@@ -2,9 +2,9 @@ interface BaseInput {
dataFieldName: string;
errorMessage?: string;
type: InputTypeValue;
labelTKey?: (() => Promise<string>) | string;
label?: (() => Promise<string>) | string;
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>;
placeholderTKey?: (() => Promise<string>) | string;
placeholder?: (() => Promise<string>) | string;
}
export interface NumberInput extends BaseInput {
@@ -16,8 +16,8 @@ export interface NumberInput extends BaseInput {
}
export interface BooleanInput extends BaseInput {
trueLabelTKey: (() => Promise<string>) | string;
falseLabelTKey: (() => Promise<string>) | string;
trueLabel: (() => Promise<string>) | string;
falseLabel: (() => Promise<string>) | string;
defaultValue?: boolean;
}
@@ -92,18 +92,18 @@ export type ChoiceItem = { label: string; key: string };
export type InputType = number | string | boolean | ChoiceItem;
export interface Info {
messageTKey: string;
message: string;
link?: {
href: string;
textTKey: string;
text: string;
};
}
export interface Description {
textTKey: string;
text: string;
link?: {
href: string;
textTKey: string;
text: string;
};
}

View File

@@ -58,7 +58,7 @@ describe("SelfServeUtils", () => {
id: "dbThroughput",
dataFieldName: "dbThroughput",
type: "number",
labelTKey: "Database Throughput",
label: "Database Throughput",
min: 1,
max: 5,
step: 1,
@@ -71,7 +71,7 @@ describe("SelfServeUtils", () => {
id: "collThroughput",
dataFieldName: "collThroughput",
type: "number",
labelTKey: "Coll Throughput",
label: "Coll Throughput",
min: 1,
max: 5,
step: 1,
@@ -84,7 +84,7 @@ describe("SelfServeUtils", () => {
id: "invalidThroughput",
dataFieldName: "invalidThroughput",
type: "boolean",
labelTKey: "Invalid Coll Throughput",
label: "Invalid Coll Throughput",
min: 1,
max: 5,
step: 1,
@@ -98,8 +98,8 @@ describe("SelfServeUtils", () => {
id: "collName",
dataFieldName: "collName",
type: "string",
labelTKey: "Coll Name",
placeholderTKey: "placeholder text",
label: "Coll Name",
placeholder: "placeholder text",
},
],
[
@@ -108,9 +108,9 @@ describe("SelfServeUtils", () => {
id: "enableLogging",
dataFieldName: "enableLogging",
type: "boolean",
labelTKey: "Enable Logging",
trueLabelTKey: "Enable",
falseLabelTKey: "Disable",
label: "Enable Logging",
trueLabel: "Enable",
falseLabel: "Disable",
},
],
[
@@ -119,8 +119,8 @@ describe("SelfServeUtils", () => {
id: "invalidEnableLogging",
dataFieldName: "invalidEnableLogging",
type: "boolean",
labelTKey: "Invalid Enable Logging",
placeholderTKey: "placeholder text",
label: "Invalid Enable Logging",
placeholder: "placeholder text",
},
],
[
@@ -129,7 +129,7 @@ describe("SelfServeUtils", () => {
id: "regions",
dataFieldName: "regions",
type: "object",
labelTKey: "Regions",
label: "Regions",
choices: [
{ label: "South West US", key: "SWUS" },
{ label: "North Central US", key: "NCUS" },
@@ -143,14 +143,14 @@ describe("SelfServeUtils", () => {
id: "invalidRegions",
dataFieldName: "invalidRegions",
type: "object",
labelTKey: "Invalid Regions",
placeholderTKey: "placeholder text",
label: "Invalid Regions",
placeholder: "placeholder text",
},
],
]);
const expectedDescriptor = {
root: {
id: "TestClass",
id: "root",
children: [
{
id: "dbThroughput",
@@ -158,7 +158,7 @@ describe("SelfServeUtils", () => {
id: "dbThroughput",
dataFieldName: "dbThroughput",
type: "number",
labelTKey: "Database Throughput",
label: "Database Throughput",
min: 1,
max: 5,
step: 1,
@@ -172,7 +172,7 @@ describe("SelfServeUtils", () => {
id: "collThroughput",
dataFieldName: "collThroughput",
type: "number",
labelTKey: "Coll Throughput",
label: "Coll Throughput",
min: 1,
max: 5,
step: 1,
@@ -186,7 +186,7 @@ describe("SelfServeUtils", () => {
id: "invalidThroughput",
dataFieldName: "invalidThroughput",
type: "boolean",
labelTKey: "Invalid Coll Throughput",
label: "Invalid Coll Throughput",
min: 1,
max: 5,
step: 1,
@@ -201,8 +201,8 @@ describe("SelfServeUtils", () => {
id: "collName",
dataFieldName: "collName",
type: "string",
labelTKey: "Coll Name",
placeholderTKey: "placeholder text",
label: "Coll Name",
placeholder: "placeholder text",
},
children: [] as Node[],
},
@@ -212,9 +212,9 @@ describe("SelfServeUtils", () => {
id: "enableLogging",
dataFieldName: "enableLogging",
type: "boolean",
labelTKey: "Enable Logging",
trueLabelTKey: "Enable",
falseLabelTKey: "Disable",
label: "Enable Logging",
trueLabel: "Enable",
falseLabel: "Disable",
},
children: [] as Node[],
},
@@ -224,8 +224,8 @@ describe("SelfServeUtils", () => {
id: "invalidEnableLogging",
dataFieldName: "invalidEnableLogging",
type: "boolean",
labelTKey: "Invalid Enable Logging",
placeholderTKey: "placeholder text",
label: "Invalid Enable Logging",
placeholder: "placeholder text",
errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidEnableLogging'.",
},
children: [] as Node[],
@@ -236,7 +236,7 @@ describe("SelfServeUtils", () => {
id: "regions",
dataFieldName: "regions",
type: "object",
labelTKey: "Regions",
label: "Regions",
choices: [
{ label: "South West US", key: "SWUS" },
{ label: "North Central US", key: "NCUS" },
@@ -251,8 +251,8 @@ describe("SelfServeUtils", () => {
id: "invalidRegions",
dataFieldName: "invalidRegions",
type: "object",
labelTKey: "Invalid Regions",
placeholderTKey: "placeholder text",
label: "Invalid Regions",
placeholder: "placeholder text",
errorMessage: "label and choices are required for Choice input 'invalidRegions'.",
},
children: [] as Node[],
@@ -270,7 +270,7 @@ describe("SelfServeUtils", () => {
"invalidRegions",
],
};
const descriptor = mapToSmartUiDescriptor("TestClass", context);
const descriptor = mapToSmartUiDescriptor(context);
expect(descriptor).toEqual(expectedDescriptor);
});
});

View File

@@ -32,14 +32,14 @@ export interface DecoratorProperties {
id: string;
info?: (() => Promise<Info>) | Info;
type?: InputTypeValue;
labelTKey?: (() => Promise<string>) | string;
placeholderTKey?: (() => Promise<string>) | string;
label?: (() => Promise<string>) | string;
placeholder?: (() => Promise<string>) | string;
dataFieldName?: string;
min?: (() => Promise<number>) | number;
max?: (() => Promise<number>) | number;
step?: (() => Promise<number>) | number;
trueLabelTKey?: (() => Promise<string>) | string;
falseLabelTKey?: (() => Promise<string>) | string;
trueLabel?: (() => Promise<string>) | string;
falseLabel?: (() => Promise<string>) | string;
choices?: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
uiType?: string;
errorMessage?: string;
@@ -100,21 +100,18 @@ export const updateContextWithDecorator = <T extends keyof DecoratorProperties,
export const buildSmartUiDescriptor = (className: string, target: unknown): void => {
const context = Reflect.getMetadata(className, target) as Map<string, DecoratorProperties>;
const smartUiDescriptor = mapToSmartUiDescriptor(className, context);
const smartUiDescriptor = mapToSmartUiDescriptor(context);
Reflect.defineMetadata(className, smartUiDescriptor, target);
};
export const mapToSmartUiDescriptor = (
className: string,
context: Map<string, DecoratorProperties>
): SelfServeDescriptor => {
export const mapToSmartUiDescriptor = (context: Map<string, DecoratorProperties>): SelfServeDescriptor => {
const root = context.get("root");
context.delete("root");
const inputNames: string[] = [];
const smartUiDescriptor: SelfServeDescriptor = {
root: {
id: className,
id: "root",
info: root?.info,
children: [],
},
@@ -150,7 +147,7 @@ const addToDescriptor = (
const getInput = (value: DecoratorProperties): AnyDisplay => {
switch (value.type) {
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}'.`;
}
return value as NumberInput;
@@ -158,17 +155,17 @@ const getInput = (value: DecoratorProperties): AnyDisplay => {
if (value.description) {
return value as DescriptionDisplay;
}
if (!value.labelTKey) {
if (!value.label) {
value.errorMessage = `label is required for string input '${value.id}'.`;
}
return value as StringInput;
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}'.`;
}
return value as BooleanInput;
default:
if (!value.labelTKey || !value.choices) {
if (!value.label || !value.choices) {
value.errorMessage = `label and choices are required for Choice input '${value.id}'.`;
}
return value as ChoiceInput;

View File

@@ -1,78 +1,33 @@
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 {
sku: string;
instances: number;
status: string;
endpoint: string;
}
export const getPath = (subscriptionId: string, resourceGroup: string, name: string): string => {
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/sqlx`;
export const getRegionSpecificMinInstances = async (): Promise<number> => {
// TODO: write RP call to get min number of instances needed for this region
throw new Error("getRegionSpecificMinInstances not implemented");
};
export const updateDedicatedGatewayResource = async (sku: string, instances: number): Promise<void> => {
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
const body: UpdateDedicatedGatewayRequestParameters = {
properties: {
instanceSize: sku,
instanceCount: instances,
serviceType: "Sqlx",
},
};
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
export const getRegionSpecificMaxInstances = async (): Promise<number> => {
// TODO: write RP call to get max number of instances needed for this region
throw new Error("getRegionSpecificMaxInstances not implemented");
};
export const deleteDedicatedGatewayResource = async (): Promise<void> => {
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "DELETE", apiVersion });
export const updateDedicatedGatewayProvisioning = async (sku: string, instances: number): Promise<void> => {
// TODO: write RP call to update dedicated gateway provisioning
throw new Error(
`updateDedicatedGatewayProvisioning not implemented. Parameters- sku: ${sku}, instances:${instances}`
);
};
export const getDedicatedGatewayResource = async (): Promise<SqlxServiceResource> => {
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
return armRequest<SqlxServiceResource>({ host: configContext.ARM_ENDPOINT, path, method: "GET", apiVersion });
};
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 initializeDedicatedGatewayProvisioning = async (): Promise<DedicatedGatewayResponse> => {
// TODO: write RP call to initialize UI for dedicated gateway provisioning
throw new Error("initializeDedicatedGatewayProvisioning not implemented");
};
export const refreshDedicatedGatewayProvisioning = async (): Promise<RefreshResult> => {
try {
const response = await getDedicatedGatewayResource();
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 };
}
// TODO: write RP call to check if dedicated gateway update has gone through
throw new Error("refreshDedicatedGatewayProvisioning not implemented");
};

View File

@@ -6,18 +6,9 @@ import {
RefreshResult,
SelfServeBaseClass,
SelfServeNotification,
SelfServeNotificationType,
SmartUiInput,
} from "../SelfServeTypes";
import {
ResourceStatus,
refreshDedicatedGatewayProvisioning,
updateDedicatedGatewayResource,
deleteDedicatedGatewayResource,
getCurrentProvisioningState,
} from "./SqlX.rp";
let disableAttributesOnDedicatedGatewayChange = false;
import { refreshDedicatedGatewayProvisioning } from "./SqlX.rp";
const onEnableDedicatedGatewayChange = (
currentState: Map<string, SmartUiInput>,
@@ -25,38 +16,31 @@ const onEnableDedicatedGatewayChange = (
): Map<string, SmartUiInput> => {
const sku = currentState.get("sku");
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("sku", {
value: sku.value,
hidden: hideAttributes,
disabled: disableAttributesOnDedicatedGatewayChange,
});
currentState.set("instances", {
value: instances.value,
hidden: hideAttributes,
disabled: disableAttributesOnDedicatedGatewayChange,
});
currentState.set("sku", { value: sku.value, hidden: isSkuHidden });
currentState.set("instances", { value: instances.value, hidden: isSkuHidden });
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[]> => {
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> => {
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> => {
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()
@@ -66,80 +50,22 @@ export default class SqlX extends SelfServeBaseClass {
};
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<SelfServeNotification> => {
const response = await getCurrentProvisioningState();
// null implies the resource has not been provisioned.
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 };
validate(currentValues);
// TODO: add pre processing logic before calling the updateDedicatedGatewayProvisioning() RP call.
throw new Error(`onSave not implemented. No. of properties to save: ${currentValues.size}`);
};
public initialize = async (): Promise<Map<string, SmartUiInput>> => {
// Based on the RP call enableDedicatedGateway will be true if it has not yet been enabled and false if it has.
const defaults = new Map<string, SmartUiInput>();
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;
// TODO: get initialization data from initializeDedicatedGatewayProvisioning() RP call.
throw new Error("onSave not implemented");
};
@Values({
description: {
textTKey: "DedicatedGatewayDescription",
text: "Provisioning dedicated gateways for SqlX accounts.",
link: {
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)
@Values({
labelTKey: "DedicatedGateway",
trueLabelTKey: "Enable",
falseLabelTKey: "Disable",
label: "Dedicated Gateway",
trueLabel: "Enable",
falseLabel: "Disable",
})
enableDedicatedGateway: boolean;
@Values({
labelTKey: "SKUs",
label: "SKUs",
choices: getSkus,
placeholderTKey: "Select SKUs",
placeholder: "Select SKUs",
})
sku: ChoiceItem;
@Values({
labelTKey: "NumberOfInstances",
label: "Number of instances",
min: getInstancesMin,
max: getInstancesMax,
step: 1,

View File

@@ -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