merge master branch

This commit is contained in:
hardiknai-techm
2021-04-29 08:35:26 +05:30
153 changed files with 40742 additions and 3573 deletions

View File

@@ -20,51 +20,6 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("json-editor")).toBe(true);
});
it("should register documents-tab component", () => {
expect(ko.components.isRegistered("documents-tab")).toBe(true);
});
it("should register stored-procedure-tab component", () => {
expect(ko.components.isRegistered("stored-procedure-tab")).toBe(true);
});
it("should register trigger-tab component", () => {
expect(ko.components.isRegistered("trigger-tab")).toBe(true);
});
it("should register user-defined-function-tab component", () => {
expect(ko.components.isRegistered("user-defined-function-tab")).toBe(true);
});
it("should register settings-tab-v2 component", () => {
expect(ko.components.isRegistered("database-settings-tab-v2")).toBe(true);
expect(ko.components.isRegistered("collection-settings-tab-v2")).toBe(true);
});
it("should register query-tab component", () => {
expect(ko.components.isRegistered("query-tab")).toBe(true);
});
it("should register tables-query-tab component", () => {
expect(ko.components.isRegistered("tables-query-tab")).toBe(true);
});
it("should register graph-tab component", () => {
expect(ko.components.isRegistered("graph-tab")).toBe(true);
});
it("should register notebookv2-tab component", () => {
expect(ko.components.isRegistered("notebookv2-tab")).toBe(true);
});
it("should register terminal-tab component", () => {
expect(ko.components.isRegistered("terminal-tab")).toBe(true);
});
it("should register mongo-shell-tab component", () => {
expect(ko.components.isRegistered("mongo-shell-tab")).toBe(true);
});
it("should registeradd-collection-pane component", () => {
expect(ko.components.isRegistered("add-collection-pane")).toBe(true);
});
@@ -73,10 +28,6 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("graph-styling-pane")).toBe(true);
});
it("should register string-input-pane component", () => {
expect(ko.components.isRegistered("string-input-pane")).toBe(true);
});
it("should register dynamic-list component", () => {
expect(ko.components.isRegistered("dynamic-list")).toBe(true);
});

View File

@@ -8,21 +8,6 @@ import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent";
import * as PaneComponents from "./Panes/PaneComponents";
import ConflictsTab from "./Tabs/ConflictsTab";
import DocumentsTab from "./Tabs/DocumentsTab";
import GalleryTab from "./Tabs/GalleryTab";
import GraphTab from "./Tabs/GraphTab";
import MongoShellTab from "./Tabs/MongoShellTab";
import NotebookTabV2 from "./Tabs/NotebookV2Tab";
import NotebookViewerTab from "./Tabs/NotebookViewerTab";
import QueryTab from "./Tabs/QueryTab";
import QueryTablesTab from "./Tabs/QueryTablesTab";
import SchemaAnalyzerTab from "./Tabs/SchemaAnalyzerTab";
import { DatabaseSettingsTabV2, SettingsTabV2 } from "./Tabs/SettingsTabV2";
import StoredProcedureTab from "./Tabs/StoredProcedureTab";
import TerminalTab from "./Tabs/TerminalTab";
import TriggerTab from "./Tabs/TriggerTab";
import UserDefinedFunctionTab from "./Tabs/UserDefinedFunctionTab";
ko.components.register("input-typeahead", new InputTypeaheadComponent());
ko.components.register("error-display", new ErrorDisplayComponent());
@@ -33,26 +18,6 @@ ko.components.register("diff-editor", new DiffEditorComponent());
ko.components.register("dynamic-list", DynamicListComponent);
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
// Collection Tabs
[
DocumentsTab,
StoredProcedureTab,
TriggerTab,
UserDefinedFunctionTab,
SettingsTabV2,
QueryTab,
QueryTablesTab,
GraphTab,
MongoShellTab,
ConflictsTab,
NotebookTabV2,
TerminalTab,
GalleryTab,
NotebookViewerTab,
DatabaseSettingsTabV2,
SchemaAnalyzerTab,
].forEach(({ component: { name, template } }) => ko.components.register(name, { template }));
// Panes
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
@@ -60,5 +25,4 @@ ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPane
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent());
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent());
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());

View File

@@ -10,7 +10,6 @@ import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
import * as ViewModels from "../Contracts/ViewModels";
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
import { userContext } from "../UserContext";
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
import Explorer from "./Explorer";
@@ -39,7 +38,7 @@ export class ResourceTreeContextMenuButtonFactory {
},
];
if (userContext.defaultExperience !== DefaultAccountExperienceType.Table) {
if (userContext.apiType !== "Tables") {
items.push({
iconSrc: DeleteDatabaseIcon,
onClick: () => container.openDeleteDatabaseConfirmationPane(),
@@ -63,7 +62,7 @@ export class ResourceTreeContextMenuButtonFactory {
});
}
if (container.isPreferredApiMongoDB()) {
if (userContext.apiType === "Mongo") {
items.push({
iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null),

View File

@@ -1,6 +1,6 @@
import * as ViewModels from "../../../Contracts/ViewModels";
import { loadMonaco, monaco } from "../../LazyMonaco";
import template from "./diff-editor-component.html";
import * as monaco from "monaco-editor";
/**
* Helper class for ko component registration
@@ -92,7 +92,7 @@ export class DiffEditorViewModel {
/**
* Create the monaco editor on diff mode and attach to DOM
*/
protected createDiffEditor(
protected async createDiffEditor(
originalContent: string,
modifiedContent: string,
createCallback: (e: monaco.editor.IStandaloneDiffEditor) => void
@@ -111,7 +111,7 @@ export class DiffEditorViewModel {
}
const language = this.params.editorLanguage || "json";
const monaco = await loadMonaco();
const originalModel = monaco.editor.createModel(originalContent, language);
const modifiedModel = monaco.editor.createModel(modifiedContent, language);
const diffEditor: monaco.editor.IStandaloneDiffEditor = monaco.editor.createDiffEditor(

View File

@@ -1,7 +1,6 @@
import { loadMonaco, monaco } from "../../LazyMonaco";
import { JsonEditorParams, JsonEditorViewModel } from "../JsonEditor/JsonEditorComponent";
import template from "./editor-component.html";
import * as monaco from "monaco-editor";
import { SqlCompletionItemProvider, ErrorMarkProvider } from "@azure/cosmos-language-service";
/**
* Helper class for ko component registration
@@ -49,15 +48,17 @@ class EditorViewModel extends JsonEditorViewModel {
return this.params.contentType;
}
protected registerCompletionItemProvider() {
let sqlCompletionItemProvider = new SqlCompletionItemProvider();
protected async registerCompletionItemProvider() {
if (EditorViewModel.providerRegistered.indexOf("sql") < 0) {
monaco.languages.registerCompletionItemProvider("sql", sqlCompletionItemProvider);
const { SqlCompletionItemProvider } = await import("@azure/cosmos-language-service");
const monaco = await loadMonaco();
monaco.languages.registerCompletionItemProvider("sql", new SqlCompletionItemProvider());
EditorViewModel.providerRegistered.push("sql");
}
}
protected getErrorMarkers(input: string): Q.Promise<monaco.editor.IMarkerData[]> {
protected async getErrorMarkers(input: string): Promise<monaco.editor.IMarkerData[]> {
const { ErrorMarkProvider } = await import("@azure/cosmos-language-service");
return ErrorMarkProvider.getErrorMark(input);
}
}

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as monaco from "monaco-editor";
import { loadMonaco, monaco } from "../../LazyMonaco";
export interface EditorReactProps {
language: string;
@@ -61,7 +61,7 @@ export class EditorReact extends React.Component<EditorReactProps> {
/**
* Create the monaco editor and attach to DOM
*/
private createEditor(createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
private async createEditor(createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
const options: monaco.editor.IEditorConstructionOptions = {
value: this.props.content,
language: this.props.language,
@@ -74,6 +74,7 @@ export class EditorReact extends React.Component<EditorReactProps> {
};
this.rootNode.innerHTML = "";
const monaco = await loadMonaco();
createCallback(monaco.editor.create(this.rootNode, options));
}

View File

@@ -128,21 +128,21 @@ class InputTypeaheadViewModel {
},
},
callback: {
onClick: (node: any, a: any, item: OnClickItem, event: any) => {
onClick: (_node: unknown, _a: unknown, item: OnClickItem) => {
cache.selection = item;
if (params.selection) {
params.selection(item);
}
},
onResult(node: any, query: any, result: any, resultCount: any, resultCountPerGroup: any) {
onResult(_node: unknown, query: any) {
cache.inputValue = query;
if (params.inputValue) {
params.inputValue(query);
}
},
},
template: (query: string, item: any) => {
template: (_query: string, item: any) => {
// Don't display id if caption *IS* the id
return item.caption === item.value
? "<span>{{caption}}</span>"

View File

@@ -1,6 +1,5 @@
import Q from "q";
import * as monaco from "monaco-editor";
import * as ViewModels from "../../../Contracts/ViewModels";
import { loadMonaco, monaco } from "../../LazyMonaco";
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
import template from "./json-editor-component.html";
@@ -88,7 +87,7 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
/**
* Create the monaco editor and attach to DOM
*/
protected createEditor(content: string, createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
protected async createEditor(content: string, createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
this.registerCompletionItemProvider();
this.editorContainer = document.getElementById(this.getEditorId());
const options: monaco.editor.IEditorConstructionOptions = {
@@ -102,6 +101,7 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
};
this.editorContainer.innerHTML = "";
const monaco = await loadMonaco();
createCallback(monaco.editor.create(this.editorContainer, options));
}
@@ -109,15 +109,16 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
protected registerCompletionItemProvider() {}
// Interface. Will be implemented in children editor view model such as EditorViewModel.
protected getErrorMarkers(input: string): Q.Promise<monaco.editor.IMarkerData[]> {
return Q.Promise(() => {});
protected async getErrorMarkers(_: string): Promise<monaco.editor.IMarkerData[]> {
return [];
}
protected getEditorLanguage(): string {
return "json";
}
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
protected async configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
const monaco = await loadMonaco();
this.editor = editor;
const queryEditorModel = this.editor.getModel();
if (!this.params.isReadOnly && this.params.updatedContent) {

View File

@@ -1,25 +1,25 @@
import { Card } from "@uifabric/react-cards";
import {
BaseButton,
Button,
FontWeights,
Icon,
IconButton,
Image,
ImageFit,
Persona,
Text,
Link,
BaseButton,
Button,
LinkBase,
Persona,
Separator,
TooltipHost,
Spinner,
SpinnerSize,
Text,
TooltipHost,
} from "office-ui-fabric-react";
import * as React from "react";
import React, { FunctionComponent, useState } from "react";
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
import { IGalleryItem } from "../../../../Juno/JunoClient";
import * as FileSystemUtil from "../../../Notebook/FileSystemUtil";
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
export interface GalleryCardComponentProps {
data: IGalleryItem;
@@ -34,166 +34,48 @@ export interface GalleryCardComponentProps {
onDeleteClick: (beforeDelete: () => void, afterDelete: () => void) => void;
}
interface GalleryCardComponentState {
isDeletingPublishedNotebook: boolean;
}
export const GalleryCardComponent: FunctionComponent<GalleryCardComponentProps> = ({
data,
isFavorite,
showDownload,
showDelete,
onClick,
onTagClick,
onFavoriteClick,
onUnfavoriteClick,
onDownloadClick,
onDeleteClick,
}: GalleryCardComponentProps) => {
const CARD_WIDTH = 256;
const cardImageHeight = 144;
const cardDescriptionMaxChars = 80;
const cardItemGapBig = 10;
const cardItemGapSmall = 8;
const cardDeleteSpinnerHeight = 360;
const smallTextLineHeight = 18;
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps, GalleryCardComponentState> {
public static readonly CARD_WIDTH = 256;
private static readonly cardImageHeight = 144;
public static readonly cardHeightToWidthRatio =
GalleryCardComponent.cardImageHeight / GalleryCardComponent.CARD_WIDTH;
private static readonly cardDescriptionMaxChars = 80;
private static readonly cardItemGapBig = 10;
private static readonly cardItemGapSmall = 8;
private static readonly cardDeleteSpinnerHeight = 360;
private static readonly smallTextLineHeight = 18;
const [isDeletingPublishedNotebook, setIsDeletingPublishedNotebook] = useState<boolean>(false);
constructor(props: GalleryCardComponentProps) {
super(props);
this.state = {
isDeletingPublishedNotebook: false,
};
}
const cardButtonsVisible = isFavorite !== undefined || showDownload || showDelete;
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "short",
day: "numeric",
};
const dateString = new Date(data.created).toLocaleString("default", options);
const cardTitle = FileSystemUtil.stripExtension(data.name, "ipynb");
public render(): JSX.Element {
const cardButtonsVisible = this.props.isFavorite !== undefined || this.props.showDownload || this.props.showDelete;
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "short",
day: "numeric",
};
const dateString = new Date(this.props.data.created).toLocaleString("default", options);
const cardTitle = FileSystemUtil.stripExtension(this.props.data.name, "ipynb");
return (
<Card
style={{ background: "white" }}
aria-label={cardTitle}
data-is-focusable="true"
tokens={{ width: GalleryCardComponent.CARD_WIDTH, childrenGap: 0 }}
onClick={(event) => this.onClick(event, this.props.onClick)}
>
{this.state.isDeletingPublishedNotebook && (
<Card.Item tokens={{ padding: GalleryCardComponent.cardItemGapBig }}>
<Spinner
size={SpinnerSize.large}
label={`Deleting '${cardTitle}'`}
styles={{ root: { height: GalleryCardComponent.cardDeleteSpinnerHeight } }}
/>
</Card.Item>
)}
{!this.state.isDeletingPublishedNotebook && (
<>
<Card.Item tokens={{ padding: GalleryCardComponent.cardItemGapBig }}>
<Persona
imageUrl={this.props.data.isSample && CosmosDBLogo}
text={this.props.data.author}
secondaryText={dateString}
/>
</Card.Item>
<Card.Item>
<Image
src={this.props.data.thumbnailUrl}
width={GalleryCardComponent.CARD_WIDTH}
height={GalleryCardComponent.cardImageHeight}
imageFit={ImageFit.cover}
alt={`${cardTitle} cover image`}
/>
</Card.Item>
<Card.Section styles={{ root: { padding: GalleryCardComponent.cardItemGapBig } }}>
<Text variant="small" nowrap styles={{ root: { height: GalleryCardComponent.smallTextLineHeight } }}>
{this.props.data.tags ? (
this.props.data.tags.map((tag, index, array) => (
<span key={tag}>
<Link onClick={(event) => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link>
{index === array.length - 1 ? <></> : ", "}
</span>
))
) : (
<br />
)}
</Text>
<Text
styles={{
root: {
fontWeight: FontWeights.semibold,
paddingTop: GalleryCardComponent.cardItemGapSmall,
paddingBottom: GalleryCardComponent.cardItemGapSmall,
},
}}
nowrap
>
{cardTitle}
</Text>
<Text variant="small" styles={{ root: { height: GalleryCardComponent.smallTextLineHeight * 2 } }}>
{this.renderTruncatedDescription()}
</Text>
<span>
{this.props.data.views !== undefined &&
this.generateIconText("RedEye", this.props.data.views.toString())}
{this.props.data.downloads !== undefined &&
this.generateIconText("Download", this.props.data.downloads.toString())}
{this.props.data.favorites !== undefined &&
this.generateIconText("Heart", this.props.data.favorites.toString())}
</span>
</Card.Section>
{cardButtonsVisible && (
<Card.Section
styles={{
root: {
marginLeft: GalleryCardComponent.cardItemGapBig,
marginRight: GalleryCardComponent.cardItemGapBig,
},
}}
>
<Separator styles={{ root: { padding: 0, height: 1 } }} />
<span>
{this.props.isFavorite !== undefined &&
this.generateIconButtonWithTooltip(
this.props.isFavorite ? "HeartFill" : "Heart",
this.props.isFavorite ? "Unfavorite" : "Favorite",
"left",
this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick
)}
{this.props.showDownload &&
this.generateIconButtonWithTooltip("Download", "Download", "left", this.props.onDownloadClick)}
{this.props.showDelete &&
this.generateIconButtonWithTooltip("Delete", "Remove", "right", () =>
this.props.onDeleteClick(
() => this.setState({ isDeletingPublishedNotebook: true }),
() => this.setState({ isDeletingPublishedNotebook: false })
)
)}
</span>
</Card.Section>
)}
</>
)}
</Card>
);
}
private renderTruncatedDescription = (): string => {
let truncatedDescription = this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars);
if (this.props.data.description.length > GalleryCardComponent.cardDescriptionMaxChars) {
const renderTruncatedDescription = (): string => {
let truncatedDescription = data.description.substr(0, cardDescriptionMaxChars);
if (data.description.length > cardDescriptionMaxChars) {
truncatedDescription = `${truncatedDescription} ...`;
}
return truncatedDescription;
};
private generateIconText = (iconName: string, text: string): JSX.Element => {
const generateIconText = (iconName: string, text: string): JSX.Element => {
return (
<Text variant="tiny" styles={{ root: { color: "#605E5C", paddingRight: GalleryCardComponent.cardItemGapSmall } }}>
<Text variant="tiny" styles={{ root: { color: "#605E5C", paddingRight: cardItemGapSmall } }}>
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} /> {text}
</Text>
);
@@ -203,7 +85,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
* Fluent UI doesn't support tooltips on IconButtons out of the box. In the meantime the recommendation is
* to do the following (from https://developer.microsoft.com/en-us/fluentui#/controls/web/button)
*/
private generateIconButtonWithTooltip = (
const generateIconButtonWithTooltip = (
iconName: string,
title: string,
horizontalAlign: "right" | "left",
@@ -220,13 +102,13 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
iconProps={{ iconName }}
title={title}
ariaLabel={title}
onClick={(event) => this.onClick(event, activate)}
onClick={(event) => handlerOnClick(event, activate)}
/>
</TooltipHost>
);
};
private onClick = (
const handlerOnClick = (
event:
| React.MouseEvent<HTMLElement | HTMLAnchorElement | HTMLButtonElement | LinkBase, MouseEvent>
| React.MouseEvent<
@@ -239,4 +121,112 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
event.preventDefault();
activate();
};
}
return (
<Card
style={{ background: "white" }}
aria-label={cardTitle}
data-is-focusable="true"
tokens={{ width: CARD_WIDTH, childrenGap: 0 }}
onClick={(event) => handlerOnClick(event, onClick)}
>
{isDeletingPublishedNotebook && (
<Card.Item tokens={{ padding: cardItemGapBig }}>
<Spinner
size={SpinnerSize.large}
label={`Deleting '${cardTitle}'`}
styles={{ root: { height: cardDeleteSpinnerHeight } }}
/>
</Card.Item>
)}
{!isDeletingPublishedNotebook && (
<>
<Card.Item tokens={{ padding: cardItemGapBig }}>
<Persona imageUrl={data.isSample && CosmosDBLogo} text={data.author} secondaryText={dateString} />
</Card.Item>
<Card.Item>
<Image
src={data.thumbnailUrl}
width={CARD_WIDTH}
height={cardImageHeight}
imageFit={ImageFit.cover}
alt={`${cardTitle} cover image`}
/>
</Card.Item>
<Card.Section styles={{ root: { padding: cardItemGapBig } }}>
<Text variant="small" nowrap styles={{ root: { height: smallTextLineHeight } }}>
{data.tags ? (
data.tags.map((tag, index, array) => (
<span key={tag}>
<Link onClick={(event) => handlerOnClick(event, () => onTagClick(tag))}>{tag}</Link>
{index === array.length - 1 ? <></> : ", "}
</span>
))
) : (
<br />
)}
</Text>
<Text
styles={{
root: {
fontWeight: FontWeights.semibold,
paddingTop: cardItemGapSmall,
paddingBottom: cardItemGapSmall,
},
}}
nowrap
>
{cardTitle}
</Text>
<Text variant="small" styles={{ root: { height: smallTextLineHeight * 2 } }}>
{renderTruncatedDescription()}
</Text>
<span>
{data.views !== undefined && generateIconText("RedEye", data.views.toString())}
{data.downloads !== undefined && generateIconText("Download", data.downloads.toString())}
{data.favorites !== undefined && generateIconText("Heart", data.favorites.toString())}
</span>
</Card.Section>
{cardButtonsVisible && (
<Card.Section
styles={{
root: {
marginLeft: cardItemGapBig,
marginRight: cardItemGapBig,
},
}}
>
<Separator styles={{ root: { padding: 0, height: 1 } }} />
<span>
{isFavorite !== undefined &&
generateIconButtonWithTooltip(
isFavorite ? "HeartFill" : "Heart",
isFavorite ? "Unfavorite" : "Favorite",
"left",
isFavorite ? onUnfavoriteClick : onFavoriteClick
)}
{showDownload && generateIconButtonWithTooltip("Download", "Download", "left", onDownloadClick)}
{showDelete &&
generateIconButtonWithTooltip("Delete", "Remove", "right", () =>
onDeleteClick(
() => setIsDeletingPublishedNotebook(true),
() => setIsDeletingPublishedNotebook(false)
)
)}
</span>
</Card.Section>
)}
</>
)}
</Card>
);
};

View File

@@ -1,123 +0,0 @@
import * as React from "react";
import { JunoClient } from "../../../Juno/JunoClient";
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react";
import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
export interface CodeOfConductComponentProps {
junoClient: JunoClient;
onAcceptCodeOfConduct: (result: boolean) => void;
}
interface CodeOfConductComponentState {
readCodeOfConduct: boolean;
}
export class CodeOfConductComponent extends React.Component<CodeOfConductComponentProps, CodeOfConductComponentState> {
private viewCodeOfConductTraced: boolean;
private descriptionPara1: string;
private descriptionPara2: string;
private descriptionPara3: string;
private link1: { label: string; url: string };
constructor(props: CodeOfConductComponentProps) {
super(props);
this.state = {
readCodeOfConduct: false,
};
this.descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct";
this.descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.";
this.descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the ";
this.link1 = { label: "code of conduct.", url: CodeOfConductEndpoints.codeOfConduct };
}
private async acceptCodeOfConduct(): Promise<void> {
const startKey = traceStart(Action.NotebooksGalleryAcceptCodeOfConduct);
try {
const response = await this.props.junoClient.acceptCodeOfConduct();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
}
traceSuccess(Action.NotebooksGalleryAcceptCodeOfConduct, {}, startKey);
this.props.onAcceptCodeOfConduct(response.data);
} catch (error) {
traceFailure(
Action.NotebooksGalleryAcceptCodeOfConduct,
{
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct");
}
}
private onChangeCheckbox = (): void => {
this.setState({ readCodeOfConduct: !this.state.readCodeOfConduct });
};
public render(): JSX.Element {
if (!this.viewCodeOfConductTraced) {
this.viewCodeOfConductTraced = true;
trace(Action.NotebooksGalleryViewCodeOfConduct);
}
return (
<Stack tokens={{ childrenGap: 20 }}>
<Stack.Item>
<Text style={{ fontWeight: 500, fontSize: "20px" }}>{this.descriptionPara1}</Text>
</Stack.Item>
<Stack.Item>
<Text>{this.descriptionPara2}</Text>
</Stack.Item>
<Stack.Item>
<Text>
{this.descriptionPara3}
<Link href={this.link1.url} target="_blank">
{this.link1.label}
</Link>
</Text>
</Stack.Item>
<Stack.Item>
<Checkbox
styles={{
label: {
margin: 0,
padding: "2 0 2 0",
},
text: {
fontSize: 12,
},
}}
label="I have read and accept the code of conduct."
onChange={this.onChangeCheckbox}
/>
</Stack.Item>
<Stack.Item>
<PrimaryButton
ariaLabel="Continue"
title="Continue"
onClick={async () => await this.acceptCodeOfConduct()}
tabIndex={0}
className="genericPaneSubmitBtn"
text="Continue"
disabled={!this.state.readCodeOfConduct}
/>
</Stack.Item>
</Stack>
);
}
}

View File

@@ -1,9 +1,9 @@
jest.mock("../../../Juno/JunoClient");
jest.mock("../../../../Juno/JunoClient");
import { shallow } from "enzyme";
import React from "react";
import { CodeOfConductComponent, CodeOfConductComponentProps } from "./CodeOfConductComponent";
import { JunoClient } from "../../../Juno/JunoClient";
import { HttpStatusCodes } from "../../../Common/Constants";
import { CodeOfConductComponent, CodeOfConductComponentProps } from ".";
import { HttpStatusCodes } from "../../../../Common/Constants";
import { JunoClient } from "../../../../Juno/JunoClient";
describe("CodeOfConductComponent", () => {
let codeOfConductProps: CodeOfConductComponentProps;

View File

@@ -0,0 +1,110 @@
import { Checkbox, Link, PrimaryButton, Stack, Text } from "office-ui-fabric-react";
import React, { FunctionComponent, useEffect, useState } from "react";
import { CodeOfConductEndpoints, HttpStatusCodes } from "../../../../Common/Constants";
import { getErrorMessage, getErrorStack, handleError } from "../../../../Common/ErrorHandlingUtils";
import { JunoClient } from "../../../../Juno/JunoClient";
import { Action } from "../../../../Shared/Telemetry/TelemetryConstants";
import { trace, traceFailure, traceStart, traceSuccess } from "../../../../Shared/Telemetry/TelemetryProcessor";
export interface CodeOfConductComponentProps {
junoClient: JunoClient;
onAcceptCodeOfConduct: (result: boolean) => void;
}
export const CodeOfConductComponent: FunctionComponent<CodeOfConductComponentProps> = ({
junoClient,
onAcceptCodeOfConduct,
}: CodeOfConductComponentProps) => {
const descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct";
const descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.";
const descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the ";
const link1: { label: string; url: string } = {
label: "code of conduct.",
url: CodeOfConductEndpoints.codeOfConduct,
};
const [readCodeOfConduct, setReadCodeOfConduct] = useState<boolean>(false);
const acceptCodeOfConduct = async (): Promise<void> => {
const startKey = traceStart(Action.NotebooksGalleryAcceptCodeOfConduct);
try {
const response = await junoClient.acceptCodeOfConduct();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
}
traceSuccess(Action.NotebooksGalleryAcceptCodeOfConduct, {}, startKey);
onAcceptCodeOfConduct(response.data);
} catch (error) {
traceFailure(
Action.NotebooksGalleryAcceptCodeOfConduct,
{
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct");
}
};
const onChangeCheckbox = (): void => {
setReadCodeOfConduct(!readCodeOfConduct);
};
useEffect(() => {
trace(Action.NotebooksGalleryViewCodeOfConduct);
}, []);
return (
<Stack tokens={{ childrenGap: 20 }}>
<Stack.Item>
<Text style={{ fontWeight: 500, fontSize: "20px" }}>{descriptionPara1}</Text>
</Stack.Item>
<Stack.Item>
<Text>{descriptionPara2}</Text>
</Stack.Item>
<Stack.Item>
<Text>
{descriptionPara3}
<Link href={link1.url} target="_blank">
{link1.label}
</Link>
</Text>
</Stack.Item>
<Stack.Item>
<Checkbox
styles={{
label: {
margin: 0,
padding: "2 0 2 0",
},
text: {
fontSize: 12,
},
}}
label="I have read and accept the code of conduct."
onChange={onChangeCheckbox}
/>
</Stack.Item>
<Stack.Item>
<PrimaryButton
ariaLabel="Continue"
title="Continue"
onClick={async () => await acceptCodeOfConduct()}
tabIndex={0}
className="genericPaneSubmitBtn"
text="Continue"
disabled={!readCodeOfConduct}
/>
</Stack.Item>
</Stack>
);
};

View File

@@ -1,29 +0,0 @@
import ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import {
GalleryAndNotebookViewerComponentProps,
GalleryAndNotebookViewerComponent,
} from "./GalleryAndNotebookViewerComponent";
export class GalleryAndNotebookViewerComponentAdapter implements ReactAdapter {
private key: string;
public parameters: ko.Observable<number>;
constructor(private props: GalleryAndNotebookViewerComponentProps) {
this.reset();
this.parameters = ko.observable<number>(Date.now());
}
public renderComponent(): JSX.Element {
return <GalleryAndNotebookViewerComponent key={this.key} {...this.props} />;
}
public reset(): void {
this.key = `GalleryAndNotebookViewerComponent-${Date.now()}`;
}
public triggerRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
}

View File

@@ -34,6 +34,7 @@ import { CodeOfConductComponent } from "./CodeOfConductComponent";
import "./GalleryViewerComponent.less";
import { InfoComponent } from "./InfoComponent/InfoComponent";
const CARD_WIDTH = 256;
export interface GalleryViewerComponentProps {
container?: Explorer;
junoClient: JunoClient;
@@ -643,7 +644,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private getPageSpecification = (itemIndex?: number, visibleRect?: IRectangle): IPageSpecification => {
if (itemIndex === 0) {
this.columnCount = Math.floor(visibleRect.width / GalleryCardComponent.CARD_WIDTH) || this.columnCount;
this.columnCount = Math.floor(visibleRect.width / CARD_WIDTH) || this.columnCount;
this.rowCount = GalleryViewerComponent.rowsPerPage;
}

View File

@@ -136,15 +136,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
this.container = this.collection?.container;
this.offer = this.collection?.offer();
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
this.shouldShowIndexingPolicyEditor =
this.container && userContext.apiType !== "Cassandra" && !this.container.isPreferredApiMongoDB();
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
// Mongo container with system partition key still treat as "Fixed"
this.isFixedContainer =
this.container.isPreferredApiMongoDB() &&
(!this.collection?.partitionKey || this.collection?.partitionKey.systemKey);
userContext.apiType === "Mongo" && (!this.collection?.partitionKey || this.collection?.partitionKey.systemKey);
} else {
this.database = this.props.settingsTab.database;
this.container = this.database?.container;
@@ -236,7 +234,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
public loadMongoIndexes = async (): Promise<void> => {
if (
this.container.isPreferredApiMongoDB() &&
userContext.apiType === "Mongo" &&
this.container.isEnableMongoCapabilityPresent() &&
this.container.databaseAccount()
) {
@@ -1002,7 +1000,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
tab: SettingsV2TabTypes.IndexingPolicyTab,
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />,
});
} else if (this.container.isPreferredApiMongoDB()) {
} else if (userContext.apiType === "Mongo") {
const mongoIndexTabContext = this.getMongoIndexTabContent(mongoIndexingPolicyComponentProps);
if (mongoIndexTabContext) {
tabs.push({

View File

@@ -1,9 +1,9 @@
import { MessageBar, MessageBarType, Stack } from "office-ui-fabric-react";
import * as React from "react";
import * as DataModels from "../../../../Contracts/DataModels";
import * as monaco from "monaco-editor";
import { isDirty, isIndexTransforming } from "../SettingsUtils";
import { MessageBar, MessageBarType, Stack } from "office-ui-fabric-react";
import { loadMonaco, monaco } from "../../../LazyMonaco";
import { indexingPolicynUnsavedWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils";
import { isDirty, isIndexTransforming } from "../SettingsUtils";
import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
export interface IndexingPolicyComponentProps {
@@ -84,9 +84,9 @@ export class IndexingPolicyComponent extends React.Component<
return false;
};
private createIndexingPolicyEditor = (): void => {
private async createIndexingPolicyEditor(): Promise<void> {
const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4);
const monaco = await loadMonaco();
this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, {
value: value,
language: "json",
@@ -98,7 +98,7 @@ export class IndexingPolicyComponent extends React.Component<
indexingPolicyEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this));
this.props.logIndexingPolicySuccessMessage();
}
};
}
private onEditorContentChange = (): void => {
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();

View File

@@ -323,7 +323,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
userContext.apiType === "Cassandra" ||
userContext.apiType === "Tables" ||
!this.props.collection.partitionKeyProperty ||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey)
(userContext.apiType === "Mongo" && this.props.collection.partitionKey.systemKey)
) {
return false;
}

View File

@@ -177,40 +177,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function],
"visible": [Function],
},
EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
@@ -233,9 +199,7 @@ exports[`SettingsComponent renders 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -258,20 +222,6 @@ exports[`SettingsComponent renders 1`] = `
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -426,9 +376,7 @@ exports[`SettingsComponent renders 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -479,40 +427,6 @@ exports[`SettingsComponent renders 1`] = `
"defaultExperience": [Function],
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -544,7 +458,6 @@ exports[`SettingsComponent renders 1`] = `
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiMongoDB": [Function],
"isPublishNotebookPaneEnabled": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
@@ -576,21 +489,9 @@ exports[`SettingsComponent renders 1`] = `
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -616,20 +517,6 @@ exports[`SettingsComponent renders 1`] = `
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
@@ -805,40 +692,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function],
"visible": [Function],
},
EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
@@ -861,9 +714,7 @@ exports[`SettingsComponent renders 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -886,20 +737,6 @@ exports[`SettingsComponent renders 1`] = `
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -1054,9 +891,7 @@ exports[`SettingsComponent renders 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -1107,40 +942,6 @@ exports[`SettingsComponent renders 1`] = `
"defaultExperience": [Function],
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -1172,7 +973,6 @@ exports[`SettingsComponent renders 1`] = `
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiMongoDB": [Function],
"isPublishNotebookPaneEnabled": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
@@ -1204,21 +1004,9 @@ exports[`SettingsComponent renders 1`] = `
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -1244,20 +1032,6 @@ exports[`SettingsComponent renders 1`] = `
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
@@ -1446,40 +1220,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function],
"visible": [Function],
},
EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
@@ -1502,9 +1242,7 @@ exports[`SettingsComponent renders 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -1527,20 +1265,6 @@ exports[`SettingsComponent renders 1`] = `
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -1695,9 +1419,7 @@ exports[`SettingsComponent renders 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -1748,40 +1470,6 @@ exports[`SettingsComponent renders 1`] = `
"defaultExperience": [Function],
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -1813,7 +1501,6 @@ exports[`SettingsComponent renders 1`] = `
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiMongoDB": [Function],
"isPublishNotebookPaneEnabled": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
@@ -1845,21 +1532,9 @@ exports[`SettingsComponent renders 1`] = `
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -1885,20 +1560,6 @@ exports[`SettingsComponent renders 1`] = `
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
@@ -2074,40 +1735,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function],
"visible": [Function],
},
EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
@@ -2130,9 +1757,7 @@ exports[`SettingsComponent renders 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -2155,20 +1780,6 @@ exports[`SettingsComponent renders 1`] = `
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -2323,9 +1934,7 @@ exports[`SettingsComponent renders 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -2376,40 +1985,6 @@ exports[`SettingsComponent renders 1`] = `
"defaultExperience": [Function],
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -2441,7 +2016,6 @@ exports[`SettingsComponent renders 1`] = `
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiMongoDB": [Function],
"isPublishNotebookPaneEnabled": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
@@ -2473,21 +2047,9 @@ exports[`SettingsComponent renders 1`] = `
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -2513,20 +2075,6 @@ exports[`SettingsComponent renders 1`] = `
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],

View File

@@ -14,7 +14,6 @@ describe("ContainerSampleGenerator", () => {
const createExplorerStub = (database: ViewModels.Database): Explorer => {
const explorerStub = {} as Explorer;
explorerStub.databases = ko.observableArray<ViewModels.Database>([database]);
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
explorerStub.canExceedMaximumValue = ko.computed<boolean>(() => false);
explorerStub.findDatabaseWithId = () => database;
explorerStub.refreshAllDatabases = () => Q.resolve();

View File

@@ -6,7 +6,6 @@ import React from "react";
import _ from "underscore";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
import * as Constants from "../Common/Constants";
import { ExplorerMetrics } from "../Common/Constants";
import { readCollection } from "../Common/dataAccess/readCollection";
@@ -26,12 +25,11 @@ import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProvi
import { RouteHandler } from "../RouteHandlers/RouteHandler";
import { trackEvent } from "../Shared/appInsights";
import * as SharedConstants from "../Shared/Constants";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { ExplorerSettings } from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager";
import { updateUserContext, userContext } from "../UserContext";
import { userContext } from "../UserContext";
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { stringToBlob } from "../Utils/BlobUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
@@ -42,11 +40,13 @@ import * as ComponentRegisterer from "./ComponentRegisterer";
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
import { DialogProps, TextFieldProps } from "./Controls/Dialog";
import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { GalleryTab as GalleryTabKind } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { ConsoleData } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import * as FileSystemUtil from "./Notebook/FileSystemUtil";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
import type NotebookManager from "./Notebook/NotebookManager";
import type { NotebookPaneContent } from "./Notebook/NotebookManager";
import { NotebookUtil } from "./Notebook/NotebookUtil";
import AddCollectionPane from "./Panes/AddCollectionPane";
import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
@@ -62,9 +62,9 @@ import { LoadQueryPane } from "./Panes/LoadQueryPane/LoadQueryPane";
import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane";
import { SettingsPane } from "./Panes/SettingsPane/SettingsPane";
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
import { StringInputPane } from "./Panes/StringInputPane";
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import { EditTableEntityPanel } from "./Panes/Tables/EditTableEntityPanel";
import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel";
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
@@ -73,7 +73,6 @@ import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import QueryTablesTab from "./Tabs/QueryTablesTab";
import TabsBase from "./Tabs/TabsBase";
import { TabsManager } from "./Tabs/TabsManager";
import TerminalTab from "./Tabs/TerminalTab";
import Database from "./Tree/Database";
@@ -118,11 +117,6 @@ export default class Explorer {
* Use userContext.apiType instead
* */
public defaultExperience: ko.Observable<string>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Mongo"
* */
public isPreferredApiMongoDB: ko.Computed<boolean>;
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
/**
* @deprecated
@@ -165,19 +159,14 @@ export default class Explorer {
// Tabs
public isTabsContentExpanded: ko.Observable<boolean>;
public galleryTab: any;
public notebookViewerTab: any;
public tabsManager: TabsManager;
// Contextual panes
public addDatabasePane: AddDatabasePane;
public addCollectionPane: AddCollectionPane;
public graphStylingPane: GraphStylingPane;
public editTableEntityPane: EditTableEntityPane;
public cassandraAddCollectionPane: CassandraAddCollectionPane;
public stringInputPane: StringInputPane;
public gitHubReposPane: ContextualPaneBase;
public publishNotebookPaneAdapter: ReactAdapter;
// features
public isGitHubPaneEnabled: ko.Observable<boolean>;
@@ -203,7 +192,7 @@ export default class Explorer {
public hasStorageAnalyticsAfecFeature: ko.Observable<boolean>;
public isSynapseLinkUpdating: ko.Observable<boolean>;
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
public notebookManager?: any; // This is dynamically loaded
public notebookManager?: NotebookManager;
public openDialog: ExplorerParams["openDialog"];
public closeDialog: ExplorerParams["closeDialog"];
@@ -383,16 +372,16 @@ export default class Explorer {
direction: SplitterDirection.Vertical,
});
this.defaultExperience = ko.observable<string>();
this.databaseAccount.subscribe((databaseAccount) => {
const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(
databaseAccount
);
this.defaultExperience(defaultExperience);
// TODO. Remove this entirely
updateUserContext({
defaultExperience: DefaultExperienceUtility.mapDefaultExperienceStringToEnum(defaultExperience),
});
});
// this.databaseAccount.subscribe((databaseAccount) => {
// const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(
// databaseAccount
// );
// this.defaultExperience(defaultExperience);
// // TODO. Remove this entirely
// updateUserContext({
// apiType: DefaultExperienceUtility.mapDefaultExperienceStringToEnum(defaultExperience),
// });
// });
this.isFixedCollectionWithSharedThroughputSupported = ko.computed(() => {
if (userContext.features.enableFixedCollectionWithSharedThroughput) {
@@ -414,27 +403,6 @@ export default class Explorer {
) !== undefined
);
this.isPreferredApiMongoDB = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.MongoDB.toLowerCase()) {
return true;
}
if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.ApiForMongoDB.toLowerCase()) {
return true;
}
if (
this.databaseAccount &&
this.databaseAccount() &&
this.databaseAccount().kind.toLowerCase() === Constants.AccountKind.MongoDB
) {
return true;
}
return false;
});
this.isEnableMongoCapabilityPresent = ko.computed(() => {
const capabilities = this.databaseAccount && this.databaseAccount()?.properties?.capabilities;
if (!capabilities) {
@@ -500,13 +468,6 @@ export default class Explorer {
container: this,
});
this.editTableEntityPane = new EditTableEntityPane({
id: "edittableentitypane",
visible: ko.observable<boolean>(false),
container: this,
});
this.cassandraAddCollectionPane = new CassandraAddCollectionPane({
id: "cassandraaddcollectionpane",
visible: ko.observable<boolean>(false),
@@ -514,13 +475,6 @@ export default class Explorer {
container: this,
});
this.stringInputPane = new StringInputPane({
id: "stringinputpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.tabsManager = params?.tabsManager ?? new TabsManager();
this.tabsManager.openedTabs.subscribe((tabs) => {
if (tabs.length === 0) {
@@ -533,9 +487,7 @@ export default class Explorer {
this.addDatabasePane,
this.addCollectionPane,
this.graphStylingPane,
this.editTableEntityPane,
this.cassandraAddCollectionPane,
this.stringInputPane,
];
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
this.isTabsContentExpanded = ko.observable(false);
@@ -604,7 +556,6 @@ export default class Explorer {
this.addCollectionPane.collectionIdTitle("Table id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.refreshTreeTitle("Refresh tables");
this.editTableEntityPane.title("Edit Table Entity");
this.tableDataClient = new TablesAPIDataClient();
break;
case "Cassandra":
@@ -618,7 +569,6 @@ export default class Explorer {
this.addCollectionPane.collectionIdTitle("Table id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.refreshTreeTitle("Refresh tables");
this.editTableEntityPane.title("Edit Table Row");
this.tableDataClient = new CassandraAPIDataClient();
break;
}
@@ -636,10 +586,10 @@ export default class Explorer {
this.isNotebookEnabled = ko.observable(false);
this.isNotebookEnabled.subscribe(async () => {
if (!this.notebookManager) {
const notebookManagerModule = await import(
/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager"
);
this.notebookManager = new notebookManagerModule.default();
const NotebookManager = await (
await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager")
).default;
this.notebookManager = new NotebookManager();
this.notebookManager.initialize({
container: this,
notebookBasePath: this.notebookBasePath,
@@ -1424,10 +1374,13 @@ export default class Explorer {
return Promise.resolve(false);
}
public async publishNotebook(name: string, content: string | unknown, parentDomElement?: HTMLElement): Promise<void> {
public async publishNotebook(
name: string,
content: NotebookPaneContent,
parentDomElement?: HTMLElement
): Promise<void> {
if (this.notebookManager) {
await this.notebookManager.openPublishNotebookPane(name, content, parentDomElement);
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
this.isPublishNotebookPaneEnabled(true);
}
}
@@ -1540,7 +1493,7 @@ export default class Explorer {
return true;
}
public renameNotebook(notebookFile: NotebookContentItem): Q.Promise<NotebookContentItem> {
public renameNotebook(notebookFile: NotebookContentItem): void {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to rename notebook, but notebook is not enabled";
handleError(error, "Explorer/renameNotebook");
@@ -1556,57 +1509,59 @@ export default class Explorer {
);
if (openedNotebookTabs.length > 0) {
this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
return Q.reject();
} else {
this.openSidePanel(
"",
<StringInputPane
explorer={this}
closePanel={() => {
this.closeSidePanel();
this.resourceTree.triggerRender();
}}
inputLabel="Enter new notebook name"
submitButtonLabel="Rename"
errorMessage="Could not rename notebook"
inProgressMessage="Renaming notebook to"
successMessage="Renamed notebook to"
paneTitle="Rename Notebook"
defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")}
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input)
}
notebookFile={notebookFile}
/>
);
}
const originalPath = notebookFile.path;
const result = this.stringInputPane
.openWithOptions<NotebookContentItem>({
errorMessage: "Could not rename notebook",
inProgressMessage: "Renaming notebook to",
successMessage: "Renamed notebook to",
inputLabel: "Enter new notebook name",
paneTitle: "Rename Notebook",
submitButtonLabel: "Rename",
defaultInput: FileSystemUtil.stripExtension(notebookFile.name, "ipynb"),
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input),
})
.then((newNotebookFile) => {
const notebookTabs = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
);
notebookTabs.forEach((tab) => {
tab.tabTitle(newNotebookFile.name);
tab.tabPath(newNotebookFile.path);
(tab as NotebookV2Tab).notebookPath(newNotebookFile.path);
});
return newNotebookFile;
});
result.then(() => this.resourceTree.triggerRender());
return result;
}
public onCreateDirectory(parent: NotebookContentItem): Q.Promise<NotebookContentItem> {
public onCreateDirectory(parent: NotebookContentItem): void {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create notebook directory, but notebook is not enabled";
handleError(error, "Explorer/onCreateDirectory");
throw new Error(error);
}
const result = this.stringInputPane.openWithOptions<NotebookContentItem>({
errorMessage: "Could not create directory ",
inProgressMessage: "Creating directory ",
successMessage: "Created directory ",
inputLabel: "Enter new directory name",
paneTitle: "Create new directory",
submitButtonLabel: "Create",
defaultInput: "",
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input),
});
result.then(() => this.resourceTree.triggerRender());
return result;
this.openSidePanel(
"",
<StringInputPane
explorer={this}
closePanel={() => {
this.closeSidePanel();
this.resourceTree.triggerRender();
}}
errorMessage="Could not create directory "
inProgressMessage="Creating directory "
successMessage="Created directory "
inputLabel="Enter new directory name"
paneTitle="Create new directory"
submitButtonLabel="Create"
defaultInput=""
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input)
}
notebookFile={parent}
/>
);
}
public readFile(notebookFile: NotebookContentItem): Promise<string> {
@@ -1922,86 +1877,68 @@ export default class Explorer {
}
public async openGallery(
selectedTab?: GalleryTab,
selectedTab?: GalleryTabKind,
notebookUrl?: string,
galleryItem?: IGalleryItem,
isFavorite?: boolean
) {
let title: string = "Gallery";
let hashLocation: string = "gallery";
const title = "Gallery";
const hashLocation = "gallery";
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
const galleryTab = this.tabsManager
.getTabs(ViewModels.CollectionTabKind.Gallery)
.find((tab) => tab.hashLocation() == hashLocation);
const galleryTabOptions: any = {
// GalleryTabOptions
account: userContext.databaseAccount,
container: this,
junoClient: this.notebookManager?.junoClient,
selectedTab: selectedTab || GalleryTab.PublicGallery,
notebookUrl,
galleryItem,
isFavorite,
// TabOptions
tabKind: ViewModels.CollectionTabKind.Gallery,
title: title,
tabPath: title,
documentClientUtility: null,
isActive: ko.observable(false),
hashLocation: hashLocation,
onUpdateTabsButtons: this.onUpdateTabsButtons,
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null,
};
const galleryTabs = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.Gallery,
(tab) => tab.hashLocation() == hashLocation
);
let galleryTab = galleryTabs && galleryTabs[0];
if (galleryTab) {
if (galleryTab instanceof GalleryTab) {
this.tabsManager.activateTab(galleryTab);
(galleryTab as any).reset(galleryTabOptions);
} else {
if (!this.galleryTab) {
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
}
const newTab = new this.galleryTab.default(galleryTabOptions);
this.tabsManager.activateNewTab(newTab);
this.tabsManager.activateNewTab(
new GalleryTab(
{
tabKind: ViewModels.CollectionTabKind.Gallery,
title: title,
tabPath: title,
hashLocation: hashLocation,
onUpdateTabsButtons: this.onUpdateTabsButtons,
onLoadStartKey: null,
isTabsContentExpanded: ko.observable(true),
},
{
account: userContext.databaseAccount,
container: this,
junoClient: this.notebookManager?.junoClient,
selectedTab: selectedTab || GalleryTabKind.PublicGallery,
notebookUrl,
galleryItem,
isFavorite,
}
)
);
}
}
public async openNotebookViewer(notebookUrl: string) {
const title = path.basename(notebookUrl);
const hashLocation = notebookUrl;
const NotebookViewerTab = await (
await import(/* webpackChunkName: "NotebookViewerTab" */ "./Tabs/NotebookViewerTab")
).default;
if (!this.notebookViewerTab) {
this.notebookViewerTab = await import(/* webpackChunkName: "NotebookViewerTab" */ "./Tabs/NotebookViewerTab");
}
const notebookViewerTabModule = this.notebookViewerTab;
let isNotebookViewerOpen = (tab: TabsBase) => {
const notebookViewerTab = tab as typeof notebookViewerTabModule.default;
return notebookViewerTab.notebookUrl === notebookUrl;
};
const notebookViewerTabs = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2, (tab) => {
return tab.hashLocation() == hashLocation && isNotebookViewerOpen(tab);
const notebookViewerTab = this.tabsManager.getTabs(ViewModels.CollectionTabKind.NotebookV2).find((tab) => {
return tab.hashLocation() == hashLocation && tab instanceof NotebookViewerTab && tab.notebookUrl === notebookUrl;
});
let notebookViewerTab = notebookViewerTabs && notebookViewerTabs[0];
if (notebookViewerTab) {
this.tabsManager.activateNewTab(notebookViewerTab);
} else {
notebookViewerTab = new this.notebookViewerTab.default({
const notebookViewerTab = new NotebookViewerTab({
account: userContext.databaseAccount,
tabKind: ViewModels.CollectionTabKind.NotebookViewer,
node: null,
title: title,
tabPath: title,
documentClientUtility: null,
collection: null,
hashLocation: hashLocation,
isActive: ko.observable(false),
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null,
onUpdateTabsButtons: this.onUpdateTabsButtons,
@@ -2124,7 +2061,7 @@ export default class Explorer {
}
public openDeleteCollectionConfirmationPane(): void {
let collectionName = PricingUtils.getCollectionName(userContext.defaultExperience);
let collectionName = PricingUtils.getCollectionName(userContext.apiType);
this.openSidePanel(
"Delete " + collectionName,
<DeleteCollectionConfirmationPane
@@ -2227,6 +2164,19 @@ export default class Explorer {
);
}
public openEditTableEntityPanel(queryTablesTab: QueryTablesTab, tableEntityListViewModel: TableListViewModal): void {
this.openSidePanel(
"Edit Table Entity",
<EditTableEntityPanel
explorer={this}
closePanel={this.closeSidePanel}
queryTablesTab={queryTablesTab}
tableEntityListViewModel={tableEntityListViewModel}
cassandraApiClient={new CassandraAPIDataClient()}
/>
);
}
public openTableSelectQueryPanel(queryViewModal: QueryViewModel): void {
this.openSidePanel(
"Select Column",

View File

@@ -11,7 +11,6 @@ import * as ko from "knockout";
import Q from "q";
import _ from "underscore";
import * as Constants from "../../../Common/Constants";
import { HashMap } from "../../../Common/HashMap";
import { NeighborType } from "../../../Contracts/ViewModels";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import { GraphConfig } from "../../Tabs/GraphTab";
@@ -195,8 +194,8 @@ export class D3ForceGraph implements GraphRenderer {
* Count edges and store in a hashmap: vertex id <--> number of links
* @param linkSelection
*/
public static countEdges(links: D3Link[]): HashMap<number> {
const countMap = new HashMap<number>();
public static countEdges(links: D3Link[]): Map<string, number> {
const countMap = new Map<string, number>();
links.forEach((l: D3Link) => {
let val = countMap.get(l.inV) || 0;
val += 1;
@@ -407,7 +406,7 @@ export class D3ForceGraph implements GraphRenderer {
const rootId = graph.findRootNodeId();
// Remember nodes current position
const posMap = new HashMap<Point2D>();
const posMap = new Map<string, Point2D>();
this.simulation.nodes().forEach((d: D3Node) => {
if (d.x == undefined || d.y == undefined) {
return;
@@ -501,8 +500,8 @@ export class D3ForceGraph implements GraphRenderer {
if (!nodes || nodes.length === 0) {
return;
}
const nodeFinalPositionMap = new HashMap<Point2D>();
const nodeFinalPositionMap = new Map<string, Point2D>();
const viewCenter = this.viewCenter;
const nonFixedNodes = _.filter(nodes, (node: D3Node) => {
return !node._isFixedPosition && node.x === viewCenter.x && node.y === viewCenter.y;
@@ -559,7 +558,7 @@ export class D3ForceGraph implements GraphRenderer {
newNodes.selectAll(".loadmore").attr("visibility", "hidden").transition().delay(600).attr("visibility", "visible");
}
private restartSimulation(graph: GraphData<D3Node, D3Link>, posMap: HashMap<Point2D>) {
private restartSimulation(graph: GraphData<D3Node, D3Link>, posMap: Map<string, Point2D>) {
if (!graph) {
return;
}

View File

@@ -1,5 +1,5 @@
import { ObjectCache } from "../../../Common/ObjectCache";
import { GremlinVertex, GraphData } from "./GraphData";
import { GraphData, GremlinVertex } from "./GraphData";
/**
* Remember vertex edge information
@@ -10,9 +10,8 @@ export class EdgeInfoCache extends ObjectCache<GremlinVertex> {
* @param vertex
*/
public addVertex(vertex: GremlinVertex): void {
let v: GremlinVertex;
if (super.has(vertex.id)) {
v = super.get(vertex.id);
let v = super.get(vertex.id);
if (super.has(vertex.id) && v) {
GraphData.addEdgeInfoToVertex(v, vertex);
v._outEdgeIds = vertex._outEdgeIds;
v._inEdgeIds = vertex._inEdgeIds;
@@ -29,8 +28,8 @@ export class EdgeInfoCache extends ObjectCache<GremlinVertex> {
* @param id
*/
public mergeEdgeInfo(target: GremlinVertex): void {
if (super.has(target.id)) {
const cachedVertex = super.get(target.id);
const cachedVertex = super.get(target.id);
if (super.has(target.id) && cachedVertex) {
GraphData.addEdgeInfoToVertex(target, cachedVertex);
target._outEdgeIds = cachedVertex._outEdgeIds;
target._inEdgeIds = cachedVertex._inEdgeIds;

View File

@@ -1,7 +1,7 @@
import * as sinon from "sinon";
import { GremlinClient, GremlinClientParameters } from "./GremlinClient";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import * as Logger from "../../../Common/Logger";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { GremlinClient, GremlinClientParameters } from "./GremlinClient";
describe("Gremlin Client", () => {
const emptyParams: GremlinClientParameters = {
@@ -70,7 +70,7 @@ describe("Gremlin Client", () => {
gremlinClient.execute("fake query");
gremlinClient.execute("fake query");
gremlinClient.execute("fake query");
expect(gremlinClient.pendingResults.size()).toBe(3);
expect(gremlinClient.pendingResults.size).toBe(3);
});
it("should clean up pending request ids after success", async () => {
@@ -89,7 +89,7 @@ describe("Gremlin Client", () => {
return requestId;
});
await gremlinClient.execute("fake query");
expect(gremlinClient.pendingResults.size()).toBe(0);
expect(gremlinClient.pendingResults.size).toBe(0);
});
it("should log and display error out on unknown requestId", () => {
@@ -247,7 +247,7 @@ describe("Gremlin Client", () => {
sinon.stub(gremlinClient.client, "executeGremlinQuery").callsFake((query: string): string => requestId);
gremlinClient.execute("fake query").finally(() => {
try {
expect(gremlinClient.pendingResults.size()).toBe(0);
expect(gremlinClient.pendingResults.size).toBe(0);
done();
} catch (e) {
done(e);

View File

@@ -4,7 +4,6 @@
import * as Q from "q";
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
import { HashMap } from "../../../Common/HashMap";
import { logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
import { GremlinSimpleClient, Result } from "./GremlinSimpleClient";
@@ -30,7 +29,7 @@ interface PendingResultData {
export class GremlinClient {
public client: GremlinSimpleClient;
public pendingResults: HashMap<PendingResultData>; // public for testing purposes
public pendingResults: Map<string, PendingResultData>; // public for testing purposes
private maxResultSize: number;
private static readonly PENDING_REQUEST_TIMEOUT_MS = 6 /* minutes */ * 60 /* seconds */ * 1000 /* ms */;
private static readonly TIMEOUT_ERROR_MSG = `Pending request timed out (${GremlinClient.PENDING_REQUEST_TIMEOUT_MS} ms)`;
@@ -38,7 +37,7 @@ export class GremlinClient {
public initialize(params: GremlinClientParameters) {
this.destroy();
this.pendingResults = new HashMap();
this.pendingResults = new Map();
this.maxResultSize = params.maxResultSize;
this.client = new GremlinSimpleClient({
@@ -68,9 +67,9 @@ export class GremlinClient {
// Fail all pending requests if no request id (fatal)
if (!requestId) {
this.pendingResults.keys().forEach((reqId: string) => {
for (const reqId of this.pendingResults.keys()) {
this.abortPendingRequest(reqId, errorMessage, null);
});
}
}
} else {
this.abortPendingRequest(requestId, errorMessage, result.requestCharge);

View File

@@ -0,0 +1,5 @@
import type * as monaco from "monaco-editor/esm/vs/editor/editor.api";
export type { monaco };
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const loadMonaco = () => import(/* webpackChunkName: "lazy-monaco" */ "monaco-editor/esm/vs/editor/editor.api");

View File

@@ -29,7 +29,6 @@ export class CommandBarComponentAdapter implements ReactAdapter {
// These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates
const toWatch = [
container.isPreferredApiMongoDB,
container.deleteCollectionText,
container.deleteDatabaseText,
container.addCollectionText,

View File

@@ -23,7 +23,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
@@ -67,7 +66,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
@@ -128,6 +126,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
beforeAll(() => {
mockExplorer = {} as Explorer;
mockExplorer.addDatabaseText = ko.observable("mockText");
mockExplorer.addCollectionText = ko.observable("mockText");
updateUserContext({
databaseAccount: {
@@ -143,16 +142,25 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
});
afterAll(() => {
updateUserContext({
apiType: "SQL",
});
});
beforeEach(() => {
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => true);
updateUserContext({
apiType: "Mongo",
});
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
});
it("Mongo Api not available - button should be hidden", () => {
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
updateUserContext({
apiType: "SQL",
});
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
expect(openMongoShellBtn).toBeUndefined();
@@ -222,7 +230,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true);
@@ -321,7 +328,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
},
} as DatabaseAccount,
});
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true);

View File

@@ -70,7 +70,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
buttons.push(createEnableNotebooksButton(container));
}
if (container.isPreferredApiMongoDB()) {
if (userContext.apiType === "Mongo") {
buttons.push(createOpenMongoTerminalButton(container));
}
@@ -97,7 +97,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
}
const isSupportedOpenQueryApi =
userContext.apiType === "SQL" || container.isPreferredApiMongoDB() || userContext.apiType === "Gremlin";
userContext.apiType === "SQL" || userContext.apiType === "Mongo" || userContext.apiType === "Gremlin";
const isSupportedOpenQueryFromDiskApi = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) {
const openQueryBtn = createOpenQueryButton(container);
@@ -133,7 +133,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
export function createContextCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (!container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()) {
if (!container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
const label = "New Shell";
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
@@ -145,7 +145,7 @@ export function createContextCommandBarButtons(container: Explorer): CommandButt
commandButtonLabel: label,
ariaLabel: label,
hasPopup: true,
disabled: container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB(),
disabled: container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo",
};
buttons.push(newMongoShellBtn);
}

View File

@@ -11,7 +11,7 @@ let fakeAjaxResponse: AjaxResponse = {
responseType: "json",
};
export const sessions = {
create: (serverConfig: unknown, body: object): Observable<AjaxResponse> => of(fakeAjaxResponse),
create: (): Observable<AjaxResponse> => of(fakeAjaxResponse),
__setResponse: (response: AjaxResponse) => {
fakeAjaxResponse = response;
},

View File

@@ -220,7 +220,7 @@ export class NotebookContentClient {
return this.contentProvider
.remove(this.getServerConfig(), path)
.toPromise()
.then((xhr: AjaxResponse) => path);
.then(() => path);
}
/**

View File

@@ -3,7 +3,7 @@
*/
import { ImmutableNotebook } from "@nteract/commutable";
import { IContentProvider } from "@nteract/core";
import type { IContentProvider } from "@nteract/core";
import ko from "knockout";
import React from "react";
import { contents } from "rx-jupyter";
@@ -22,12 +22,16 @@ import Explorer from "../Explorer";
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
import { GitHubReposPane } from "../Panes/GitHubReposPane";
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
import { NotebookContainerClient } from "./NotebookContainerClient";
import { NotebookContentClient } from "./NotebookContentClient";
type NotebookPaneContent = string | ImmutableNotebook;
export type { NotebookPaneContent };
export interface NotebookManagerOptions {
container: Explorer;
notebookBasePath: ko.Observable<string>;
@@ -49,7 +53,6 @@ export default class NotebookManager {
private gitHubClient: GitHubClient;
public gitHubReposPane: ContextualPaneBase;
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
public initialize(params: NotebookManagerOptions): void {
this.params = params;
@@ -87,8 +90,6 @@ export default class NotebookManager {
this.notebookContentProvider
);
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
this.gitHubClient.setToken(token?.access_token);
@@ -116,10 +117,23 @@ export default class NotebookManager {
public async openPublishNotebookPane(
name: string,
content: string | ImmutableNotebook,
content: NotebookPaneContent,
parentDomElement: HTMLElement
): Promise<void> {
await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement);
const explorer = this.params.container;
explorer.openSidePanel(
"New Collection",
<PublishNotebookPane
explorer={this.params.container}
junoClient={this.junoClient}
closePanel={this.params.container.closeSidePanel}
openNotificationConsole={this.params.container.expandConsole}
name={name}
author={getFullName()}
notebookContent={content}
parentDomElement={parentDomElement}
/>
);
}
public openCopyNotebookPane(name: string, content: string): void {

View File

@@ -251,10 +251,7 @@
</div>
<!-- Indexing For Shared Throughput - start -->
<div
class="seconddivpadding"
data-bind="visible: showIndexingOptionsForSharedThroughput() && !container.isPreferredApiMongoDB()"
>
<div class="seconddivpadding" data-bind="visible: showIndexingOptionsForSharedThroughput() && !isMongo()">
<div
class="useIndexingForSharedThroughput createNewDatabaseOrUseExisting"
aria-label="Indexing For Shared Throughput"
@@ -297,7 +294,7 @@
<p
class="seconddivpadding"
data-bind="visible: container.isPreferredApiMongoDB() && !databaseHasSharedOffer() || container.isFixedCollectionWithSharedThroughputSupported"
data-bind="visible: isMongo() && !databaseHasSharedOffer() || container.isFixedCollectionWithSharedThroughputSupported"
>
<span class="mandatoryStar">*</span>
<span class="addCollectionLabel">Storage capacity</span>
@@ -312,7 +309,7 @@
<div class="tabs">
<div
tabindex="0"
data-bind="event: { keydown: onStorageOptionsKeyDown }, visible: container.isPreferredApiMongoDB() && !databaseHasSharedOffer() || container.isFixedCollectionWithSharedThroughputSupported"
data-bind="event: { keydown: onStorageOptionsKeyDown }, visible: isMongo() && !databaseHasSharedOffer() || container.isFixedCollectionWithSharedThroughputSupported"
aria-label="Storage capacity"
>
<!-- Fixed option button - Start -->

View File

@@ -119,7 +119,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.isPreferredApiTable = options.isPreferredApiTable;
this.partitionKey = ko.observable<string>();
this.partitionKey.subscribe((newPartitionKey: string) => {
if (this.container.isPreferredApiMongoDB() || !newPartitionKey || newPartitionKey[0] === "/") {
if (userContext.apiType === "Mongo" || !newPartitionKey || newPartitionKey[0] === "/") {
return;
}
@@ -354,7 +354,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
// TODO: Create derived classes for Tables and Mongo to replace the If statements below
this.partitionKeyName = ko.computed<string>(() => {
if (this.container && !!this.container.isPreferredApiMongoDB()) {
if (userContext.apiType === "Mongo") {
return "Shard key";
}
@@ -364,7 +364,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
this.lowerCasePartitionKeyName = ko.computed<string>(() => this.partitionKeyName().toLowerCase());
this.partitionKeyPlaceholder = ko.computed<string>(() => {
if (this.container && !!this.container.isPreferredApiMongoDB()) {
if (userContext.apiType === "Mongo") {
return "e.g., address.zipCode";
}
@@ -376,7 +376,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
});
this.uniqueKeysPlaceholder = ko.pureComputed<string>(() => {
if (this.container && !!this.container.isPreferredApiMongoDB()) {
if (userContext.apiType === "Mongo") {
return "Comma separated paths e.g. firstName,address.zipCode";
}
@@ -396,11 +396,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return false;
}
if (
this.container.isPreferredApiMongoDB() &&
!this.isUnlimitedStorageSelected() &&
this.databaseHasSharedOffer()
) {
if (userContext.apiType === "Mongo" && !this.isUnlimitedStorageSelected() && this.databaseHasSharedOffer()) {
return false;
}
@@ -480,7 +476,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
userContext.portalEnv,
this.isFreeTierAccount(),
this.container.isFirstResourceCreated(),
this.container.defaultExperience(),
userContext.apiType,
true
);
});
@@ -589,7 +585,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
return true;
}
if (this.container.isPreferredApiMongoDB()) {
if (userContext.apiType === "Mongo") {
return true;
}
@@ -728,6 +724,10 @@ export default class AddCollectionPane extends ContextualPaneBase {
}
}
private isMongo(): boolean {
return userContext.apiType === "Mongo";
}
private _onDatabasesChange(newDatabaseIds: ViewModels.Database[]) {
this.databaseIds(newDatabaseIds?.map((database: ViewModels.Database) => database.id()));
}
@@ -810,7 +810,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
let indexingPolicy: DataModels.IndexingPolicy;
let createMongoWildcardIndex: boolean;
// todo - remove mongo indexing policy ticket # 616274
if (this.container.isPreferredApiMongoDB() && this.container.isEnableMongoCapabilityPresent()) {
if (userContext.apiType === "Mongo" && this.container.isEnableMongoCapabilityPresent()) {
createMongoWildcardIndex = this.shouldCreateMongoWildcardIndex();
} else if (this.showIndexingOptionsForSharedThroughput()) {
if (this.useIndexingForSharedThroughput()) {
@@ -1145,7 +1145,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
let transform = (value: string) => {
return value;
};
if (this.container.isPreferredApiMongoDB()) {
if (userContext.apiType === "Mongo") {
transform = (value: string) => {
return this._convertShardKeyToPartitionKey(value);
};

View File

@@ -19,7 +19,6 @@ import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import { SubscriptionType } from "../../Contracts/SubscriptionType";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { CollectionCreation, IndexingPolicies } from "../../Shared/Constants";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
@@ -68,16 +67,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
super(props);
this.state = {
createNewDatabase: userContext.defaultExperience !== DefaultAccountExperienceType.Table,
createNewDatabase: userContext.apiType !== "Tables",
newDatabaseId: "",
isSharedThroughputChecked: this.getSharedThroughputDefault(),
selectedDatabaseId:
userContext.defaultExperience === DefaultAccountExperienceType.Table
? CollectionCreation.TablesAPIDefaultDatabase
: undefined,
selectedDatabaseId: userContext.apiType === "Tables" ? CollectionCreation.TablesAPIDefaultDatabase : undefined,
collectionId: "",
enableIndexing: true,
isSharded: userContext.defaultExperience !== DefaultAccountExperienceType.Table,
isSharded: userContext.apiType !== "Tables",
partitionKey: "",
enableDedicatedThroughput: false,
createMongoWildCardIndex: true,
@@ -108,7 +104,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
userContext.portalEnv,
true,
this.props.explorer.isFirstResourceCreated(),
userContext.defaultExperience,
userContext.apiType,
true
)}
messageType="info"
@@ -120,7 +116,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
)}
<div className="panelMainContent">
<Stack hidden={userContext.defaultExperience === DefaultAccountExperienceType.Table}>
<Stack hidden={userContext.apiType === "Tables"}>
<Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span>
<Text className="panelTextBold" variant="small">
@@ -317,7 +313,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack>
)}
{userContext.defaultExperience === DefaultAccountExperienceType.MongoDB &&
{userContext.apiType === "Mongo" &&
(!this.state.isSharedThroughputChecked ||
this.props.explorer.isFixedCollectionWithSharedThroughputSupported()) && (
<Stack>
@@ -393,12 +389,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
className="panelTextField"
placeholder={this.getPartitionKeyPlaceHolder()}
aria-label={this.getPartitionKeyName()}
pattern={userContext.defaultExperience === DefaultAccountExperienceType.Graph ? "^/[^/]*" : ".*"}
title={
userContext.defaultExperience === DefaultAccountExperienceType.Graph
? "May not use composite partition key"
: ""
}
pattern={userContext.apiType === "Gremlin" ? "^/[^/]*" : ".*"}
title={userContext.apiType === "Gremlin" ? "May not use composite partition key" : ""}
value={this.state.partitionKey}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ partitionKey: event.target.value })
@@ -447,7 +439,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
/>
)}
{userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB && (
{userContext.apiType === "SQL" && (
<Stack>
<Stack horizontal>
<Text className="panelTextBold" variant="small">
@@ -471,7 +463,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
type="text"
autoComplete="off"
placeholder={
userContext.defaultExperience === DefaultAccountExperienceType.MongoDB
userContext.apiType === "Mongo"
? "Comma separated paths e.g. firstName,address.zipCode"
: "Comma separated paths e.g. /firstName,/address/zipCode"
}
@@ -545,7 +537,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack>
)}
{userContext.defaultExperience === DefaultAccountExperienceType.DocumentDB && (
{userContext.apiType === "SQL" && (
<Stack className="panelGroupSpacing">
<Stack horizontal verticalAlign="start">
<Checkbox
@@ -657,30 +649,30 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
private getCollectionName(): string {
switch (userContext.defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
switch (userContext.apiType) {
case "SQL":
return "Container";
case DefaultAccountExperienceType.MongoDB:
case "Mongo":
return "Collection";
case DefaultAccountExperienceType.Cassandra:
case DefaultAccountExperienceType.Table:
case "Cassandra":
case "Tables":
return "Table";
case DefaultAccountExperienceType.Graph:
case "Gremlin":
return "Graph";
default:
throw new Error(`Unsupported default experience type: ${userContext.defaultExperience}`);
throw new Error(`Unsupported default experience type: ${userContext.apiType}`);
}
}
private getPartitionKeyName(): string {
return userContext.defaultExperience === DefaultAccountExperienceType.MongoDB ? "Shard key" : "Partition key";
return userContext.apiType === "Mongo" ? "Shard key" : "Partition key";
}
private getPartitionKeyPlaceHolder(): string {
switch (userContext.defaultExperience) {
case DefaultAccountExperienceType.MongoDB:
switch (userContext.apiType) {
case "Mongo":
return "e.g., address.zipCode";
case DefaultAccountExperienceType.Graph:
case "Gremlin":
return "e.g., /address";
default:
return "e.g., /address/zipCode";
@@ -817,11 +809,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false;
}
switch (userContext.defaultExperience) {
case DefaultAccountExperienceType.DocumentDB:
case DefaultAccountExperienceType.MongoDB:
switch (userContext.apiType) {
case "SQL":
case "Mongo":
return true;
case DefaultAccountExperienceType.Cassandra:
case "Cassandra":
return this.props.explorer.hasStorageAnalyticsAfecFeature();
default:
return false;
@@ -855,7 +847,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
const validPaths: string[] = uniqueKey.split(",")?.filter((path) => path?.length > 0);
const trimmedPaths: string[] = validPaths?.map((path) => path.trim());
if (trimmedPaths?.length > 0) {
if (userContext.defaultExperience === DefaultAccountExperienceType.MongoDB) {
if (userContext.apiType === "Mongo") {
trimmedPaths.map((path) => {
const transformedPath = path.split(".").join("/");
if (transformedPath[0] !== "/") {
@@ -888,7 +880,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
}
if (
userContext.defaultExperience === DefaultAccountExperienceType.Graph &&
userContext.apiType === "Gremlin" &&
(this.state.partitionKey === "/id" || this.state.partitionKey === "/label")
) {
this.setState({ errorMessage: "/id and /label as partition keys are not allowed for graph." });
@@ -924,7 +916,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
let databaseId = this.state.createNewDatabase ? this.state.newDatabaseId.trim() : this.state.selectedDatabaseId;
let partitionKeyString = this.state.partitionKey.trim();
if (userContext.defaultExperience === DefaultAccountExperienceType.Table) {
if (userContext.apiType === "Tables") {
// Table require fixed Database: TablesDB, and fixed Partition Key: /'$pk'
databaseId = CollectionCreation.TablesAPIDefaultDatabase;
partitionKeyString = "/'$pk'";

View File

@@ -238,7 +238,7 @@ export default class AddDatabasePane extends ContextualPaneBase {
userContext.portalEnv,
this.isFreeTierAccount(),
this.container.isFirstResourceCreated(),
this.container.defaultExperience(),
userContext.apiType,
false
);
});

View File

@@ -2,7 +2,6 @@ import * as ko from "knockout";
import * as _ from "underscore";
import * as Constants from "../../Common/Constants";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { HashMap } from "../../Common/HashMap";
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
@@ -51,7 +50,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
public ruToolTipText: ko.Computed<string>;
public canConfigureThroughput: ko.PureComputed<boolean>;
private keyspaceOffers: HashMap<DataModels.Offer>;
private keyspaceOffers: Map<string, DataModels.Offer>;
constructor(options: ViewModels.PaneOptions) {
super(options);
@@ -60,7 +59,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
this.keyspaceCreateNew = ko.observable<boolean>(true);
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText());
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.keyspaceOffers = new HashMap<DataModels.Offer>();
this.keyspaceOffers = new Map();
this.keyspaceIds = ko.observableArray<string>();
this.keyspaceHasSharedOffer = ko.observable<boolean>(false);
this.keyspaceThroughput = ko.observable<number>();

View File

@@ -7,7 +7,6 @@ import { deleteCollection } from "../../../Common/dataAccess/deleteCollection";
import DeleteFeedback from "../../../Common/DeleteFeedback";
import { ApiKind, DatabaseAccount } from "../../../Contracts/DataModels";
import { Collection, Database, TreeNode } from "../../../Contracts/ViewModels";
import { DefaultAccountExperienceType } from "../../../DefaultAccountExperienceType";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../../../UserContext";
@@ -107,7 +106,7 @@ describe("Delete Collection Confirmation Pane", () => {
},
id: "testDatabaseAccountId",
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB,
apiType: "SQL",
});
(deleteCollection as jest.Mock).mockResolvedValue(undefined);
(TelemetryProcessor.trace as jest.Mock).mockReturnValue(undefined);

View File

@@ -73,7 +73,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
const deleteFeedback = new DeleteFeedback(
userContext.databaseAccount?.id,
userContext.databaseAccount?.name,
DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.defaultExperience),
DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.apiType),
deleteCollectionFeedback
);

View File

@@ -7,7 +7,6 @@ import { deleteDatabase } from "../../Common/dataAccess/deleteDatabase";
import DeleteFeedback from "../../Common/DeleteFeedback";
import { ApiKind, DatabaseAccount } from "../../Contracts/DataModels";
import { Collection, Database } from "../../Contracts/ViewModels";
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { updateUserContext } from "../../UserContext";
@@ -68,7 +67,7 @@ describe("Delete Database Confirmation Pane", () => {
},
id: "testDatabaseAccountId",
} as DatabaseAccount,
defaultExperience: DefaultAccountExperienceType.DocumentDB,
apiType: "SQL",
});
(deleteDatabase as jest.Mock).mockResolvedValue(undefined);
(TelemetryProcessor.trace as jest.Mock).mockReturnValue(undefined);

View File

@@ -94,7 +94,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
const deleteFeedback = new DeleteFeedback(
userContext?.databaseAccount.id,
userContext?.databaseAccount.name,
DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.defaultExperience),
DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.apiType),
databaseFeedbackInput
);

View File

@@ -3,7 +3,6 @@ import AddDatabasePaneTemplate from "./AddDatabasePane.html";
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
import GitHubReposPaneTemplate from "./GitHubReposPane.html";
import GraphStylingPaneTemplate from "./GraphStylingPane.html";
import StringInputPaneTemplate from "./StringInputPane.html";
import TableAddEntityPaneTemplate from "./Tables/TableAddEntityPane.html";
import TableEditEntityPaneTemplate from "./Tables/TableEditEntityPane.html";
@@ -66,15 +65,6 @@ export class CassandraAddCollectionPaneComponent {
}
}
export class StringInputPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: StringInputPaneTemplate,
};
}
}
export class GitHubReposPaneComponent {
constructor() {
return {

View File

@@ -8,14 +8,15 @@ describe("PublishNotebookPaneComponent", () => {
notebookName: "SampleNotebook.ipynb",
notebookDescription: "sample description",
notebookTags: "tag1, tag2",
imageSrc: "https://i.ytimg.com/vi/E_lByLdKeKY/maxresdefault.jpg",
notebookAuthor: "CosmosDB",
notebookCreatedDate: "2020-07-17T00:00:00Z",
notebookObject: undefined,
notebookParentDomElement: undefined,
onChangeName: undefined,
onChangeDescription: undefined,
onChangeTags: undefined,
onChangeImageSrc: undefined,
setNotebookName: undefined,
setNotebookDescription: undefined,
setNotebookTags: undefined,
setImageSrc: undefined,
onError: undefined,
clearFormError: undefined,
};

View File

@@ -0,0 +1,205 @@
import { ImmutableNotebook, toJS } from "@nteract/commutable";
import React, { FunctionComponent, useEffect, useState } from "react";
import { HttpStatusCodes } from "../../../Common/Constants";
import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
import { JunoClient } from "../../../Juno/JunoClient";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { CodeOfConductComponent } from "../../Controls/NotebookGallery/CodeOfConductComponent";
import { GalleryTab } from "../../Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../../Explorer";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import {
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
export interface PublishNotebookPaneAProps {
explorer: Explorer;
closePanel: () => void;
openNotificationConsole: () => void;
junoClient: JunoClient;
name: string;
author: string;
notebookContent: string | ImmutableNotebook;
parentDomElement: HTMLElement;
}
export const PublishNotebookPane: FunctionComponent<PublishNotebookPaneAProps> = ({
explorer: container,
junoClient,
closePanel,
name,
author,
notebookContent,
parentDomElement,
}: PublishNotebookPaneAProps): JSX.Element => {
const [isCodeOfConductAccepted, setIsCodeOfConductAccepted] = useState<boolean>(false);
const [content, setContent] = useState<string>("");
const [formError, setFormError] = useState<string>("");
const [formErrorDetail, setFormErrorDetail] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>();
const [notebookName, setNotebookName] = useState<string>(name);
const [notebookDescription, setNotebookDescription] = useState<string>("");
const [notebookTags, setNotebookTags] = useState<string>("");
const [imageSrc, setImageSrc] = useState<string>();
const CodeOfConductAccepted = async () => {
try {
const response = await junoClient.isCodeOfConductAccepted();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
}
setIsCodeOfConductAccepted(response.data);
} catch (error) {
handleError(
error,
"PublishNotebookPaneAdapter/isCodeOfConductAccepted",
"Failed to check if code of conduct was accepted"
);
}
};
const [notebookObject, setNotebookObject] = useState<ImmutableNotebook>();
useEffect(() => {
CodeOfConductAccepted();
let newContent;
if (typeof notebookContent === "string") {
newContent = notebookContent as string;
} else {
newContent = JSON.stringify(toJS(notebookContent));
setNotebookObject(notebookContent);
}
setContent(newContent);
}, []);
const submit = async (): Promise<void> => {
const clearPublishingMessage = NotificationConsoleUtils.logConsoleProgress(`Publishing ${name} to gallery`);
setIsExecuting(true);
let startKey: number;
if (!notebookName || !notebookDescription || !author || !imageSrc) {
setFormError(`Failed to publish ${notebookName} to gallery`);
setFormErrorDetail("Name, description, author and cover image are required");
createFormError(formError, formErrorDetail, "PublishNotebookPaneAdapter/submit");
setIsExecuting(false);
return;
}
try {
startKey = traceStart(Action.NotebooksGalleryPublish, {});
const response = await junoClient.publishNotebook(
notebookName,
notebookDescription,
notebookTags?.split(","),
author,
imageSrc,
content
);
const data = response.data;
if (data) {
let isPublishPending = false;
if (data.pendingScanJobIds?.length > 0) {
isPublishPending = true;
NotificationConsoleUtils.logConsoleInfo(
`Content of ${name} is currently being scanned for illegal content. It will not be available in the public gallery until the review is complete (may take a few days).`
);
} else {
NotificationConsoleUtils.logConsoleInfo(`Published ${notebookName} to gallery`);
container.openGallery(GalleryTab.Published);
}
traceSuccess(
Action.NotebooksGalleryPublish,
{
notebookId: data.id,
isPublishPending,
},
startKey
);
}
} catch (error) {
traceFailure(
Action.NotebooksGalleryPublish,
{
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
const errorMessage = getErrorMessage(error);
setFormError(`Failed to publish ${FileSystemUtil.stripExtension(notebookName, "ipynb")} to gallery`);
setFormErrorDetail(`${errorMessage}`);
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", formError);
return;
} finally {
clearPublishingMessage();
setIsExecuting(false);
}
closePanel();
};
const createFormError = (formError: string, formErrorDetail: string, area: string): void => {
setFormError(formError);
setFormErrorDetail(formErrorDetail);
handleError(formErrorDetail, area, formError);
};
const clearFormError = (): void => {
setFormError("");
setFormErrorDetail("");
};
const props: GenericRightPaneProps = {
container: container,
formError: formError,
formErrorDetail: formErrorDetail,
id: "publishnotebookpane",
isExecuting: isExecuting,
title: "Publish to gallery",
submitButtonText: "Publish",
onSubmit: () => submit(),
onClose: closePanel,
isSubmitButtonHidden: !isCodeOfConductAccepted,
};
const publishNotebookPaneProps: PublishNotebookPaneProps = {
notebookDescription,
notebookTags,
imageSrc,
notebookName,
notebookAuthor: author,
notebookCreatedDate: new Date().toISOString(),
notebookObject: notebookObject,
notebookParentDomElement: parentDomElement,
onError: createFormError,
clearFormError: clearFormError,
setNotebookName,
setNotebookDescription,
setNotebookTags,
setImageSrc,
};
return (
<GenericRightPaneComponent {...props}>
{!isCodeOfConductAccepted ? (
<div style={{ padding: "25px", marginTop: "10px" }}>
<CodeOfConductComponent
junoClient={junoClient}
onAcceptCodeOfConduct={(isAccepted) => {
setIsCodeOfConductAccepted(isAccepted);
}}
/>
</div>
) : (
<PublishNotebookPaneComponent {...publishNotebookPaneProps} />
)}
</GenericRightPaneComponent>
);
};

View File

@@ -0,0 +1,297 @@
import { ImmutableNotebook } from "@nteract/commutable";
import Html2Canvas from "html2canvas";
import { Dropdown, IDropdownProps, ITextFieldProps, Stack, Text, TextField } from "office-ui-fabric-react";
import React, { FunctionComponent, useState } from "react";
import { GalleryCardComponent } from "../../Controls/NotebookGallery/Cards/GalleryCardComponent";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import { NotebookUtil } from "../../Notebook/NotebookUtil";
import "./styled.less";
export interface PublishNotebookPaneProps {
notebookName: string;
notebookAuthor: string;
notebookTags: string;
imageSrc: string;
notebookDescription: string;
notebookCreatedDate: string;
notebookObject: ImmutableNotebook;
notebookParentDomElement?: HTMLElement;
onError: (formError: string, formErrorDetail: string, area: string) => void;
clearFormError: () => void;
setNotebookName: (newValue: string) => void;
setNotebookDescription: (newValue: string) => void;
setNotebookTags: (newValue: string) => void;
setImageSrc: (newValue: string) => void;
}
enum ImageTypes {
Url = "URL",
CustomImage = "Custom Image",
TakeScreenshot = "Take Screenshot",
UseFirstDisplayOutput = "Use First Display Output",
}
export const PublishNotebookPaneComponent: FunctionComponent<PublishNotebookPaneProps> = ({
notebookName,
notebookTags,
imageSrc,
notebookDescription,
notebookAuthor,
notebookCreatedDate,
notebookObject,
notebookParentDomElement,
onError,
clearFormError,
setNotebookName,
setNotebookDescription,
setNotebookTags,
setImageSrc,
}: PublishNotebookPaneProps) => {
const [type, setType] = useState<string>(ImageTypes.CustomImage);
const CARD_WIDTH = 256;
const cardImageHeight = 144;
const cardHeightToWidthRatio = cardImageHeight / CARD_WIDTH;
const maxImageSizeInMib = 1.5;
const descriptionPara1 =
"When published, this notebook will appear in the Azure Cosmos DB notebooks public gallery. Make sure you have removed any sensitive data or output before publishing.";
const descriptionPara2 = `Would you like to publish and share "${FileSystemUtil.stripExtension(
notebookName,
"ipynb"
)}" to the gallery?`;
const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url];
const thumbnailSelectorProps: IDropdownProps = {
label: "Cover image",
defaultSelectedKey: ImageTypes.CustomImage,
ariaLabel: "Cover image",
options: options.map((value: string) => ({ text: value, key: value })),
onChange: async (event, options) => {
setImageSrc("");
clearFormError();
if (options.text === ImageTypes.TakeScreenshot) {
try {
await takeScreenshot(notebookParentDomElement, screenshotErrorHandler);
} catch (error) {
screenshotErrorHandler(error);
}
} else if (options.text === ImageTypes.UseFirstDisplayOutput) {
try {
await takeScreenshot(findFirstOutput(), firstOutputErrorHandler);
} catch (error) {
firstOutputErrorHandler(error);
}
}
setType(options.text);
},
};
const thumbnailUrlProps: ITextFieldProps = {
label: "Cover image url",
ariaLabel: "Cover image url",
required: true,
onChange: (event, newValue) => {
setImageSrc(newValue);
},
};
const screenshotErrorHandler = (error: Error) => {
const formError = "Failed to take screen shot";
const formErrorDetail = `${error}`;
const area = "PublishNotebookPaneComponent/takeScreenshot";
onError(formError, formErrorDetail, area);
};
const firstOutputErrorHandler = (error: Error) => {
const formError = "Failed to capture first output";
const formErrorDetail = `${error}`;
const area = "PublishNotebookPaneComponent/UseFirstOutput";
onError(formError, formErrorDetail, area);
};
if (notebookParentDomElement) {
options.push(ImageTypes.TakeScreenshot);
if (notebookObject) {
options.push(ImageTypes.UseFirstDisplayOutput);
}
}
const imageToBase64 = (file: File, updateImageSrc: (result: string) => void) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
updateImageSrc(reader.result.toString());
};
reader.onerror = (error) => {
const formError = `Failed to convert ${file.name} to base64 format`;
const formErrorDetail = `${error}`;
const area = "PublishNotebookPaneComponent/selectImageFile";
onError(formError, formErrorDetail, area);
};
};
const takeScreenshot = (target: HTMLElement, onError: (error: Error) => void): void => {
const updateImageSrcWithScreenshot = (canvasUrl: string): void => {
setImageSrc(canvasUrl);
};
target.scrollIntoView();
Html2Canvas(target, {
useCORS: true,
allowTaint: true,
scale: 1,
logging: true,
})
.then((canvas) => {
//redraw canvas to fit Card Cover Image dimensions
const originalImageData = canvas.toDataURL();
const requiredHeight = parseInt(canvas.style.width.split("px")[0]) * cardHeightToWidthRatio;
canvas.height = requiredHeight;
const context = canvas.getContext("2d");
const image = new Image();
image.src = originalImageData;
image.onload = () => {
context.drawImage(image, 0, 0);
updateImageSrcWithScreenshot(canvas.toDataURL());
};
})
.catch((error) => {
onError(error);
});
};
const renderThumbnailSelectors = (type: string) => {
switch (type) {
case ImageTypes.Url:
return <TextField {...thumbnailUrlProps} />;
case ImageTypes.CustomImage:
return (
<input
id="selectImageFile"
type="file"
accept="image/*"
onChange={(event) => {
const file = event.target.files[0];
if (file.size / 1024 ** 2 > maxImageSizeInMib) {
event.target.value = "";
const formError = `Failed to upload ${file.name}`;
const formErrorDetail = `Image is larger than ${maxImageSizeInMib} MiB. Please Choose a different image.`;
const area = "PublishNotebookPaneComponent/selectImageFile";
onError(formError, formErrorDetail, area);
setImageSrc("");
return;
} else {
clearFormError();
}
imageToBase64(file, (result: string) => {
setImageSrc(result);
});
}}
/>
);
default:
return <></>;
}
};
const findFirstOutput = (): HTMLElement => {
const indexOfFirstCodeCellWithDisplay = NotebookUtil.findFirstCodeCellWithDisplay(notebookObject);
const cellOutputDomElements = notebookParentDomElement.querySelectorAll<HTMLElement>(".nteract-cell-outputs");
return cellOutputDomElements[indexOfFirstCodeCellWithDisplay];
};
return (
<div className="publishNotebookPanelContent">
<Stack className="panelMainContent" tokens={{ childrenGap: 20 }}>
<Stack.Item>
<Text>{descriptionPara1}</Text>
</Stack.Item>
<Stack.Item>
<Text>{descriptionPara2}</Text>
</Stack.Item>
<Stack.Item>
<TextField
label="Name"
ariaLabel="Name"
defaultValue={FileSystemUtil.stripExtension(notebookName, "ipynb")}
required
onChange={(event, newValue) => {
const notebookName = newValue + ".ipynb";
setNotebookName(notebookName);
}}
/>
</Stack.Item>
<Stack.Item>
<TextField
label="Description"
ariaLabel="Description"
multiline
rows={3}
required
onChange={(event, newValue) => {
setNotebookDescription(newValue);
}}
/>
</Stack.Item>
<Stack.Item>
<TextField
label="Tags"
ariaLabel="Tags"
placeholder="Optional tag 1, Optional tag 2"
onChange={(event, newValue) => {
setNotebookTags(newValue);
}}
/>
</Stack.Item>
<Stack.Item>
<Dropdown {...thumbnailSelectorProps} />
</Stack.Item>
<Stack.Item>{renderThumbnailSelectors(type)}</Stack.Item>
<Stack.Item>
<Text>Preview</Text>
</Stack.Item>
<Stack.Item>
<GalleryCardComponent
data={{
id: undefined,
name: notebookName,
description: notebookDescription,
gitSha: undefined,
tags: notebookTags.split(","),
author: notebookAuthor,
thumbnailUrl: imageSrc,
created: notebookCreatedDate,
isSample: false,
downloads: undefined,
favorites: undefined,
views: undefined,
newCellId: undefined,
policyViolations: undefined,
pendingScanJobIds: undefined,
}}
isFavorite={undefined}
showDownload={false}
showDelete={false}
onClick={() => undefined}
onTagClick={undefined}
onFavoriteClick={undefined}
onUnfavoriteClick={undefined}
onDownloadClick={undefined}
onDeleteClick={undefined}
/>
</Stack.Item>
</Stack>
</div>
);
};

View File

@@ -88,7 +88,7 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
Object {
"author": "CosmosDB",
"created": "2020-07-17T00:00:00Z",
"description": "",
"description": "sample description",
"downloads": undefined,
"favorites": undefined,
"gitSha": undefined,
@@ -99,12 +99,14 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
"pendingScanJobIds": undefined,
"policyViolations": undefined,
"tags": Array [
"",
"tag1",
" tag2",
],
"thumbnailUrl": undefined,
"thumbnailUrl": "https://i.ytimg.com/vi/E_lByLdKeKY/maxresdefault.jpg",
"views": undefined,
}
}
onClick={[Function]}
showDelete={false}
showDownload={false}
/>

View File

@@ -1,241 +0,0 @@
import { toJS } from "@nteract/commutable";
import { ImmutableNotebook } from "@nteract/commutable/src";
import ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { HttpStatusCodes } from "../../Common/Constants";
import { getErrorMessage, getErrorStack, handleError } from "../../Common/ErrorHandlingUtils";
import { JunoClient } from "../../Juno/JunoClient";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent";
import { GalleryTab } from "../Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../Explorer";
import * as FileSystemUtil from "../Notebook/FileSystemUtil";
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
import { RightPaneForm, RightPaneFormProps } from "./RightPaneForm/RightPaneForm";
export class PublishNotebookPaneAdapter implements ReactAdapter {
parameters: ko.Observable<number>;
private isOpened: boolean;
private isExecuting: boolean;
private formError: string;
private formErrorDetail: string;
private name: string;
private author: string;
private content: string;
private description: string;
private tags: string;
private imageSrc: string;
private notebookObject: ImmutableNotebook;
private parentDomElement: HTMLElement;
private isCodeOfConductAccepted: boolean;
constructor(private container: Explorer, private junoClient: JunoClient) {
this.parameters = ko.observable(Date.now());
this.reset();
this.triggerRender();
}
public renderComponent(): JSX.Element {
if (!this.isOpened) {
return undefined;
}
const props: RightPaneFormProps = {
container: this.container,
formError: this.formError,
formErrorDetail: this.formErrorDetail,
id: "publishnotebookpane",
isExecuting: this.isExecuting,
title: "Publish to gallery",
submitButtonText: "Publish",
onClose: () => this.close(),
onSubmit: () => this.submit(),
isSubmitButtonHidden: !this.isCodeOfConductAccepted,
};
const publishNotebookPaneProps: PublishNotebookPaneProps = {
notebookName: this.name,
notebookDescription: "",
notebookTags: "",
notebookAuthor: this.author,
notebookCreatedDate: new Date().toISOString(),
notebookObject: this.notebookObject,
notebookParentDomElement: this.parentDomElement,
onChangeName: (newValue: string) => (this.name = newValue),
onChangeDescription: (newValue: string) => (this.description = newValue),
onChangeTags: (newValue: string) => (this.tags = newValue),
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
onError: this.createFormError,
clearFormError: this.clearFormError,
};
return (
<RightPaneForm {...props}>
{!this.isCodeOfConductAccepted ? (
<div style={{ padding: "15px", marginTop: "10px" }}>
<CodeOfConductComponent
junoClient={this.junoClient}
onAcceptCodeOfConduct={() => {
this.isCodeOfConductAccepted = true;
this.triggerRender();
}}
/>
</div>
) : (
<PublishNotebookPaneComponent {...publishNotebookPaneProps} />
)}
</RightPaneForm>
);
}
public triggerRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
public async open(
name: string,
author: string,
notebookContent: string | ImmutableNotebook,
parentDomElement: HTMLElement
): Promise<void> {
try {
const response = await this.junoClient.isCodeOfConductAccepted();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
}
this.isCodeOfConductAccepted = response.data;
} catch (error) {
handleError(
error,
"PublishNotebookPaneAdapter/isCodeOfConductAccepted",
"Failed to check if code of conduct was accepted"
);
}
this.name = name;
this.author = author;
if (typeof notebookContent === "string") {
this.content = notebookContent as string;
} else {
this.content = JSON.stringify(toJS(notebookContent));
this.notebookObject = notebookContent;
}
this.parentDomElement = parentDomElement;
this.isOpened = true;
this.triggerRender();
}
public close(): void {
this.reset();
this.triggerRender();
}
public async submit(): Promise<void> {
const clearPublishingMessage = NotificationConsoleUtils.logConsoleProgress(`Publishing ${this.name} to gallery`);
this.isExecuting = true;
this.triggerRender();
let startKey: number;
if (!this.name || !this.description || !this.author || !this.imageSrc) {
const formError = `Failed to publish ${this.name} to gallery`;
const formErrorDetail = "Name, description, author and cover image are required";
this.createFormError(formError, formErrorDetail, "PublishNotebookPaneAdapter/submit");
this.isExecuting = false;
return;
}
try {
startKey = traceStart(Action.NotebooksGalleryPublish, {});
const response = await this.junoClient.publishNotebook(
this.name,
this.description,
this.tags?.split(","),
this.author,
this.imageSrc,
this.content
);
const data = response.data;
if (data) {
let isPublishPending = false;
if (data.pendingScanJobIds?.length > 0) {
isPublishPending = true;
NotificationConsoleUtils.logConsoleInfo(
`Content of ${this.name} is currently being scanned for illegal content. It will not be available in the public gallery until the review is complete (may take a few days).`
);
} else {
NotificationConsoleUtils.logConsoleInfo(`Published ${this.name} to gallery`);
this.container.openGallery(GalleryTab.Published);
}
traceSuccess(
Action.NotebooksGalleryPublish,
{
notebookId: data.id,
isPublishPending,
},
startKey
);
}
} catch (error) {
traceFailure(
Action.NotebooksGalleryPublish,
{
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
const errorMessage = getErrorMessage(error);
this.formError = `Failed to publish ${FileSystemUtil.stripExtension(this.name, "ipynb")} to gallery`;
this.formErrorDetail = `${errorMessage}`;
handleError(errorMessage, "PublishNotebookPaneAdapter/submit", this.formError);
return;
} finally {
clearPublishingMessage();
this.isExecuting = false;
this.triggerRender();
}
this.close();
}
private createFormError = (formError: string, formErrorDetail: string, area: string): void => {
this.formError = formError;
this.formErrorDetail = formErrorDetail;
handleError(formErrorDetail, area, formError);
this.triggerRender();
};
private clearFormError = (): void => {
this.formError = undefined;
this.formErrorDetail = undefined;
this.triggerRender();
};
private reset = (): void => {
this.isOpened = false;
this.isExecuting = false;
this.formError = undefined;
this.formErrorDetail = undefined;
this.name = undefined;
this.author = undefined;
this.content = undefined;
this.description = undefined;
this.tags = undefined;
this.imageSrc = undefined;
this.notebookObject = undefined;
this.parentDomElement = undefined;
this.isCodeOfConductAccepted = undefined;
};
}

View File

@@ -1,326 +0,0 @@
import { ITextFieldProps, Stack, Text, TextField, Dropdown, IDropdownProps } from "office-ui-fabric-react";
import * as React from "react";
import { GalleryCardComponent } from "../Controls/NotebookGallery/Cards/GalleryCardComponent";
import * as FileSystemUtil from "../Notebook/FileSystemUtil";
import "./PublishNotebookPaneComponent.less";
import Html2Canvas from "html2canvas";
import { ImmutableNotebook } from "@nteract/commutable/src";
import { NotebookUtil } from "../Notebook/NotebookUtil";
export interface PublishNotebookPaneProps {
notebookName: string;
notebookDescription: string;
notebookTags: string;
notebookAuthor: string;
notebookCreatedDate: string;
notebookObject: ImmutableNotebook;
notebookParentDomElement?: HTMLElement;
onChangeName: (newValue: string) => void;
onChangeDescription: (newValue: string) => void;
onChangeTags: (newValue: string) => void;
onChangeImageSrc: (newValue: string) => void;
onError: (formError: string, formErrorDetail: string, area: string) => void;
clearFormError: () => void;
}
interface PublishNotebookPaneState {
type: string;
notebookName: string;
notebookDescription: string;
notebookTags: string;
imageSrc: string;
}
enum ImageTypes {
Url = "URL",
CustomImage = "Custom Image",
TakeScreenshot = "Take Screenshot",
UseFirstDisplayOutput = "Use First Display Output",
}
export class PublishNotebookPaneComponent extends React.Component<PublishNotebookPaneProps, PublishNotebookPaneState> {
private static readonly maxImageSizeInMib = 1.5;
private descriptionPara1: string;
private descriptionPara2: string;
private nameProps: ITextFieldProps;
private descriptionProps: ITextFieldProps;
private tagsProps: ITextFieldProps;
private thumbnailUrlProps: ITextFieldProps;
private thumbnailSelectorProps: IDropdownProps;
private imageToBase64: (file: File, updateImageSrc: (result: string) => void) => void;
private takeScreenshot: (target: HTMLElement, onError: (error: Error) => void) => void;
constructor(props: PublishNotebookPaneProps) {
super(props);
this.state = {
type: ImageTypes.CustomImage,
notebookName: props.notebookName,
notebookDescription: "",
notebookTags: "",
imageSrc: undefined,
};
this.imageToBase64 = (file: File, updateImageSrc: (result: string) => void) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
updateImageSrc(reader.result.toString());
};
const onError = this.props.onError;
reader.onerror = (error) => {
const formError = `Failed to convert ${file.name} to base64 format`;
const formErrorDetail = `${error}`;
const area = "PublishNotebookPaneComponent/selectImageFile";
onError(formError, formErrorDetail, area);
};
};
this.takeScreenshot = (target: HTMLElement, onError: (error: Error) => void): void => {
const updateImageSrcWithScreenshot = (canvasUrl: string): void => {
this.props.onChangeImageSrc(canvasUrl);
this.setState({ imageSrc: canvasUrl });
};
target.scrollIntoView();
Html2Canvas(target, {
useCORS: true,
allowTaint: true,
scale: 1,
logging: true,
})
.then((canvas) => {
//redraw canvas to fit Card Cover Image dimensions
const originalImageData = canvas.toDataURL();
const requiredHeight =
parseInt(canvas.style.width.split("px")[0]) * GalleryCardComponent.cardHeightToWidthRatio;
canvas.height = requiredHeight;
const context = canvas.getContext("2d");
const image = new Image();
image.src = originalImageData;
image.onload = () => {
context.drawImage(image, 0, 0);
updateImageSrcWithScreenshot(canvas.toDataURL());
};
})
.catch((error) => {
onError(error);
});
};
this.descriptionPara1 =
"When published, this notebook will appear in the Azure Cosmos DB notebooks public gallery. Make sure you have removed any sensitive data or output before publishing.";
this.descriptionPara2 = `Would you like to publish and share "${FileSystemUtil.stripExtension(
this.props.notebookName,
"ipynb"
)}" to the gallery?`;
this.thumbnailUrlProps = {
label: "Cover image url",
ariaLabel: "Cover image url",
required: true,
onChange: (event, newValue) => {
this.props.onChangeImageSrc(newValue);
this.setState({ imageSrc: newValue });
},
};
const screenshotErrorHandler = (error: Error) => {
const formError = "Failed to take screen shot";
const formErrorDetail = `${error}`;
const area = "PublishNotebookPaneComponent/takeScreenshot";
this.props.onError(formError, formErrorDetail, area);
};
const firstOutputErrorHandler = (error: Error) => {
const formError = "Failed to capture first output";
const formErrorDetail = `${error}`;
const area = "PublishNotebookPaneComponent/UseFirstOutput";
this.props.onError(formError, formErrorDetail, area);
};
const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url];
if (this.props.notebookParentDomElement) {
options.push(ImageTypes.TakeScreenshot);
if (this.props.notebookObject) {
options.push(ImageTypes.UseFirstDisplayOutput);
}
}
this.thumbnailSelectorProps = {
label: "Cover image",
defaultSelectedKey: ImageTypes.CustomImage,
ariaLabel: "Cover image",
options: options.map((value: string) => ({ text: value, key: value })),
onChange: async (event, options) => {
this.setState({ imageSrc: undefined });
this.props.onChangeImageSrc(undefined);
this.props.clearFormError();
if (options.text === ImageTypes.TakeScreenshot) {
try {
await this.takeScreenshot(this.props.notebookParentDomElement, screenshotErrorHandler);
} catch (error) {
screenshotErrorHandler(error);
}
} else if (options.text === ImageTypes.UseFirstDisplayOutput) {
try {
await this.takeScreenshot(this.findFirstOutput(), firstOutputErrorHandler);
} catch (error) {
firstOutputErrorHandler(error);
}
}
this.setState({ type: options.text });
},
};
this.nameProps = {
label: "Name",
ariaLabel: "Name",
defaultValue: FileSystemUtil.stripExtension(this.props.notebookName, "ipynb"),
required: true,
onChange: (event, newValue) => {
const notebookName = newValue + ".ipynb";
this.props.onChangeName(notebookName);
this.setState({ notebookName });
},
};
this.descriptionProps = {
label: "Description",
ariaLabel: "Description",
multiline: true,
rows: 3,
required: true,
onChange: (event, newValue) => {
this.props.onChangeDescription(newValue);
this.setState({ notebookDescription: newValue });
},
};
this.tagsProps = {
label: "Tags",
ariaLabel: "Tags",
placeholder: "Optional tag 1, Optional tag 2",
onChange: (event, newValue) => {
this.props.onChangeTags(newValue);
this.setState({ notebookTags: newValue });
},
};
}
private renderThumbnailSelectors(type: string) {
switch (type) {
case ImageTypes.Url:
return <TextField {...this.thumbnailUrlProps} />;
case ImageTypes.CustomImage:
return (
<input
id="selectImageFile"
type="file"
accept="image/*"
onChange={(event) => {
const file = event.target.files[0];
if (file.size / 1024 ** 2 > PublishNotebookPaneComponent.maxImageSizeInMib) {
event.target.value = "";
const formError = `Failed to upload ${file.name}`;
const formErrorDetail = `Image is larger than ${PublishNotebookPaneComponent.maxImageSizeInMib} MiB. Please Choose a different image.`;
const area = "PublishNotebookPaneComponent/selectImageFile";
this.props.onError(formError, formErrorDetail, area);
this.props.onChangeImageSrc(undefined);
this.setState({ imageSrc: undefined });
return;
} else {
this.props.clearFormError();
}
this.imageToBase64(file, (result: string) => {
this.props.onChangeImageSrc(result);
this.setState({ imageSrc: result });
});
}}
/>
);
default:
return <></>;
}
}
private findFirstOutput(): HTMLElement {
const indexOfFirstCodeCellWithDisplay = NotebookUtil.findFirstCodeCellWithDisplay(this.props.notebookObject);
const cellOutputDomElements = this.props.notebookParentDomElement.querySelectorAll<HTMLElement>(
".nteract-cell-outputs"
);
return cellOutputDomElements[indexOfFirstCodeCellWithDisplay];
}
public render(): JSX.Element {
return (
<div className="publishNotebookPanelContent">
<Stack className="panelMainContent" tokens={{ childrenGap: 20 }}>
<Stack.Item>
<Text>{this.descriptionPara1}</Text>
</Stack.Item>
<Stack.Item>
<Text>{this.descriptionPara2}</Text>
</Stack.Item>
<Stack.Item>
<TextField {...this.nameProps} />
</Stack.Item>
<Stack.Item>
<TextField {...this.descriptionProps} />
</Stack.Item>
<Stack.Item>
<TextField {...this.tagsProps} />
</Stack.Item>
<Stack.Item>
<Dropdown {...this.thumbnailSelectorProps} />
</Stack.Item>
<Stack.Item>{this.renderThumbnailSelectors(this.state.type)}</Stack.Item>
<Stack.Item>
<Text>Preview</Text>
</Stack.Item>
<Stack.Item>
<GalleryCardComponent
data={{
id: undefined,
name: this.state.notebookName,
description: this.state.notebookDescription,
gitSha: undefined,
tags: this.state.notebookTags.split(","),
author: this.props.notebookAuthor,
thumbnailUrl: this.state.imageSrc,
created: this.props.notebookCreatedDate,
isSample: false,
downloads: undefined,
favorites: undefined,
views: undefined,
newCellId: undefined,
policyViolations: undefined,
pendingScanJobIds: undefined,
}}
isFavorite={undefined}
showDownload={false}
showDelete={false}
onClick={undefined}
onTagClick={undefined}
onFavoriteClick={undefined}
onUnfavoriteClick={undefined}
onDownloadClick={undefined}
onDeleteClick={undefined}
/>
</Stack.Item>
</Stack>
</div>
);
}
}

View File

@@ -154,40 +154,6 @@ exports[`Load Query Pane should render Default properly 1`] = `
"title": [Function],
"visible": [Function],
},
EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
@@ -210,9 +176,7 @@ exports[`Load Query Pane should render Default properly 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -235,20 +199,6 @@ exports[`Load Query Pane should render Default properly 1`] = `
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -403,9 +353,7 @@ exports[`Load Query Pane should render Default properly 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -456,40 +404,6 @@ exports[`Load Query Pane should render Default properly 1`] = `
"defaultExperience": [Function],
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -521,7 +435,6 @@ exports[`Load Query Pane should render Default properly 1`] = `
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiMongoDB": [Function],
"isPublishNotebookPaneEnabled": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
@@ -553,21 +466,9 @@ exports[`Load Query Pane should render Default properly 1`] = `
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -593,20 +494,6 @@ exports[`Load Query Pane should render Default properly 1`] = `
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],

View File

@@ -153,40 +153,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"title": [Function],
"visible": [Function],
},
EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
@@ -209,9 +175,7 @@ exports[`Settings Pane should render Default properly 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -234,20 +198,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -402,9 +352,7 @@ exports[`Settings Pane should render Default properly 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -455,40 +403,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"defaultExperience": [Function],
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -520,7 +434,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiMongoDB": [Function],
"isPublishNotebookPaneEnabled": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
@@ -552,21 +465,9 @@ exports[`Settings Pane should render Default properly 1`] = `
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -592,20 +493,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
@@ -904,40 +791,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"title": [Function],
"visible": [Function],
},
EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
@@ -960,9 +813,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -985,20 +836,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -1153,9 +990,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -1206,40 +1041,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"defaultExperience": [Function],
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -1271,7 +1072,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiMongoDB": [Function],
"isPublishNotebookPaneEnabled": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
@@ -1303,21 +1103,9 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -1343,20 +1131,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],

View File

@@ -1,66 +0,0 @@
<div data-bind="visible: visible, event: { keydown: onPaneKeyDown }">
<div class="contextual-pane-out" data-bind="click: cancel, clickBubble: false"></div>
<div class="contextual-pane" id="stringInputPane">
<!-- String Input form -- Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- String Input header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2" data-bind="text: title"></span>
<div class="closeImg" role="button" aria-label="Close pane" tabindex="0" data-bind="click: cancel"></div>
</div>
<!-- String Input header - End -->
<!-- String Input errors - Start -->
<div
class="warningErrorContainer"
aria-live="assertive"
data-bind="visible: formErrors() && formErrors() !== ''"
>
<div class="warningErrorContent">
<span><img class="paneErrorIcon" src="/error_red.svg" alt="Error" /></span>
<span class="warningErrorDetailsLinkContainer">
<span class="formErrors" data-bind="text: formErrors, attr: { title: formErrors }"></span>
<a
class="errorLink"
role="link"
data-bind="visible: formErrorsDetails() && formErrorsDetails() !== '', click: showErrorDetails"
>More details</a
>
</span>
</div>
</div>
<!-- String Input errors - End -->
<!-- String Input inputs - Start -->
<div class="paneMainContent">
<div>
<p data-bind="text: inputLabel"></p>
<p>
<input
type="text"
name="collectionIdConfirmation"
required
class="collid"
data-bind="textInput: stringInput, hasFocus: firstFieldHasFocus"
aria-label="inputLabel"
/>
</p>
</div>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">
<input type="submit" data-bind="attr: { value: submitButtonLabel }" class="btncreatecoll1" />
</div>
</div>
<!-- String Input inputs - End -->
</form>
</div>
<!-- String Input form - Start -->
<!-- Loader - Start -->
<div class="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" data-bind="visible: isExecuting">
<img class="dataExplorerLoader" src="/LoadingIndicator_3Squares.gif" />
</div>
<!-- Loader - End -->
</div>
</div>

View File

@@ -1,99 +0,0 @@
import * as ko from "knockout";
import Q from "q";
import * as ViewModels from "../../Contracts/ViewModels";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { ContextualPaneBase } from "./ContextualPaneBase";
export interface StringInputPaneOpenOptions {
paneTitle: string;
inputLabel: string;
errorMessage: string;
inProgressMessage: string;
successMessage: string;
onSubmit: (input: string) => Promise<any>;
submitButtonLabel: string;
defaultInput?: string;
}
/**
* Generic pane to get a single string input from user
*/
export class StringInputPane extends ContextualPaneBase {
private openOptions: StringInputPaneOpenOptions;
private submitButtonLabel: ko.Observable<string>;
private inputLabel: ko.Observable<string>;
private stringInput: ko.Observable<string>;
private paneDeferred: Q.Deferred<any>;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.resetData();
this.inputLabel = ko.observable("");
this.submitButtonLabel = ko.observable("Load");
this.stringInput = ko.observable();
}
public submit() {
this.formErrors("");
this.formErrorsDetails("");
const clearInProgressMessage = logConsoleProgress(`${this.openOptions.inProgressMessage} ${this.stringInput()}`);
this.isExecuting(true);
this.openOptions
.onSubmit(this.stringInput())
.then(
(value: any) => {
logConsoleInfo(`${this.openOptions.successMessage}: ${this.stringInput()}`);
this.close();
this.paneDeferred.resolve(value);
},
(reason) => {
let error = reason;
if (reason instanceof Error) {
error = reason.message;
} else if (typeof reason === "object") {
error = JSON.stringify(reason);
}
// If it's an AjaxError (AjaxObservable), add more error
if (reason.response && reason.response.message) {
error += `. ${reason.response.message}`;
}
this.formErrors(this.openOptions.errorMessage);
this.formErrorsDetails(`${this.openOptions.errorMessage}: ${error}`);
logConsoleError(`${this.openOptions.errorMessage} ${this.stringInput()}: ${error}`);
this.paneDeferred.reject(error);
}
)
.finally(() => {
this.isExecuting(false);
clearInProgressMessage();
});
}
public close() {
super.close();
this.resetData();
this.resetFileInput();
}
public openWithOptions<T>(options: StringInputPaneOpenOptions): Q.Promise<T> {
this.openOptions = options;
this.title(this.openOptions.paneTitle);
if (this.openOptions.submitButtonLabel) {
this.submitButtonLabel(this.openOptions.submitButtonLabel);
}
this.inputLabel(this.openOptions.inputLabel);
this.stringInput(this.openOptions.defaultInput);
super.open();
this.paneDeferred = Q.defer<T>();
return this.paneDeferred.promise;
}
private resetFileInput(): void {
this.stringInput("");
}
}

View File

@@ -0,0 +1,28 @@
import { mount } from "enzyme";
import React from "react";
import Explorer from "../../Explorer";
import { StringInputPane } from "./StringInputPane";
const props = {
explorer: new Explorer(),
closePanel: (): void => undefined,
errorMessage: "Could not create directory ",
inProgressMessage: "Creating directory ",
successMessage: "Created directory ",
inputLabel: "Enter new directory name",
paneTitle: "Create new directory",
submitButtonLabel: "Create",
defaultInput: "",
onSubmit: jest.fn(),
notebookFile: {
name: "Untitled1123.ipynb",
path: "notebooks/Untitled1123.ipynb",
type: 0,
timestamp: 1618452275805,
},
};
describe("StringInput Pane", () => {
it("should render Create new directory properly", () => {
const wrapper = mount(<StringInputPane {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,122 @@
import { TextField } from "office-ui-fabric-react";
import React, { FormEvent, FunctionComponent, useState } from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../../Utils/NotificationConsoleUtils";
import Explorer from "../../Explorer";
import * as FileSystemUtil from "../../Notebook/FileSystemUtil";
import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
import NotebookV2Tab from "../../Tabs/NotebookV2Tab";
import {
GenericRightPaneComponent,
GenericRightPaneProps,
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
export interface StringInputPanelProps {
explorer: Explorer;
closePanel: () => void;
errorMessage: string;
inProgressMessage: string;
successMessage: string;
inputLabel: string;
paneTitle: string;
submitButtonLabel: string;
defaultInput: string;
onSubmit: (notebookFile: NotebookContentItem, input: string) => Promise<NotebookContentItem>;
notebookFile: NotebookContentItem;
}
export const StringInputPane: FunctionComponent<StringInputPanelProps> = ({
explorer: container,
closePanel,
errorMessage,
inProgressMessage,
successMessage,
inputLabel,
paneTitle,
submitButtonLabel,
defaultInput,
onSubmit,
notebookFile,
}: StringInputPanelProps): JSX.Element => {
const [stringInput, setStringInput] = useState<string>(defaultInput);
const [formErrors, setFormErrors] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false);
const submit = async (): Promise<void> => {
if (stringInput === "") {
const errorMessage = "Please " + inputLabel;
setFormErrors(errorMessage);
logConsoleError("Error while " + paneTitle + " : " + errorMessage);
return;
} else {
setFormErrors("");
setFormErrorsDetails("");
}
const clearMessage = logConsoleProgress(`${inProgressMessage} ${stringInput}`);
try {
const newNotebookFile: NotebookContentItem = await onSubmit(notebookFile, stringInput);
logConsoleInfo(`${successMessage}: ${stringInput}`);
const originalPath = notebookFile.path;
const notebookTabs = container.tabsManager.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
);
notebookTabs.forEach((tab) => {
tab.tabTitle(newNotebookFile.name);
tab.tabPath(newNotebookFile.path);
(tab as NotebookV2Tab).notebookPath(newNotebookFile.path);
});
closePanel();
} catch (reason) {
let error = reason;
if (reason instanceof Error) {
error = reason.message;
} else if (typeof reason === "object") {
error = JSON.stringify(reason);
}
// If it's an AjaxError (AjaxObservable), add more error
if (reason?.response?.message) {
error += `. ${reason.response.message}`;
}
setFormErrors(errorMessage);
setFormErrorsDetails(`${errorMessage}: ${error}`);
logConsoleError(`${errorMessage} ${stringInput}: ${error}`);
} finally {
setIsExecuting(false);
clearMessage();
}
};
const genericPaneProps: GenericRightPaneProps = {
container: container,
formError: formErrors,
formErrorDetail: formErrorsDetails,
id: "stringInputPane",
isExecuting: isExecuting,
title: paneTitle,
submitButtonText: submitButtonLabel,
onClose: closePanel,
onSubmit: submit,
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<div className="paneMainContent">
<TextField
label={inputLabel}
name="collectionIdConfirmation"
value={stringInput}
autoFocus
required
onChange={(event: FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) =>
setStringInput(newValue)
}
aria-label={inputLabel}
/>
</div>
</GenericRightPaneComponent>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
import { mount } from "enzyme";
import * as ko from "knockout";
import React from "react";
import Explorer from "../../Explorer";
import TableListViewModal from "../../Tables/DataTable/TableEntityListViewModel";
import * as Entities from "../../Tables/Entities";
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { EditTableEntityPanel } from "./EditTableEntityPanel";
describe("Excute Edit Table Entity Pane", () => {
const fakeExplorer = {} as Explorer;
const fakeQueryTablesTab = {} as QueryTablesTab;
const fakeTableEntityListViewModel = {} as TableListViewModal;
fakeTableEntityListViewModel.items = ko.observableArray<Entities.ITableEntity>();
const fakeCassandraApiClient = {} as CassandraAPIDataClient;
fakeTableEntityListViewModel.headers = [];
fakeTableEntityListViewModel.selected = ko.observableArray<Entities.ITableEntity>([{}]);
const props = {
explorer: fakeExplorer,
closePanel: (): void => undefined,
queryTablesTab: fakeQueryTablesTab,
tableEntityListViewModel: fakeTableEntityListViewModel,
cassandraApiClient: fakeCassandraApiClient,
};
it("should render Default properly", () => {
const wrapper = mount(<EditTableEntityPanel {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("initially display 4 input field, 2 properties and 1 entity values", () => {
const wrapper = mount(<EditTableEntityPanel {...props} />);
expect(wrapper.find("input[type='text']")).toHaveLength(0);
});
it("add a new entity row", () => {
const wrapper = mount(<EditTableEntityPanel {...props} />);
wrapper.find(".addButtonEntiy").last().simulate("click");
expect(wrapper.find("input[type='text']")).toHaveLength(1);
});
it("remove a entity field", () => {
const wrapper = mount(<EditTableEntityPanel {...props} />);
// Since default entity row doesn't have delete option, so added row then delete for test cases.
wrapper.find(".addButtonEntiy").last().simulate("click");
wrapper.find("#deleteEntity").last().simulate("click");
expect(wrapper.find("input[type='text']")).toHaveLength(0);
});
});

View File

@@ -0,0 +1,419 @@
import { useBoolean } from "@uifabric/react-hooks";
import {
IDropdownOption,
Image,
IPanelProps,
IRenderFunction,
Label,
Stack,
Text,
TextField,
} from "office-ui-fabric-react";
import React, { FunctionComponent, useEffect, useState } from "react";
import * as _ from "underscore";
import AddPropertyIcon from "../../../../images/Add-property.svg";
import RevertBackIcon from "../../../../images/RevertBack.svg";
import { TableEntity } from "../../../Common/TableEntity";
import { userContext } from "../../../UserContext";
import Explorer from "../../Explorer";
import * as TableConstants from "../../Tables/Constants";
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
import * as Entities from "../../Tables/Entities";
import { CassandraAPIDataClient } from "../../Tables/TableDataClient";
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
import QueryTablesTab from "../../Tabs/QueryTablesTab";
import { PanelContainerComponent } from "../PanelContainerComponent";
import {
attributeNameLabel,
attributeValueLabel,
backImageProps,
cassandraOptions,
columnProps,
dataTypeLabel,
defaultStringPlaceHolder,
detailedHelp,
entityFromAttributes,
getAddButtonLabel,
getEntityValuePlaceholder,
getFormattedTime,
imageProps,
isValidEntities,
options,
} from "./Validators/EntityTableHelper";
interface EditTableEntityPanelProps {
explorer: Explorer;
closePanel: () => void;
queryTablesTab: QueryTablesTab;
tableEntityListViewModel: TableEntityListViewModel;
cassandraApiClient: CassandraAPIDataClient;
}
interface EntityRowType {
property: string;
type: string;
value: string;
isPropertyTypeDisable: boolean;
isDeleteOptionVisible: boolean;
id: number;
entityValuePlaceholder: string;
isEntityTypeDate: boolean;
entityTimeValue?: string;
isEntityValueDisable?: boolean;
}
export const EditTableEntityPanel: FunctionComponent<EditTableEntityPanelProps> = ({
explorer,
closePanel,
queryTablesTab,
tableEntityListViewModel,
cassandraApiClient,
}: EditTableEntityPanelProps): JSX.Element => {
const [entities, setEntities] = useState<EntityRowType[]>([]);
const [selectedRow, setSelectedRow] = useState<number>(0);
const [entityAttributeValue, setEntityAttributeValue] = useState<string>("");
const [originalDocument, setOriginalDocument] = useState<Entities.ITableEntity>({});
const [entityAttributeProperty, setEntityAttributeProperty] = useState<string>("");
const [
isEntityValuePanelOpen,
{ setTrue: setIsEntityValuePanelTrue, setFalse: setIsEntityValuePanelFalse },
] = useBoolean(false);
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let originalDocument: { [key: string]: any } = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const entityAttribute: any = tableEntityListViewModel.selected();
const entityFormattedAttribute = constructDisplayedAttributes(entityAttribute[0]);
setEntities(entityFormattedAttribute);
if (userContext.apiType === "Tables") {
originalDocument = TableEntityProcessor.convertEntitiesToDocuments(entityAttribute, queryTablesTab.collection)[0];
originalDocument.id = (): string => originalDocument.$id;
} else {
originalDocument = entityAttribute;
}
setOriginalDocument(originalDocument);
}, []);
const constructDisplayedAttributes = (entity: Entities.ITableEntity): EntityRowType[] => {
const displayedAttributes: EntityRowType[] = [];
const keys = Object.keys(entity);
keys.length &&
keys.forEach((key: string) => {
if (
key !== TableEntityProcessor.keyProperties.attachments &&
key !== TableEntityProcessor.keyProperties.etag &&
key !== TableEntityProcessor.keyProperties.resourceId &&
key !== TableEntityProcessor.keyProperties.self &&
(userContext.apiType !== "Cassandra" || key !== TableConstants.EntityKeyNames.RowKey)
) {
if (userContext.apiType === "Cassandra") {
const cassandraKeys = queryTablesTab.collection.cassandraKeys.partitionKeys
.concat(queryTablesTab.collection.cassandraKeys.clusteringKeys)
.map((key) => key.property);
const entityAttribute: Entities.ITableEntityAttribute = entity[key];
const entityAttributeType: string = entityAttribute.$;
const displayValue: string = getPropertyDisplayValue(entity, key, entityAttributeType);
const nonEditableType: boolean =
entityAttributeType === TableConstants.CassandraType.Blob ||
entityAttributeType === TableConstants.CassandraType.Inet;
const isDisable = !_.contains<string>(cassandraKeys, key) && !nonEditableType;
const time =
entityAttributeType === TableConstants.TableType.DateTime ? getFormattedTime(displayValue) : "";
displayedAttributes.push({
property: key,
type: entityAttributeType,
value: displayValue,
isPropertyTypeDisable: !nonEditableType,
isDeleteOptionVisible: isDisable,
id: displayedAttributes.length,
entityValuePlaceholder: defaultStringPlaceHolder,
isEntityTypeDate: entityAttributeType === "DateTime",
isEntityValueDisable: !isDisable,
entityTimeValue: time,
});
} else {
const entityAttribute: Entities.ITableEntityAttribute = entity[key];
const entityAttributeType: string = entityAttribute.$;
const displayValue: string = getPropertyDisplayValue(entity, key, entityAttributeType);
const editable: boolean = isAttributeEditable(key, entityAttributeType);
// As per VSO:189935, Binary properties are read-only, we still want to be able to remove them.
const removable: boolean = editable || entityAttributeType === TableConstants.TableType.Binary;
const time =
entityAttributeType === TableConstants.TableType.DateTime ? getFormattedTime(displayValue) : "";
displayedAttributes.push({
property: key,
type: entityAttributeType,
value: displayValue,
isPropertyTypeDisable: !editable,
isDeleteOptionVisible: removable,
id: displayedAttributes.length,
entityValuePlaceholder: defaultStringPlaceHolder,
isEntityTypeDate: entityAttributeType === "DateTime",
isEntityValueDisable: !editable,
entityTimeValue: time,
});
}
}
});
return displayedAttributes;
};
const isAttributeEditable = (attributeName: string, entityAttributeType: string) => {
return !(
attributeName === TableConstants.EntityKeyNames.PartitionKey ||
attributeName === TableConstants.EntityKeyNames.RowKey ||
attributeName === TableConstants.EntityKeyNames.Timestamp ||
// As per VSO:189935, Making Binary properties read-only in Edit Entity dialog until we have a full story for it.
entityAttributeType === TableConstants.TableType.Binary
);
};
const getPropertyDisplayValue = (entity: Entities.ITableEntity, name: string, type: string): string => {
const attribute: Entities.ITableEntityAttribute = entity[name];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let displayValue: any = attribute._;
const isBinary: boolean = type === TableConstants.TableType.Binary;
// Showing the value in base64 for binary properties since that is what the Azure Storage Client Library expects.
// This means that, even if the Azure Storage API returns a byte[] of binary content, it needs that same array
// *base64 - encoded * as the value for the updated property or the whole update operation will fail.
if (isBinary && displayValue && $.isArray(displayValue.data)) {
const bytes: number[] = displayValue.data;
displayValue = getBase64DisplayValue(bytes);
}
return displayValue;
};
const getBase64DisplayValue = (bytes: number[]): string => {
let displayValue = "";
try {
const chars: string[] = bytes.map((byte: number) => String.fromCharCode(byte));
const toEncode: string = chars.join("");
displayValue = window.btoa(toEncode);
} catch (error) {
console.error(error);
}
return displayValue;
};
const submit = async (event: React.FormEvent<HTMLInputElement>): Promise<void> => {
if (!isValidEntities(entities)) {
return undefined;
}
event.preventDefault();
const entity: Entities.ITableEntity = entityFromAttributes(entities);
const tableDataClient = userContext.apiType === "Cassandra" ? cassandraApiClient : explorer.tableDataClient;
const originalDocumentData = userContext.apiType === "Cassandra" ? originalDocument[0] : originalDocument;
const newEntity: Entities.ITableEntity = await tableDataClient.updateDocument(
queryTablesTab.collection,
originalDocumentData,
entity
);
await tableEntityListViewModel.updateCachedEntity(newEntity);
if (!tryInsertNewHeaders(tableEntityListViewModel, newEntity)) {
tableEntityListViewModel.redrawTableThrottled();
}
tableEntityListViewModel.selected.removeAll();
tableEntityListViewModel.selected.push(newEntity);
closePanel();
};
const tryInsertNewHeaders = (viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean => {
let newHeaders: string[] = [];
const keys = Object.keys(newEntity);
keys &&
keys.forEach((key: string) => {
if (
!_.contains(viewModel.headers, key) &&
key !== TableEntityProcessor.keyProperties.attachments &&
key !== TableEntityProcessor.keyProperties.etag &&
key !== TableEntityProcessor.keyProperties.resourceId &&
key !== TableEntityProcessor.keyProperties.self &&
(!(userContext.apiType === "Cassandra") || key !== TableConstants.EntityKeyNames.RowKey)
) {
newHeaders.push(key);
}
});
let newHeadersInserted = false;
if (newHeaders.length) {
if (!DataTableUtilities.checkForDefaultHeader(viewModel.headers)) {
newHeaders = viewModel.headers.concat(newHeaders);
}
viewModel.updateHeaders(newHeaders, /* notifyColumnChanges */ true, /* enablePrompt */ false);
newHeadersInserted = true;
}
return newHeadersInserted;
};
// Add new entity row
const addNewEntity = (): void => {
const cloneEntities = [...entities];
cloneEntities.splice(cloneEntities.length, 0, {
property: "",
type: TableConstants.TableType.String,
value: "",
isPropertyTypeDisable: false,
isDeleteOptionVisible: true,
id: cloneEntities.length + 1,
entityValuePlaceholder: "",
isEntityTypeDate: false,
});
setEntities(cloneEntities);
};
// Delete entity row
const deleteEntityAtIndex = (indexToRemove: number): void => {
const cloneEntities = [...entities];
cloneEntities.splice(indexToRemove, 1);
setEntities(cloneEntities);
};
// handle Entity change
const entityChange = (value: string | Date, indexOfInput: number, key: string): void => {
const cloneEntities = [...entities];
if (key === "property") {
cloneEntities[indexOfInput].property = value.toString();
} else if (key === "time") {
cloneEntities[indexOfInput].entityTimeValue = value.toString();
} else {
cloneEntities[indexOfInput].value = value.toString();
}
setEntities(cloneEntities);
};
// handle Entity type
const entityTypeChange = (
_event: React.FormEvent<HTMLDivElement>,
selectedType: IDropdownOption,
indexOfEntity: number
): void => {
const entityValuePlaceholder = getEntityValuePlaceholder(selectedType.key);
const cloneEntities = [...entities];
cloneEntities[indexOfEntity].type = selectedType.key.toString();
cloneEntities[indexOfEntity].entityValuePlaceholder = entityValuePlaceholder;
cloneEntities[indexOfEntity].isEntityTypeDate = selectedType.key === TableConstants.TableType.DateTime;
setEntities(cloneEntities);
};
// Open edit entity value modal
const editEntity = (rowEndex: number): void => {
const entityAttribute: EntityRowType = entities[rowEndex] && entities[rowEndex];
setEntityAttributeValue(entityAttribute.value);
setEntityAttributeProperty(entityAttribute.property);
setSelectedRow(rowEndex);
setIsEntityValuePanelTrue();
};
const renderPanelContent = (): JSX.Element => {
return (
<form className="panelFormWrapper">
<div className="panelFormWrapper">
<div className="panelMainContent">
{entities.map((entity, index) => {
return (
<TableEntity
key={"" + entity.id + index}
isDeleteOptionVisible={entity.isDeleteOptionVisible}
entityTypeLabel={index === 0 && dataTypeLabel}
entityPropertyLabel={index === 0 && attributeNameLabel}
entityValueLabel={index === 0 && attributeValueLabel}
options={userContext.apiType === "Cassandra" ? cassandraOptions : options}
isPropertyTypeDisable={entity.isPropertyTypeDisable}
entityProperty={entity.property}
selectedKey={entity.type}
entityPropertyPlaceHolder={detailedHelp}
entityValuePlaceholder={entity.entityValuePlaceholder}
entityValue={entity.value}
isEntityTypeDate={entity.isEntityTypeDate}
entityTimeValue={entity.entityTimeValue}
isEntityValueDisable={entity.isEntityValueDisable}
onEditEntity={() => editEntity(index)}
onSelectDate={(date: Date) => {
entityChange(date, index, "value");
}}
onDeleteEntity={() => deleteEntityAtIndex(index)}
onEntityPropertyChange={(event, newInput?: string) => {
entityChange(newInput, index, "property");
}}
onEntityTypeChange={(event: React.FormEvent<HTMLDivElement>, selectedParam: IDropdownOption) => {
entityTypeChange(event, selectedParam, index);
}}
onEntityValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "value");
}}
onEntityTimeValueChange={(event, newInput?: string) => {
entityChange(newInput, index, "time");
}}
/>
);
})}
{userContext.apiType !== "Cassandra" && (
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
</Stack>
)}
</div>
{renderPanelFooter()}
</div>
</form>
);
};
const renderPanelFooter = (): JSX.Element => {
return (
<div className="paneFooter">
<div className="leftpanel-okbut">
<input type="submit" onClick={submit} className="genericPaneSubmitBtn" value="Update Entity" />
</div>
</div>
);
};
const onRenderNavigationContent: IRenderFunction<IPanelProps> = () => (
<Stack horizontal {...columnProps}>
<Image {...backImageProps} src={RevertBackIcon} alt="back" onClick={() => setIsEntityValuePanelFalse()} />
<Label>{entityAttributeProperty}</Label>
</Stack>
);
if (isEntityValuePanelOpen) {
return (
<PanelContainerComponent
headerText=""
onRenderNavigationContent={onRenderNavigationContent}
panelWidth="700px"
isOpen={true}
panelContent={
<TextField
multiline
rows={5}
className="entityValueTextField"
value={entityAttributeValue}
onChange={(event, newInput?: string) => {
setEntityAttributeValue(newInput);
entityChange(newInput, selectedRow, "value");
}}
/>
}
closePanel={() => closePanel()}
isConsoleExpanded={false}
/>
);
}
return (
<PanelContainerComponent
headerText="Edit Table Entity"
panelWidth="700px"
isOpen={true}
panelContent={renderPanelContent()}
closePanel={() => closePanel()}
isConsoleExpanded={false}
/>
);
};

View File

@@ -5,28 +5,6 @@ import * as Entities from "../../../Tables/Entities";
import * as Utilities from "../../../Tables/Utilities";
export const defaultStringPlaceHolder = "Enter identifier value.";
export const defaultEntities = [
{
property: "PartitionKey",
type: "String",
value: "",
isPropertyTypeDisable: true,
isDeleteOptionVisible: false,
id: 1,
entityValuePlaceholder: defaultStringPlaceHolder,
isEntityTypeDate: false,
},
{
property: "RowKey",
type: "String",
value: "",
isPropertyTypeDisable: true,
isDeleteOptionVisible: false,
id: 2,
entityValuePlaceholder: defaultStringPlaceHolder,
isEntityTypeDate: false,
},
];
// Dropdown options
const { String, Boolean, Binary, DateTime, Double, Guid, Int32, Int64 } = TableConstants.TableType;
@@ -133,20 +111,21 @@ export const entityFromAttributes = (entities: EntityRowType[]): Entities.ITable
// GetPlaceholder according to entity type
export const getEntityValuePlaceholder = (entityType: string | number): string => {
const { String, Boolean, DateTime, Double, Guid, Int32, Int64 } = TableConstants.TableType;
switch (entityType) {
case "String":
case String:
return stringPlaceholder;
case "Boolean":
case Boolean:
return booleanPlaceHolder;
case "DateTime":
case DateTime:
return datePlaceholder;
case "Double":
case Double:
return doublePlaceholder;
case "Guid":
case Guid:
return guidPlaceholder;
case "Int32":
case Int32:
return intPlaceholder;
case "Int64":
case Int64:
return int64Placeholder;
default:
return "";
@@ -164,13 +143,13 @@ export const isValidEntities = (entities: EntityRowType[]): boolean => {
};
const isEntityPropertyTypeDisable = (header: string): boolean => {
if (header === "PartitionKey" || header === "RowKey") {
if (header === TableConstants.EntityKeyNames.PartitionKey || header === TableConstants.EntityKeyNames.RowKey) {
return true;
}
return false;
};
export const getDefaultEntities = (headers: string[], entityTypes: { [key: string]: string }): EntityRowType[] => {
export const getDefaultEntities = (headers: string[], entityTypes: EntityType): EntityRowType[] => {
const defaultEntities: EntityRowType[] = [];
headers.forEach((header: string) => {
if (header !== "Timestamp") {
@@ -183,7 +162,7 @@ export const getDefaultEntities = (headers: string[], entityTypes: { [key: strin
isDeleteOptionVisible: !isEntityPropertyTypeDisable(header),
id: 1,
entityValuePlaceholder: defaultStringPlaceHolder,
isEntityTypeDate: entityType === "DateTime",
isEntityTypeDate: entityType === TableConstants.TableType.DateTime,
};
defaultEntities.push(entityRow);
}
@@ -212,6 +191,13 @@ export const getButtonLabel = (apiType: string): string => {
return "Add Entity";
};
export const getFormattedTime = (displayValue: string): string => {
const date = new Date(displayValue);
const minutes = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes();
const hours = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours();
return `${hours}:${minutes}`;
};
export const getCassandraDefaultEntities = (
headers: string[],
entityTypes: { [key: string]: string }
@@ -244,4 +230,9 @@ export interface EntityRowType {
entityValuePlaceholder: string;
isEntityTypeDate: boolean;
entityTimeValue?: string;
isEntityValueDisable?: boolean;
}
export interface EntityType {
[key: string]: string;
}

File diff suppressed because it is too large Load Diff

View File

@@ -153,40 +153,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"title": [Function],
"visible": [Function],
},
EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
@@ -209,9 +175,7 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -234,20 +198,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -402,9 +352,7 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -455,40 +403,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"defaultExperience": [Function],
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -520,7 +434,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiMongoDB": [Function],
"isPublishNotebookPaneEnabled": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
@@ -552,21 +465,9 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -592,20 +493,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],

View File

@@ -154,40 +154,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"title": [Function],
"visible": [Function],
},
EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
@@ -210,9 +176,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -235,20 +199,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -403,9 +353,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -456,40 +404,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"defaultExperience": [Function],
"deleteCollectionText": [Function],
"deleteDatabaseText": [Function],
"editTableEntityPane": EditTableEntityPane {
"addButtonLabel": "Add Property",
"attributeNameLabel": "Property Name",
"attributeValueLabel": "Value",
"canAdd": [Function],
"canApply": [Function],
"container": [Circular],
"dataTypeLabel": "Type",
"displayedAttributes": [Function],
"editAttribute": [Function],
"editButtonLabel": "Edit",
"editingProperty": [Function],
"edmTypes": [Function],
"finishEditingAttribute": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "edittableentitypane",
"insertAttribute": [Function],
"isEditing": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"onAddPropertyKeyDown": [Function],
"onBackButtonKeyDown": [Function],
"onDeletePropertyKeyDown": [Function],
"onEditPropertyKeyDown": [Function],
"onKeyUp": [Function],
"removeAttribute": [Function],
"removeButtonLabel": "Remove",
"scrollId": [Function],
"submitButtonText": [Function],
"title": [Function],
"visible": [Function],
},
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
@@ -523,7 +437,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function],
"isPreferredApiMongoDB": [Function],
"isPublishNotebookPaneEnabled": [Function],
"isResourceTokenCollectionNodeSelected": [Function],
"isRightPanelV2Enabled": [Function],
@@ -557,21 +470,9 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -597,20 +498,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],

View File

@@ -237,7 +237,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: "New SQL Query",
description: null,
});
} else if (this.container.isPreferredApiMongoDB()) {
} else if (userContext.apiType === "Mongo") {
items.push({
iconSrc: NewQueryIcon,
onClick: () => {

View File

@@ -58,10 +58,6 @@ export default class TableCommands {
var entityToUpdate: Entities.ITableEntity = viewModel.selected()[0];
var originalNumberOfProperties = entityToUpdate ? 0 : Object.keys(entityToUpdate).length - 1; // .metadata is always a property for etag
this._container.editTableEntityPane.originEntity = entityToUpdate;
this._container.editTableEntityPane.tableViewModel = viewModel;
this._container.editTableEntityPane.originalNumberOfProperties = originalNumberOfProperties;
this._container.editTableEntityPane.open();
return null;
}

View File

@@ -1,35 +1,34 @@
import { ConflictDefinition, FeedOptions, QueryIterator, Resource } from "@azure/cosmos";
import * as ko from "knockout";
import Q from "q";
import DeleteIcon from "../../../images/delete.svg";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
import * as Constants from "../../Common/Constants";
import { DocumentsGridMetrics, KeyCodes } from "../../Common/Constants";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteConflict } from "../../Common/dataAccess/deleteConflict";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList";
import { KeyCodes } from "../../Common/Constants";
import ConflictId from "../Tree/ConflictId";
import editable from "../../Common/EditableUtility";
import * as HeadersUtility from "../../Common/HeadersUtility";
import TabsBase from "./TabsBase";
import { DocumentsGridMetrics } from "../../Common/Constants";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import SaveIcon from "../../../images/save-cosmos.svg";
import DiscardIcon from "../../../images/discard.svg";
import DeleteIcon from "../../../images/delete.svg";
import { QueryIterator, Resource, ConflictDefinition, FeedOptions } from "@azure/cosmos";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import Explorer from "../Explorer";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { createDocument } from "../../Common/dataAccess/createDocument";
import { deleteDocument } from "../../Common/dataAccess/deleteDocument";
import { updateDocument } from "../../Common/dataAccess/updateDocument";
import { deleteConflict } from "../../Common/dataAccess/deleteConflict";
import { queryConflicts } from "../../Common/dataAccess/queryConflicts";
import Explorer from "../Explorer";
import { AccessibleVerticalList } from "../Tree/AccessibleVerticalList";
import ConflictId from "../Tree/ConflictId";
import template from "./ConflictsTab.html";
import TabsBase from "./TabsBase";
export default class ConflictsTab extends TabsBase {
public static readonly component = { name: "conflicts-tab", template };
public readonly html = template;
public selectedConflictId: ko.Observable<ConflictId>;
public selectedConflictContent: ViewModels.Editable<string>;
public selectedConflictCurrent: ViewModels.Editable<string>;

View File

@@ -1,10 +1,11 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "../../Common/Constants";
import DocumentsTab from "./DocumentsTab";
import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import DocumentId from "../Tree/DocumentId";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import DocumentsTab from "./DocumentsTab";
describe("Documents tab", () => {
describe("buildQuery", () => {
@@ -25,7 +26,6 @@ describe("Documents tab", () => {
describe("showPartitionKey", () => {
const explorer = new Explorer();
const mongoExplorer = new Explorer();
mongoExplorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB);
@@ -124,6 +124,9 @@ describe("Documents tab", () => {
});
it("should be false for Mongo accounts with system partitionKey", () => {
updateUserContext({
apiType: "Mongo",
});
const documentsTab = new DocumentsTab({
collection: mongoCollectionWithSystemPartitionKey,
partitionKey: null,

View File

@@ -21,6 +21,7 @@ import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import * as QueryUtils from "../../Utils/QueryUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
@@ -31,7 +32,7 @@ import template from "./DocumentsTab.html";
import TabsBase from "./TabsBase";
export default class DocumentsTab extends TabsBase {
public static readonly component = { name: "documents-tab", template };
public readonly html = template;
public selectedDocumentId: ko.Observable<DocumentId>;
public selectedDocumentContent: ViewModels.Editable<string>;
public initialDocumentContent: ko.Observable<string>;
@@ -71,9 +72,7 @@ export default class DocumentsTab extends TabsBase {
constructor(options: ViewModels.DocumentsTabOptions) {
super(options);
this.isPreferredApiMongoDB = !!this.collection
? this.collection.container.isPreferredApiMongoDB()
: options.isPreferredApiMongoDB;
this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB;
this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id";

View File

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

View File

@@ -1,14 +1,14 @@
import * as ViewModels from "../../Contracts/ViewModels";
import { GalleryAndNotebookViewerComponentProps } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponent";
import { GalleryAndNotebookViewerComponentAdapter } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponentAdapter";
import { GalleryTab as GalleryViewerTab, SortBy } from "../Controls/NotebookGallery/GalleryViewerComponent";
import React from "react";
import type { DatabaseAccount } from "../../Contracts/DataModels";
import type { TabOptions } from "../../Contracts/ViewModels";
import type { IGalleryItem, JunoClient } from "../../Juno/JunoClient";
import { GalleryAndNotebookViewerComponent as GalleryViewer } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponent";
import type { GalleryTab as GalleryViewerTab } from "../Controls/NotebookGallery/GalleryViewerComponent";
import { SortBy } from "../Controls/NotebookGallery/GalleryViewerComponent";
import type Explorer from "../Explorer";
import TabsBase from "./TabsBase";
import Explorer from "../Explorer";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { JunoClient, IGalleryItem } from "../../Juno/JunoClient";
import template from "./GalleryTab.html";
interface GalleryTabOptions extends ViewModels.TabOptions {
interface Props {
account: DatabaseAccount;
container: Explorer;
junoClient: JunoClient;
@@ -18,51 +18,16 @@ interface GalleryTabOptions extends ViewModels.TabOptions {
isFavorite?: boolean;
}
/**
* Notebook gallery tab
*/
export default class GalleryTab extends TabsBase {
public static readonly component = { name: "gallery-tab", template };
private container: Explorer;
private galleryAndNotebookViewerComponentProps: GalleryAndNotebookViewerComponentProps;
public galleryAndNotebookViewerComponentAdapter: GalleryAndNotebookViewerComponentAdapter;
constructor(options: GalleryTabOptions) {
constructor(options: TabOptions, private props: Props) {
super(options);
this.container = options.container;
this.galleryAndNotebookViewerComponentProps = {
container: options.container,
junoClient: options.junoClient,
notebookUrl: options.notebookUrl,
galleryItem: options.galleryItem,
isFavorite: options.isFavorite,
selectedTab: options.selectedTab,
sortBy: SortBy.MostRecent,
searchText: undefined,
};
this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter(
this.galleryAndNotebookViewerComponentProps
);
}
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 render() {
return <GalleryViewer {...this.props} sortBy={SortBy.MostRecent} searchText={undefined} />;
}
public getContainer(): Explorer {
return this.container;
return this.props.container;
}
}

View File

@@ -1 +0,0 @@
<div class="graphExplorerContainer" role="tabpanel" data-bind="react:graphExplorerAdapter, attr:{ id: tabId }"></div>

View File

@@ -10,7 +10,6 @@ import { GraphExplorerAdapter } from "../Graph/GraphExplorerComponent/GraphExplo
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
import GraphStylingPane from "../Panes/GraphStylingPane";
import { NewVertexPanel } from "../Panes/NewVertexPanel/NewVertexPanel";
import template from "./GraphTab.html";
import TabsBase from "./TabsBase";
export interface GraphIconMap {
[key: string]: { data: string; format: string };
@@ -37,7 +36,8 @@ interface GraphTabOptions extends ViewModels.TabOptions {
}
export default class GraphTab extends TabsBase {
public static readonly component = { name: "graph-tab", template };
public readonly html =
'<div class="graphExplorerContainer" role="tabpanel" data-bind="react:graphExplorerAdapter, attr: {id: tabId}"></div>';
// Graph default configuration
public static readonly DEFAULT_NODE_CAPTION = "id";
private static readonly LINK_COLOR = "#aaa";

View File

@@ -1,6 +1,5 @@
import * as ko from "knockout";
import * as Constants from "../../Common/Constants";
import { HashMap } from "../../Common/HashMap";
import { configContext, Platform } from "../../ConfigContext";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
@@ -13,15 +12,15 @@ import template from "./MongoShellTab.html";
import TabsBase from "./TabsBase";
export default class MongoShellTab extends TabsBase {
public static readonly component = { name: "mongo-shell-tab", template };
public readonly html = template;
public url: ko.Computed<string>;
private _container: Explorer;
private _runtimeEndpoint: string;
private _logTraces: HashMap<number>;
private _logTraces: Map<string, number>;
constructor(options: ViewModels.TabOptions) {
super(options);
this._logTraces = new HashMap<number>();
this._logTraces = new Map();
this._container = options.collection.container;
this.url = ko.computed<string>(() => {
const account = userContext.databaseAccount;

View File

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

View File

@@ -27,14 +27,13 @@ import { KernelSpecsDisplay } from "../Notebook/NotebookClientV2";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
import template from "./NotebookV2Tab.html";
export interface NotebookTabOptions extends NotebookTabBaseOptions {
notebookContentItem: NotebookContentItem;
}
export default class NotebookTabV2 extends NotebookTabBase {
public static readonly component = { name: "notebookv2-tab", template };
public readonly html = '<div data-bind="react:notebookComponentAdapter" style="height: 100%"></div>';
public notebookPath: ko.Observable<string>;
private selectedSparkPool: ko.Observable<string>;
private notebookComponentAdapter: NotebookComponentAdapter;

View File

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

View File

@@ -1,16 +1,15 @@
import * as ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import {
NotebookViewerComponent,
NotebookViewerComponentProps,
} from "../Controls/NotebookViewer/NotebookViewerComponent";
import TabsBase from "./TabsBase";
import Explorer from "../Explorer";
import { DatabaseAccount } from "../../Contracts/DataModels";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import template from "./NotebookViewerTab.html";
import TabsBase from "./TabsBase";
interface NotebookViewerTabOptions extends ViewModels.TabOptions {
account: DatabaseAccount;
@@ -39,7 +38,7 @@ class NotebookViewerComponentAdapter implements ReactAdapter {
}
export default class NotebookViewerTab extends TabsBase {
public static readonly component = { name: "notebook-viewer-tab", template };
public readonly html = '<div style="height: 100%" data-bind="react:notebookViewerComponentAdapter"></div>';
private container: Explorer;
public notebookUrl: string;

View File

@@ -1,11 +1,4 @@
<div
class="tab-pane"
data-bind="setTemplateReady: true,
attr:{
id: tabId
}"
role="tabpanel"
>
<div class="tab-pane" data-bind="attr:{id: tabId}" role="tabpanel">
<div class="tabPaneContentContainer">
<div class="mongoQueryHelper" data-bind="visible: isPreferredApiMongoDB && sqlQueryEditorContent().length === 0">
Start by writing a Mongo query, for example: <strong>{'id':'foo'}</strong> or <strong>{ }</strong> to get all the

View File

@@ -5,7 +5,6 @@ import * as Constants from "../../Common/Constants";
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { HashMap } from "../../Common/HashMap";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
@@ -25,7 +24,7 @@ enum ToggleState {
}
export default class QueryTab extends TabsBase implements ViewModels.WaitsForTemplate {
public static readonly component = { name: "query-tab", template };
public readonly html = template;
public queryEditorId: string;
public executeQueryButton: ViewModels.Button;
public fetchNextPageButton: ViewModels.Button;
@@ -47,7 +46,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
public splitter: Splitter;
public isPreferredApiMongoDB: boolean;
public queryMetrics: ko.Observable<HashMap<DataModels.QueryMetrics>>;
public queryMetrics: ko.Observable<Map<string, DataModels.QueryMetrics>>;
public aggregatedQueryMetrics: ko.Observable<DataModels.QueryMetrics>;
public activityId: ko.Observable<string>;
public roundTrips: ko.Observable<number>;
@@ -91,10 +90,8 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
this.isPreferredApiMongoDB = false;
this.aggregatedQueryMetrics = ko.observable<DataModels.QueryMetrics>();
this._resetAggregateQueryMetrics();
this.queryMetrics = ko.observable<HashMap<DataModels.QueryMetrics>>(new HashMap<DataModels.QueryMetrics>());
this.queryMetrics.subscribe((metrics: HashMap<DataModels.QueryMetrics>) =>
this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics))
);
this.queryMetrics = ko.observable<Map<string, DataModels.QueryMetrics>>(new Map());
this.queryMetrics.subscribe((metrics) => this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics)));
this.isQueryMetricsEnabled = ko.computed<boolean>(() => {
return userContext.apiType === "SQL" || false;
});
@@ -364,13 +361,13 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
this.queryMetrics.valueHasMutated();
}
private _aggregateQueryMetrics(metricsMap: HashMap<DataModels.QueryMetrics>): DataModels.QueryMetrics {
private _aggregateQueryMetrics(metricsMap: Map<string, DataModels.QueryMetrics>): DataModels.QueryMetrics {
if (!metricsMap) {
return null;
}
const aggregatedMetrics: DataModels.QueryMetrics = this.aggregatedQueryMetrics();
metricsMap.forEach((partitionKeyRangeId: string, queryMetrics: DataModels.QueryMetrics) => {
metricsMap.forEach((queryMetrics) => {
if (queryMetrics) {
aggregatedMetrics.documentLoadTime =
queryMetrics.documentLoadTime &&
@@ -510,7 +507,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
return null;
}
const queryMetrics: HashMap<DataModels.QueryMetrics> = this.queryMetrics();
const queryMetrics = this.queryMetrics();
let csvData: string = "";
const columnHeaders: string =
[
@@ -528,7 +525,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
"Document write time (ms)",
].join(",") + "\n";
csvData = csvData + columnHeaders;
queryMetrics.forEach((partitionKeyRangeId: string, queryMetric: DataModels.QueryMetrics) => {
queryMetrics.forEach((queryMetric, partitionKeyRangeId) => {
const partitionKeyRangeData: string =
[
partitionKeyRangeId,

View File

@@ -1,11 +1,4 @@
<div
class="tab-pane tableContainer"
data-bind="
attr:{
id: tabId
}"
role="tabpanel"
>
<div class="tab-pane tableContainer" data-bind="attr:{id: tabId}" role="tabpanel">
<!-- Tables Query Tab Query Builder - Start-->
<div
class="query-builder"

View File

@@ -19,7 +19,7 @@ import TabsBase from "./TabsBase";
// Will act as table explorer class
export default class QueryTablesTab extends TabsBase {
public static readonly component = { name: "tables-query-tab", template };
public readonly html = template;
public collection: ViewModels.Collection;
public tableEntityListViewModel = ko.observable<TableEntityListViewModel>();
public queryViewModel = ko.observable<QueryViewModel>();
@@ -151,7 +151,7 @@ export default class QueryTablesTab extends TabsBase {
};
public onEditEntityClick = (): Q.Promise<any> => {
this.tableCommands.editEntityCommand(this.tableEntityListViewModel());
this.container.openEditTableEntityPanel(this, this.tableEntityListViewModel());
return null;
};

View File

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

View File

@@ -1,10 +1,8 @@
import { SchemaAnalyzerComponentAdapter } from "../Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponentAdapter";
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
import template from "./SchemaAnalyzerTab.html";
export default class SchemaAnalyzerTab extends NotebookTabBase {
public static readonly component = { name: "schema-analyzer-tab", template };
public readonly html = '<div data-bind="react:schemaAnalyzerComponentAdapter" style="height: 100%"></div>';
private schemaAnalyzerComponentAdapter: SchemaAnalyzerComponentAdapter;
constructor(options: NotebookTabBaseOptions) {

View File

@@ -1,5 +1,4 @@
import * as ko from "knockout";
import * as monaco from "monaco-editor";
import Q from "q";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg";
@@ -8,6 +7,7 @@ import editable from "../../Common/EditableUtility";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { loadMonaco, monaco } from "../LazyMonaco";
import TabsBase from "./TabsBase";
export default abstract class ScriptTabBase extends TabsBase implements ViewModels.WaitsForTemplate {
@@ -299,38 +299,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
return !!value;
}
private static _toSeverity(severity: string): monaco.MarkerSeverity {
switch (severity.toLowerCase()) {
case "error":
return monaco.MarkerSeverity.Error;
case "warning":
return monaco.MarkerSeverity.Warning;
case "info":
return monaco.MarkerSeverity.Info;
case "ignore":
default:
return monaco.MarkerSeverity.Hint;
}
}
private static _toEditorPosition(target: number, lines: string[]): ViewModels.EditorPosition {
let cursor: number = 0;
let previousCursor: number = 0;
let i: number = 0;
while (target > cursor + lines[i].length) {
cursor += lines[i].length + 2;
i++;
}
const editorPosition: ViewModels.EditorPosition = {
line: i + 1,
column: target - cursor + 1,
};
return editorPosition;
}
protected _createBodyEditor() {
protected async _createBodyEditor() {
const id = this.editorId;
const container = document.getElementById(id);
const options = {
@@ -341,7 +310,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
};
container.innerHTML = "";
const monaco = await loadMonaco();
const editor = monaco.editor.create(container, options);
this.editor(editor);
@@ -353,32 +322,4 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
const editorModel = this.editor().getModel();
this.editorContent(editorModel.getValue());
}
private _setModelMarkers(errors: ViewModels.QueryError[]) {
const markers: monaco.editor.IMarkerData[] = errors.map((e) => this._toMarker(e));
const editorModel = this.editor().getModel();
monaco.editor.setModelMarkers(editorModel, this.tabId, markers);
}
private _resetModelMarkers() {
const queryEditorModel = this.editor().getModel();
monaco.editor.setModelMarkers(queryEditorModel, this.tabId, []);
}
private _toMarker(error: ViewModels.QueryError): monaco.editor.IMarkerData {
const editorModel = this.editor().getModel();
const lines: string[] = editorModel.getLinesContent();
const start: ViewModels.EditorPosition = ScriptTabBase._toEditorPosition(Number(error.start), lines);
const end: ViewModels.EditorPosition = ScriptTabBase._toEditorPosition(Number(error.end), lines);
return {
severity: ScriptTabBase._toSeverity(error.severity),
message: error.message,
startLineNumber: start.line,
startColumn: start.column,
endLineNumber: end.line,
endColumn: end.column,
code: error.code,
};
}
}

View File

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

View File

@@ -1,18 +1,17 @@
import * as ViewModels from "../../Contracts/ViewModels";
import * as DataModels from "../../Contracts/DataModels";
import TabsBase from "./TabsBase";
import { SettingsComponentAdapter } from "../Controls/Settings/SettingsComponentAdapter";
import { SettingsComponentProps } from "../Controls/Settings/SettingsComponent";
import { traceFailure } from "../../Shared/Telemetry/TelemetryProcessor";
import ko from "knockout";
import * as Constants from "../../Common/Constants";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import template from "./SettingsTabV2.html";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { traceFailure } from "../../Shared/Telemetry/TelemetryProcessor";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import { SettingsComponentProps } from "../Controls/Settings/SettingsComponent";
import { SettingsComponentAdapter } from "../Controls/Settings/SettingsComponentAdapter";
import TabsBase from "./TabsBase";
export class SettingsTabV2 extends TabsBase {
public static readonly component = { name: "collection-settings-tab-v2", template };
public readonly html = '<div style="height: 100%" data-bind="react:settingsComponentAdapter"></div>';
public settingsComponentAdapter: SettingsComponentAdapter;
constructor(options: ViewModels.TabOptions) {
@@ -89,7 +88,6 @@ export class CollectionSettingsTabV2 extends SettingsTabV2 {
}
export class DatabaseSettingsTabV2 extends SettingsTabV2 {
public static readonly component = { name: "database-settings-tab-v2", template };
private notificationRead: ko.Observable<boolean>;
private notification: DataModels.Notification;

View File

@@ -22,7 +22,7 @@ enum ToggleState {
}
export default class StoredProcedureTab extends ScriptTabBase {
public static readonly component = { name: "stored-procedure-tab", template };
public readonly html = template;
public collection: ViewModels.Collection;
public node: StoredProcedure;
public executeResultsEditorId: string;

View File

@@ -12,11 +12,15 @@ export const Tabs = ({ tabs, activeTab }: { tabs: readonly Tab[]; activeTab: Tab
<div id="content" className="flexContainer hideOverflows">
<div className="nav-tabs-margin">
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
{...tabs.map((tab) => <TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />)}
{tabs.map((tab) => (
<TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />
))}
</ul>
</div>
<div className="tabPanesContainer">
{...tabs.map((tab) => <TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />)}
{tabs.map((tab) => (
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
))}
</div>
</div>
</div>
@@ -103,11 +107,8 @@ function TabPane({ tab, active }: { tab: Tab; active: boolean }) {
ko.applyBindings(tab, element);
const ctx = ko.contextFor(element).createChildContext(tab);
ko.applyBindingsToDescendants(ctx, element);
return () => ko.cleanNode(element);
}
if ("render" in tab) {
tab.isTemplateReady(true);
return () => ko.cleanNode(element);
}
}, [ref, tab]);
@@ -115,5 +116,5 @@ function TabPane({ tab, active }: { tab: Tab; active: boolean }) {
return <div {...attrs}>{tab.render()}</div>;
}
return <div {...attrs} ref={ref} data-bind="html: constructor.component.template" />;
return <div {...attrs} ref={ref} data-bind="html:html" />;
}

View File

@@ -14,7 +14,6 @@ import { TabsManager } from "./TabsManager";
// TODO: Use specific actions for logging telemetry data
export default class TabsBase extends WaitsForTemplateViewModel {
private static id = 0;
public static readonly component = { name: "tab", template: "" };
public closeTabButton: ViewModels.Button;
public node: ViewModels.TreeNode;
public collection: ViewModels.CollectionBase;

View File

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

View File

@@ -1,13 +1,12 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import * as DataModels from "../../Contracts/DataModels";
import TabsBase from "./TabsBase";
import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { NotebookTerminalComponent } from "../Controls/Notebook/NotebookTerminalComponent";
import Explorer from "../Explorer";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import template from "./TerminalTab.html";
import TabsBase from "./TabsBase";
export interface TerminalTabOptions extends ViewModels.TabOptions {
account: DataModels.DatabaseAccount;
@@ -39,7 +38,7 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
}
export default class TerminalTab extends TabsBase {
public static readonly component = { name: "terminal-tab", template };
public readonly html = '<div style="height: 100%" data-bind="react:notebookTerminalComponentAdapter"></div> ';
private container: Explorer;
private notebookTerminalComponentAdapter: NotebookTerminalComponentAdapter;

View File

@@ -3,16 +3,16 @@ import * as Constants from "../../Common/Constants";
import { createTrigger } from "../../Common/dataAccess/createTrigger";
import { updateTrigger } from "../../Common/dataAccess/updateTrigger";
import editable from "../../Common/EditableUtility";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import Trigger from "../Tree/Trigger";
import ScriptTabBase from "./ScriptTabBase";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import template from "./TriggerTab.html";
export default class TriggerTab extends ScriptTabBase {
public static readonly component = { name: "trigger-tab", template };
public readonly html = template;
public collection: ViewModels.Collection;
public node: Trigger;
public triggerType: ViewModels.Editable<string>;

View File

@@ -2,16 +2,16 @@ import { Resource, UserDefinedFunctionDefinition } from "@azure/cosmos";
import * as Constants from "../../Common/Constants";
import { createUserDefinedFunction } from "../../Common/dataAccess/createUserDefinedFunction";
import { updateUserDefinedFunction } from "../../Common/dataAccess/updateUserDefinedFunction";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import UserDefinedFunction from "../Tree/UserDefinedFunction";
import ScriptTabBase from "./ScriptTabBase";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import template from "./UserDefinedFunctionTab.html";
export default class UserDefinedFunctionTab extends ScriptTabBase {
public static readonly component = { name: "user-defined-function-tab", template };
public readonly html = template;
public collection: ViewModels.Collection;
public node: UserDefinedFunction;
constructor(options: ViewModels.ScriptTabOption) {

View File

@@ -23,7 +23,7 @@ export class AccessibleVerticalList {
this.onSelect = onSelect;
}
public onKeyDown = (source: any, event: KeyboardEvent): boolean => {
public onKeyDown = (_src: unknown, event: KeyboardEvent): boolean => {
const targetContainer: Element = <Element>event.target;
if (this.items == null || this.items.length === 0) {
// no items so this should be a noop

View File

@@ -31,9 +31,6 @@ describe("Collection", () => {
function generateMockCollectionWithDataModel(data: DataModels.Collection): Collection {
const mockContainer = {} as Explorer;
mockContainer.isPreferredApiMongoDB = ko.computed(() => {
return false;
});
mockContainer.isDatabaseNodeOrNoneSelected = () => {
return false;

View File

@@ -29,7 +29,6 @@ import MongoQueryTab from "../Tabs/MongoQueryTab";
import MongoShellTab from "../Tabs/MongoShellTab";
import QueryTab from "../Tabs/QueryTab";
import QueryTablesTab from "../Tabs/QueryTablesTab";
import SchemaAnalyzerTab from "../Tabs/SchemaAnalyzerTab";
import { CollectionSettingsTabV2 } from "../Tabs/SettingsTabV2";
import ConflictId from "./ConflictId";
import DocumentId from "./DocumentId";
@@ -128,16 +127,12 @@ export default class Collection implements ViewModels.Collection {
this.partitionKey.paths[0]) ||
null;
if (!!container.isPreferredApiMongoDB() && this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) {
if (userContext.apiType === "Mongo" && this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) {
this.partitionKeyProperty = this.partitionKeyProperty.replace(/["]+/g, "");
}
// TODO #10738269 : Add this logic in a derived class for Mongo
if (
!!container.isPreferredApiMongoDB() &&
this.partitionKeyProperty &&
this.partitionKeyProperty.indexOf("$v") > -1
) {
if (userContext.apiType === "Mongo" && this.partitionKeyProperty && this.partitionKeyProperty.indexOf("$v") > -1) {
// From $v.shard.$v.key.$v > shard.key
this.partitionKeyProperty = this.partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, "");
this.partitionKeyPropertyHeader = "/" + this.partitionKeyProperty;
@@ -515,9 +510,10 @@ export default class Collection implements ViewModels.Collection {
}
};
public onSchemaAnalyzerClick = () => {
public onSchemaAnalyzerClick = async () => {
this.container.selectedNode(this);
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default;
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Mongo Schema node",
databaseName: this.databaseId,
@@ -1123,7 +1119,7 @@ export default class Collection implements ViewModels.Collection {
} else if (userContext.apiType === "Gremlin") {
this.onGraphDocumentsClick();
return;
} else if (this.container.isPreferredApiMongoDB()) {
} else if (userContext.apiType === "Mongo") {
this.onMongoDBDocumentsClick();
return;
}
@@ -1141,7 +1137,7 @@ export default class Collection implements ViewModels.Collection {
return "Rows";
} else if (userContext.apiType === "Gremlin") {
return "Graph";
} else if (this.container.isPreferredApiMongoDB()) {
} else if (userContext.apiType === "Mongo") {
return "Documents";
}

View File

@@ -761,7 +761,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
{
label: "Rename",
iconSrc: NotebookIcon,
onClick: () => this.container.renameNotebook(item).then(() => this.triggerRender()),
onClick: () => this.container.renameNotebook(item),
},
{
label: "New Directory",
@@ -930,7 +930,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
}
private cleanupDatabasesKoSubs(): void {
this.koSubsDatabaseIdMap.keys().forEach((databaseId: string) => {
for (const databaseId of this.koSubsDatabaseIdMap.keys()) {
this.koSubsDatabaseIdMap.get(databaseId).forEach((sub: ko.Subscription) => sub.dispose());
this.koSubsDatabaseIdMap.delete(databaseId);
@@ -939,7 +939,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
.get(databaseId)
.forEach((collectionId: string) => this.cleanupKoSubsForCollection(databaseId, collectionId));
}
});
}
}
private cleanupCollectionsKoSubs(databaseId: string, existingCollectionIds: string[]): void {