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