Merge branch 'master' into replace-codemirror-with-monaco

This commit is contained in:
Laurent Nguyen
2020-08-13 16:30:46 +02:00
344 changed files with 14923 additions and 47392 deletions

View File

@@ -116,14 +116,6 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("setup-notebooks-pane")).toBe(true);
});
it("should register setup-spark-cluster-pane component", () => {
expect(ko.components.isRegistered("setup-spark-cluster-pane")).toBe(true);
});
it("should register manage-spark-cluster-pane component", () => {
expect(ko.components.isRegistered("manage-spark-cluster-pane")).toBe(true);
});
it("should register dynamic-list component", () => {
expect(ko.components.isRegistered("dynamic-list")).toBe(true);
});
@@ -131,12 +123,4 @@ describe("Component Registerer", () => {
it("should register throughput-input component", () => {
expect(ko.components.isRegistered("throughput-input")).toBe(true);
});
it("should register library-manage-pane component", () => {
expect(ko.components.isRegistered("library-manage-pane")).toBe(true);
});
it("should register cluster-library-pane component", () => {
expect(ko.components.isRegistered("cluster-library-pane")).toBe(true);
});
});

View File

@@ -10,11 +10,10 @@ import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleCompo
import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead";
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
import { NewVertexComponent } from "./Graph/NewVertexComponent/NewVertexComponent";
import { TabsManagerKOComponent } from "./Tabs/TabsManager";
import { ThroughputInputComponent } from "./Controls/ThroughputInput/ThroughputInputComponent";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import { ToolbarComponent } from "./Controls/Toolbar/Toolbar";
ko.components.register("toolbar", new ToolbarComponent());
ko.components.register("input-typeahead", new InputTypeaheadComponent());
ko.components.register("new-vertex-form", NewVertexComponent);
ko.components.register("error-display", new ErrorDisplayComponent());
@@ -26,6 +25,7 @@ ko.components.register("diff-editor", new DiffEditorComponent());
ko.components.register("dynamic-list", DynamicListComponent);
ko.components.register("throughput-input", ThroughputInputComponent);
ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponentAutoPilotV3);
ko.components.register("tabs-manager", TabsManagerKOComponent());
// Collection Tabs
ko.components.register("documents-tab", new TabComponents.DocumentsTab());
@@ -76,8 +76,4 @@ ko.components.register("browse-queries-pane", new PaneComponents.BrowseQueriesPa
ko.components.register("upload-file-pane", new PaneComponents.UploadFilePaneComponent());
ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent());
ko.components.register("setup-notebooks-pane", new PaneComponents.SetupNotebooksPaneComponent());
ko.components.register("setup-spark-cluster-pane", new PaneComponents.SetupSparkClusterPaneComponent());
ko.components.register("manage-spark-cluster-pane", new PaneComponents.ManageSparkClusterPaneComponent());
ko.components.register("library-manage-pane", new PaneComponents.LibraryManagePaneComponent());
ko.components.register("cluster-library-pane", new PaneComponents.ClusterLibraryPaneComponent());
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());

View File

@@ -12,6 +12,10 @@ import AddTriggerIcon from "../../images/AddTrigger.svg";
import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
import DeleteSprocIcon from "../../images/DeleteSproc.svg";
import Explorer from "./Explorer";
import UserDefinedFunction from "./Tree/UserDefinedFunction";
import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger";
export interface CollectionContextMenuButtonParams {
databaseId: string;
@@ -26,7 +30,7 @@ export interface DatabaseContextMenuButtonParams {
*/
export class ResourceTreeContextMenuButtonFactory {
public static createDatabaseContextMenu(
container: ViewModels.Explorer,
container: Explorer,
selectedDatabase: ViewModels.Database
): TreeNodeMenuItem[] {
const newCollectionMenuItem: TreeNodeMenuItem = {
@@ -44,7 +48,7 @@ export class ResourceTreeContextMenuButtonFactory {
}
public static createCollectionContextMenuButton(
container: ViewModels.Explorer,
container: Explorer,
selectedCollection: ViewModels.Collection
): TreeNodeMenuItem[] {
const items: TreeNodeMenuItem[] = [];
@@ -115,8 +119,8 @@ export class ResourceTreeContextMenuButtonFactory {
}
public static createStoreProcedureContextMenuItems(
container: ViewModels.Explorer,
storedProcedure: ViewModels.StoredProcedure
container: Explorer,
storedProcedure: StoredProcedure
): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) {
return [];
@@ -131,10 +135,7 @@ export class ResourceTreeContextMenuButtonFactory {
];
}
public static createTriggerContextMenuItems(
container: ViewModels.Explorer,
trigger: ViewModels.Trigger
): TreeNodeMenuItem[] {
public static createTriggerContextMenuItems(container: Explorer, trigger: Trigger): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) {
return [];
}
@@ -149,8 +150,8 @@ export class ResourceTreeContextMenuButtonFactory {
}
public static createUserDefinedFunctionContextMenuItems(
container: ViewModels.Explorer,
userDefinedFunction: ViewModels.UserDefinedFunction
container: Explorer,
userDefinedFunction: UserDefinedFunction
): TreeNodeMenuItem[] {
if (container.isPreferredApiCassandra()) {
return [];

View File

@@ -3,6 +3,7 @@ import { Dialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
import { Link } from "office-ui-fabric-react/lib/Link";
import { FontIcon } from "office-ui-fabric-react";
export interface TextFieldProps extends ITextFieldProps {
label: string;
@@ -31,6 +32,8 @@ export interface DialogProps {
onSecondaryButtonClick: () => void;
primaryButtonDisabled?: boolean;
type?: DialogType;
showCloseButton?: boolean;
onDismiss?: () => void;
}
const DIALOG_MIN_WIDTH = "400px";
@@ -55,7 +58,8 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT },
subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE }
},
showCloseButton: false
showCloseButton: this.props.showCloseButton || false,
onDismiss: this.props.onDismiss
},
modalProps: { isBlocking: this.props.isModal },
minWidth: DIALOG_MIN_WIDTH,
@@ -81,7 +85,7 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
{textFieldProps && <TextField {...textFieldProps} />}
{linkProps && (
<Link href={linkProps.linkUrl} target="_blank">
{linkProps.linkText}
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
</Link>
)}
<DialogFooter>

View File

@@ -84,6 +84,8 @@ exports[`test render renders with filters 1`] = `
"elevation64": "0 25.6px 57.6px 0 rgba(0, 0, 0, 0.22), 0 4.8px 14.4px 0 rgba(0, 0, 0, 0.18)",
"elevation8": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)",
"roundedCorner2": "2px",
"roundedCorner4": "4px",
"roundedCorner6": "6px",
},
"fonts": Object {
"large": Object {
@@ -421,6 +423,8 @@ exports[`test render renders with filters 1`] = `
"elevation64": "0 25.6px 57.6px 0 rgba(0, 0, 0, 0.22), 0 4.8px 14.4px 0 rgba(0, 0, 0, 0.18)",
"elevation8": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)",
"roundedCorner2": "2px",
"roundedCorner4": "4px",
"roundedCorner6": "6px",
},
"fonts": Object {
"large": Object {
@@ -812,6 +816,8 @@ exports[`test render renders with filters 1`] = `
"elevation64": "0 25.6px 57.6px 0 rgba(0, 0, 0, 0.22), 0 4.8px 14.4px 0 rgba(0, 0, 0, 0.18)",
"elevation8": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)",
"roundedCorner2": "2px",
"roundedCorner4": "4px",
"roundedCorner6": "6px",
},
"fonts": Object {
"large": Object {
@@ -1588,6 +1594,8 @@ exports[`test render renders with filters 1`] = `
"elevation64": "0 25.6px 57.6px 0 rgba(0, 0, 0, 0.22), 0 4.8px 14.4px 0 rgba(0, 0, 0, 0.18)",
"elevation8": "0 3.2px 7.2px 0 rgba(0, 0, 0, 0.132), 0 0.6px 1.8px 0 rgba(0, 0, 0, 0.108)",
"roundedCorner2": "2px",
"roundedCorner4": "4px",
"roundedCorner6": "6px",
},
"fonts": Object {
"large": Object {

View File

@@ -23,8 +23,5 @@ interface ErrorDisplayParams {
}
class ErrorDisplayViewModel {
private params: ErrorDisplayParams;
public constructor(params: ErrorDisplayParams) {
this.params = params;
}
public constructor(public params: ErrorDisplayParams) {}
}

View File

@@ -49,6 +49,12 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
{ key: "feature.enablecodeofconduct", label: "Enable Code Of Conduct Acknowledgement", value: "true" },
{
key: "feature.enableLinkInjection",
label: "Enable Injecting Notebook Viewer Link into the first cell",
value: "true"
},
{ key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
{
key: "feature.enablefixedcollectionwithsharedthroughput",

View File

@@ -163,8 +163,14 @@ exports[`Feature panel renders all flags 1`] = `
/>
<StyledCheckboxBase
checked={false}
key="feature.canexceedmaximumvalue"
label="Can exceed max value"
key="feature.enablecodeofconduct"
label="Enable Code Of Conduct Acknowledgement"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.enableLinkInjection"
label="Enable Injecting Notebook Viewer Link into the first cell"
onChange={[Function]}
/>
</Stack>
@@ -172,6 +178,12 @@ exports[`Feature panel renders all flags 1`] = `
className="checkboxRow"
horizontalAlign="space-between"
>
<StyledCheckboxBase
checked={false}
key="feature.canexceedmaximumvalue"
label="Can exceed max value"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.enablefixedcollectionwithsharedthroughput"

View File

@@ -1,6 +1,5 @@
import { DefaultButton, IButtonProps, ITextFieldProps, TextField } from "office-ui-fabric-react";
import * as React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as Constants from "../../../Common/Constants";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { RepoListItem } from "./GitHubReposComponent";
@@ -9,9 +8,10 @@ import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import UrlUtility from "../../../Common/UrlUtility";
import Explorer from "../../Explorer";
export interface AddRepoComponentProps {
container: ViewModels.Explorer;
container: Explorer;
getRepo: (owner: string, repo: string) => Promise<IGitHubRepo>;
pinRepo: (item: RepoListItem) => void;
}

View File

@@ -0,0 +1,81 @@
import * as React from "react";
import { Stack, Text, Separator, FontIcon, CommandButton, FontWeights, ITextProps } from "office-ui-fabric-react";
export class GalleryHeaderComponent extends React.Component {
private static readonly azureText = "Microsoft Azure";
private static readonly cosmosdbText = "Cosmos DB";
private static readonly galleryText = "Gallery";
private static readonly loginText = "Sign In";
private static readonly openPortal = () => window.open("https://portal.azure.com", "_blank");
private static readonly openDataExplorer = () => (window.location.href = new URL("./", window.location.href).href);
private static readonly headerItemStyle: React.CSSProperties = {
color: "white"
};
private static readonly mainHeaderTextProps: ITextProps = {
style: GalleryHeaderComponent.headerItemStyle,
variant: "mediumPlus",
styles: {
root: {
fontWeight: FontWeights.semibold
}
}
};
private static readonly headerItemTextProps: ITextProps = { style: GalleryHeaderComponent.headerItemStyle };
private renderHeaderItem = (text: string, onClick: () => void, textProps: ITextProps): JSX.Element => {
return (
<CommandButton onClick={onClick} ariaLabel={text}>
<Text {...textProps}>{text}</Text>
</CommandButton>
);
};
public render(): JSX.Element {
return (
<Stack
tokens={{ childrenGap: 10 }}
horizontal
styles={{ root: { background: "#0078d4", paddingLeft: 20, paddingRight: 20 } }}
verticalAlign="center"
>
<Stack.Item>
{this.renderHeaderItem(
GalleryHeaderComponent.azureText,
GalleryHeaderComponent.openPortal,
GalleryHeaderComponent.mainHeaderTextProps
)}
</Stack.Item>
<Stack.Item>
<Separator vertical />
</Stack.Item>
<Stack.Item>
{this.renderHeaderItem(
GalleryHeaderComponent.cosmosdbText,
GalleryHeaderComponent.openDataExplorer,
GalleryHeaderComponent.headerItemTextProps
)}
</Stack.Item>
<Stack.Item>
<FontIcon style={GalleryHeaderComponent.headerItemStyle} iconName="ChevronRight" />
</Stack.Item>
<Stack.Item>
{this.renderHeaderItem(
GalleryHeaderComponent.galleryText,
undefined,
GalleryHeaderComponent.headerItemTextProps
)}
</Stack.Item>
<Stack.Item grow>
<></>
</Stack.Item>
<Stack.Item>
{this.renderHeaderItem(
GalleryHeaderComponent.loginText,
GalleryHeaderComponent.openDataExplorer,
GalleryHeaderComponent.headerItemTextProps
)}
</Stack.Item>
</Stack>
);
}
}

View File

@@ -1,40 +0,0 @@
import * as React from "react";
import { DetailsList, IColumn, SelectionMode } from "office-ui-fabric-react/lib/DetailsList";
import { Library } from "../../../Contracts/DataModels";
export interface ClusterLibraryItem extends Library {
installed: boolean;
}
export interface ClusterLibraryGridProps {
libraryItems: ClusterLibraryItem[];
onInstalledChanged: (libraryName: string, installed: boolean) => void;
}
export function ClusterLibraryGrid(props: ClusterLibraryGridProps): JSX.Element {
const onInstalledChanged = (e: React.FormEvent<HTMLInputElement>) => {
const target = e.target;
const libraryName = (target as any).dataset.name;
const checked = (target as any).checked;
return props.onInstalledChanged(libraryName, checked);
};
const columns: IColumn[] = [
{
key: "name",
name: "Name",
fieldName: "name",
minWidth: 150
},
{
key: "installed",
name: "Installed",
minWidth: 100,
onRender: (item: ClusterLibraryItem) => {
return <input type="checkbox" checked={item.installed} onChange={onInstalledChanged} data-name={item.name} />;
}
}
];
return <DetailsList columns={columns} items={props.libraryItems} selectionMode={SelectionMode.none} />;
}

View File

@@ -1,11 +0,0 @@
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { ClusterLibraryGrid, ClusterLibraryGridProps } from "./ClusterLibraryGrid";
export class ClusterLibraryGridAdapter implements ReactAdapter {
public parameters: ko.Observable<ClusterLibraryGridProps>;
public renderComponent(): JSX.Element {
return <ClusterLibraryGrid {...this.parameters()} />;
}
}

View File

@@ -1,156 +0,0 @@
import * as React from "react";
import DeleteIcon from "../../../../images/delete.svg";
import { Button } from "office-ui-fabric-react/lib/Button";
import { DetailsList, IColumn, SelectionMode } from "office-ui-fabric-react/lib/DetailsList";
import { Library } from "../../../Contracts/DataModels";
import { Label } from "office-ui-fabric-react/lib/Label";
import { SparkLibrary } from "../../../Common/Constants";
import { TextField } from "office-ui-fabric-react/lib/TextField";
export interface LibraryManageComponentProps {
addProps: {
nameProps: LibraryAddNameTextFieldProps;
urlProps: LibraryAddUrlTextFieldProps;
buttonProps: LibraryAddButtonProps;
};
gridProps: LibraryManageGridProps;
}
export function LibraryManageComponent(props: LibraryManageComponentProps): JSX.Element {
const {
addProps: { nameProps, urlProps, buttonProps },
gridProps
} = props;
return (
<div>
<div className="library-add-container">
<LibraryAddNameTextField {...nameProps} />
<LibraryAddUrlTextField {...urlProps} />
<LibraryAddButton {...buttonProps} />
</div>
<div className="library-grid-container">
<Label>All Libraries</Label>
<LibraryManageGrid {...gridProps} />
</div>
</div>
);
}
export interface LibraryManageGridProps {
items: Library[];
onLibraryDeleteClick: (libraryName: string) => void;
}
function LibraryManageGrid(props: LibraryManageGridProps): JSX.Element {
const columns: IColumn[] = [
{
key: "name",
name: "Name",
fieldName: "name",
minWidth: 200
},
{
key: "delete",
name: "Delete",
minWidth: 60,
onRender: (item: Library) => {
const onDelete = () => {
props.onLibraryDeleteClick(item.name);
};
return (
<span className="library-delete">
<img src={DeleteIcon} alt="Delete" onClick={onDelete} />
</span>
);
}
}
];
return <DetailsList columns={columns} items={props.items} selectionMode={SelectionMode.none} />;
}
export interface LibraryAddButtonProps {
disabled: boolean;
onLibraryAddClick: (event: React.FormEvent<any>) => void;
}
function LibraryAddButton(props: LibraryAddButtonProps): JSX.Element {
return (
<Button text="Add" className="library-add-button" onClick={props.onLibraryAddClick} disabled={props.disabled} />
);
}
export interface LibraryAddUrlTextFieldProps {
libraryAddress: string;
onLibraryAddressChange: (libraryAddress: string) => void;
onLibraryAddressValidated: (errorMessage: string, value: string) => void;
}
function LibraryAddUrlTextField(props: LibraryAddUrlTextFieldProps): JSX.Element {
const handleTextChange = (e: React.FormEvent<any>, libraryAddress: string) => {
props.onLibraryAddressChange(libraryAddress);
};
const validateText = (text: string): string => {
if (!text) {
return "";
}
const libraryUrlRegex = /^(https:\/\/.+\/)(.+)\.(jar)$/gi;
const isValidUrl = libraryUrlRegex.test(text);
if (isValidUrl) {
return "";
}
return "Need to be a valid https uri";
};
return (
<TextField
value={props.libraryAddress}
label="Url"
type="url"
className="library-add-textfield"
onChange={handleTextChange}
onGetErrorMessage={validateText}
onNotifyValidationResult={props.onLibraryAddressValidated}
placeholder="https://myrepo/myjar.jar"
autoComplete="off"
/>
);
}
export interface LibraryAddNameTextFieldProps {
libraryName: string;
onLibraryNameChange: (libraryName: string) => void;
onLibraryNameValidated: (errorMessage: string, value: string) => void;
}
function LibraryAddNameTextField(props: LibraryAddNameTextFieldProps): JSX.Element {
const handleTextChange = (e: React.FormEvent<any>, libraryName: string) => {
props.onLibraryNameChange(libraryName);
};
const validateText = (text: string): string => {
if (!text) {
return "";
}
const length = text.length;
if (length < SparkLibrary.nameMinLength || length > SparkLibrary.nameMaxLength) {
return "Library name length need to be between 3 and 63.";
}
const nameRegex = /^[a-z0-9][-a-z0-9]*[a-z0-9]$/gi;
const isValidUrl = nameRegex.test(text);
if (isValidUrl) {
return "";
}
return "Need to be a valid name. Letters, numbers and - are allowed";
};
return (
<TextField
value={props.libraryName}
label="Name"
type="text"
className="library-add-textfield"
onChange={handleTextChange}
onGetErrorMessage={validateText}
onNotifyValidationResult={props.onLibraryNameValidated}
placeholder="myjar"
autoComplete="off"
/>
);
}

View File

@@ -1,11 +0,0 @@
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { LibraryManageComponent, LibraryManageComponentProps } from "./LibraryManage";
export class LibraryManageComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<LibraryManageComponentProps>;
public renderComponent(): JSX.Element {
return <LibraryManageComponent {...this.parameters()} />;
}
}

View File

@@ -5,7 +5,7 @@
import * as React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import * as Logger from "../../../Common/Logger";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { StringUtils } from "../../../Utils/StringUtils";

View File

@@ -1,72 +0,0 @@
import { FontWeights } from "@uifabric/styling";
import { IIconStyles, ITextStyles } from "office-ui-fabric-react";
export const siteTextStyles: ITextStyles = {
root: {
color: "#025F52",
fontWeight: FontWeights.semibold
}
};
export const descriptionTextStyles: ITextStyles = {
root: {
color: "#333333",
fontWeight: FontWeights.semibold
}
};
export const subtleHelpfulTextStyles: ITextStyles = {
root: {
color: "#ccc",
fontWeight: FontWeights.regular
}
};
export const iconButtonStyles: IIconStyles = {
root: {
marginLeft: "10px",
color: "#0078D4",
backgroundColor: "#FFF",
fontSize: 16,
fontWeight: FontWeights.regular,
display: "inline-block",
selectors: {
":hover .ms-Button-icon": {
color: "#ccc"
}
}
}
};
export const iconStyles: IIconStyles = {
root: {
marginLeft: "10px",
color: "#0078D4",
backgroundColor: "#FFF",
fontSize: 16,
fontWeight: FontWeights.regular,
display: "inline-block"
}
};
export const mainHelpfulTextStyles: ITextStyles = {
root: {
color: "#000",
fontWeight: FontWeights.regular
}
};
export const subtleIconStyles: IIconStyles = {
root: {
color: "#ddd",
fontSize: 12,
fontWeight: FontWeights.regular
}
};
export const helpfulTextStyles: ITextStyles = {
root: {
color: "#333333",
fontWeight: FontWeights.regular
}
};

View File

@@ -17,9 +17,11 @@ describe("GalleryCardComponent", () => {
isSample: false,
downloads: 0,
favorites: 0,
views: 0
views: 0,
newCellId: undefined
},
isFavorite: false,
showDownload: true,
showDelete: true,
onClick: undefined,
onTagClick: undefined,

View File

@@ -1,4 +1,4 @@
import { Card, ICardTokens } from "@uifabric/react-cards";
import { Card } from "@uifabric/react-cards";
import {
FontWeights,
Icon,
@@ -18,10 +18,12 @@ import * as React from "react";
import { IGalleryItem } from "../../../../Juno/JunoClient";
import { FileSystemUtil } from "../../../Notebook/FileSystemUtil";
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
import { StyleConstants } from "../../../../Common/Constants";
export interface GalleryCardComponentProps {
data: IGalleryItem;
isFavorite: boolean;
showDownload: boolean;
showDelete: boolean;
onClick: () => void;
onTagClick: (tag: string) => void;
@@ -32,30 +34,32 @@ export interface GalleryCardComponentProps {
}
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> {
public static readonly CARD_HEIGHT = 384;
public static readonly CARD_WIDTH = 256;
private static readonly cardImageHeight = 144;
public static readonly cardHeightToWidthRatio =
GalleryCardComponent.cardImageHeight / GalleryCardComponent.CARD_WIDTH;
private static readonly cardDescriptionMaxChars = 88;
private static readonly cardTokens: ICardTokens = {
width: GalleryCardComponent.CARD_WIDTH,
height: GalleryCardComponent.CARD_HEIGHT,
childrenGap: 8,
childrenMargin: 10
};
private static readonly cardItemGapBig = 10;
private static readonly cardItemGapSmall = 8;
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 aria-label="Notebook Card" tokens={GalleryCardComponent.cardTokens} onClick={this.props.onClick}>
<Card.Item>
<Card
aria-label={cardTitle}
data-is-focusable="true"
tokens={{ width: GalleryCardComponent.CARD_WIDTH, childrenGap: 0 }}
onClick={event => this.onClick(event, this.props.onClick)}
>
<Card.Item tokens={{ padding: GalleryCardComponent.cardItemGapBig }}>
<Persona
imageUrl={this.props.data.isSample && CosmosDBLogo}
text={this.props.data.author}
@@ -63,69 +67,89 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
/>
</Card.Item>
<Card.Item fill>
<Card.Item>
<Image
src={
this.props.data.thumbnailUrl ||
`https://placehold.it/${GalleryCardComponent.CARD_WIDTH}x${GalleryCardComponent.cardImageHeight}`
}
src={this.props.data.thumbnailUrl}
width={GalleryCardComponent.CARD_WIDTH}
height={GalleryCardComponent.cardImageHeight}
imageFit={ImageFit.cover}
alt="Notebook cover image"
alt={`${cardTitle} cover image`}
/>
</Card.Item>
<Card.Section>
<Card.Section styles={{ root: { padding: GalleryCardComponent.cardItemGapBig } }}>
<Text variant="small" nowrap>
{this.props.data.tags?.map((tag, index, array) => (
<span key={tag}>
<Link onClick={(event): void => this.onTagClick(event, tag)}>{tag}</Link>
<Link onClick={event => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link>
{index === array.length - 1 ? <></> : ", "}
</span>
))}
</Text>
<Text styles={{ root: { fontWeight: FontWeights.semibold } }} nowrap>
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
<Text
styles={{
root: {
fontWeight: FontWeights.semibold,
paddingTop: GalleryCardComponent.cardItemGapSmall,
paddingBottom: GalleryCardComponent.cardItemGapSmall
}
}}
nowrap
>
{cardTitle}
</Text>
<Text variant="small" styles={{ root: { height: 36 } }}>
{this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars)}
</Text>
<span>
{this.generateIconText("RedEye", this.props.data.views.toString())}
{this.generateIconText("Download", this.props.data.downloads.toString())}
{this.props.isFavorite !== undefined &&
this.generateIconText("Heart", this.props.data.favorites.toString())}
</span>
</Card.Section>
<Card.Section horizontal styles={{ root: { alignItems: "flex-end" } }}>
{this.generateIconText("RedEye", this.props.data.views.toString())}
{this.generateIconText("Download", this.props.data.downloads.toString())}
{this.props.isFavorite !== undefined && this.generateIconText("Heart", this.props.data.favorites.toString())}
</Card.Section>
{cardButtonsVisible && (
<Card.Section
styles={{
root: {
marginLeft: GalleryCardComponent.cardItemGapBig,
marginRight: GalleryCardComponent.cardItemGapBig
}
}}
>
<Separator styles={{ root: { padding: 0, height: 1 } }} />
<Card.Item>
<Separator styles={{ root: { padding: 0, height: 1 } }} />
</Card.Item>
<span>
{this.props.isFavorite !== undefined &&
this.generateIconButtonWithTooltip(
this.props.isFavorite ? "HeartFill" : "Heart",
this.props.isFavorite ? "Unlike" : "Like",
"left",
this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick
)}
<Card.Section horizontal styles={{ root: { marginTop: 0 } }}>
{this.props.isFavorite !== undefined &&
this.generateIconButtonWithTooltip(
this.props.isFavorite ? "HeartFill" : "Heart",
this.props.isFavorite ? "Unlike" : "Like",
this.props.isFavorite ? this.onUnfavoriteClick : this.onFavoriteClick
)}
{this.props.showDownload &&
this.generateIconButtonWithTooltip("Download", "Download", "left", this.props.onDownloadClick)}
{this.generateIconButtonWithTooltip("Download", "Download", this.onDownloadClick)}
{this.props.showDelete && (
<div style={{ width: "100%", textAlign: "right" }}>
{this.generateIconButtonWithTooltip("Delete", "Remove", this.onDeleteClick)}
</div>
)}
</Card.Section>
{this.props.showDelete &&
this.generateIconButtonWithTooltip("Delete", "Remove", "right", this.props.onDeleteClick)}
</span>
</Card.Section>
)}
</Card>
);
}
private generateIconText = (iconName: string, text: string): JSX.Element => {
return (
<Text variant="tiny" styles={{ root: { color: "#ccc" } }}>
<Text
variant="tiny"
styles={{ root: { color: StyleConstants.BaseMedium, paddingRight: GalleryCardComponent.cardItemGapSmall } }}
>
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} /> {text}
</Text>
);
@@ -138,70 +162,37 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
private generateIconButtonWithTooltip = (
iconName: string,
title: string,
onClick: (
event: React.MouseEvent<
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
MouseEvent
>
) => void
horizontalAlign: "right" | "left",
activate: () => void
): JSX.Element => {
return (
<TooltipHost
content={title}
id={`TooltipHost-IconButton-${iconName}`}
calloutProps={{ gapSpace: 0 }}
styles={{ root: { display: "inline-block" } }}
styles={{ root: { display: "inline-block", float: horizontalAlign } }}
>
<IconButton iconProps={{ iconName }} title={title} ariaLabel={title} onClick={onClick} />
<IconButton
iconProps={{ iconName }}
title={title}
ariaLabel={title}
onClick={event => this.onClick(event, activate)}
/>
</TooltipHost>
);
};
private onTagClick = (
event: React.MouseEvent<HTMLElement | HTMLAnchorElement | HTMLButtonElement | LinkBase, MouseEvent>,
tag: string
private onClick = (
event:
| React.MouseEvent<HTMLElement | HTMLAnchorElement | HTMLButtonElement | LinkBase, MouseEvent>
| React.MouseEvent<
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
MouseEvent
>,
activate: () => void
): void => {
event.stopPropagation();
this.props.onTagClick(tag);
};
private onFavoriteClick = (
event: React.MouseEvent<
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
MouseEvent
>
): void => {
event.stopPropagation();
this.props.onFavoriteClick();
};
private onUnfavoriteClick = (
event: React.MouseEvent<
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
MouseEvent
>
): void => {
event.stopPropagation();
this.props.onUnfavoriteClick();
};
private onDownloadClick = (
event: React.MouseEvent<
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
MouseEvent
>
): void => {
event.stopPropagation();
this.props.onDownloadClick();
};
private onDeleteClick = (
event: React.MouseEvent<
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
MouseEvent
>
): void => {
event.stopPropagation();
this.props.onDeleteClick();
event.preventDefault();
activate();
};
}

View File

@@ -2,35 +2,47 @@
exports[`GalleryCardComponent renders 1`] = `
<Card
aria-label="Notebook Card"
aria-label="name"
data-is-focusable="true"
onClick={[Function]}
tokens={
Object {
"childrenGap": 8,
"childrenMargin": 10,
"height": 384,
"childrenGap": 0,
"width": 256,
}
}
>
<CardItem>
<CardItem
tokens={
Object {
"padding": 10,
}
}
>
<StyledPersonaBase
imageUrl={false}
secondaryText="Invalid Date"
text="author"
/>
</CardItem>
<CardItem
fill={true}
>
<StyledImageBase
alt="Notebook cover image"
<CardItem>
<Memo(StyledImageBase)
alt="name cover image"
height={144}
imageFit={2}
src="thumbnailUrl"
width={256}
/>
</CardItem>
<CardSection>
<CardSection
styles={
Object {
"root": Object {
"padding": 10,
},
}
}
>
<Text
nowrap={true}
variant="small"
@@ -51,6 +63,8 @@ exports[`GalleryCardComponent renders 1`] = `
Object {
"root": Object {
"fontWeight": 600,
"paddingBottom": 8,
"paddingTop": 8,
},
}
}
@@ -69,88 +83,91 @@ exports[`GalleryCardComponent renders 1`] = `
>
description
</Text>
<span>
<Text
styles={
Object {
"root": Object {
"color": undefined,
"paddingRight": 8,
},
}
}
variant="tiny"
>
<Memo(StyledIconBase)
iconName="RedEye"
styles={
Object {
"root": Object {
"verticalAlign": "middle",
},
}
}
/>
0
</Text>
<Text
styles={
Object {
"root": Object {
"color": undefined,
"paddingRight": 8,
},
}
}
variant="tiny"
>
<Memo(StyledIconBase)
iconName="Download"
styles={
Object {
"root": Object {
"verticalAlign": "middle",
},
}
}
/>
0
</Text>
<Text
styles={
Object {
"root": Object {
"color": undefined,
"paddingRight": 8,
},
}
}
variant="tiny"
>
<Memo(StyledIconBase)
iconName="Heart"
styles={
Object {
"root": Object {
"verticalAlign": "middle",
},
}
}
/>
0
</Text>
</span>
</CardSection>
<CardSection
horizontal={true}
styles={
Object {
"root": Object {
"alignItems": "flex-end",
"marginLeft": 10,
"marginRight": 10,
},
}
}
>
<Text
styles={
Object {
"root": Object {
"color": "#ccc",
},
}
}
variant="tiny"
>
<StyledIconBase
iconName="RedEye"
styles={
Object {
"root": Object {
"verticalAlign": "middle",
},
}
}
/>
0
</Text>
<Text
styles={
Object {
"root": Object {
"color": "#ccc",
},
}
}
variant="tiny"
>
<StyledIconBase
iconName="Download"
styles={
Object {
"root": Object {
"verticalAlign": "middle",
},
}
}
/>
0
</Text>
<Text
styles={
Object {
"root": Object {
"color": "#ccc",
},
}
}
variant="tiny"
>
<StyledIconBase
iconName="Heart"
styles={
Object {
"root": Object {
"verticalAlign": "middle",
},
}
}
/>
0
</Text>
</CardSection>
<CardItem>
<Styled
styles={
Object {
@@ -161,79 +178,63 @@ exports[`GalleryCardComponent renders 1`] = `
}
}
/>
</CardItem>
<CardSection
horizontal={true}
styles={
Object {
"root": Object {
"marginTop": 0,
},
}
}
>
<StyledTooltipHostBase
calloutProps={
Object {
"gapSpace": 0,
}
}
content="Like"
id="TooltipHost-IconButton-Heart"
styles={
Object {
"root": Object {
"display": "inline-block",
},
}
}
>
<CustomizedIconButton
ariaLabel="Like"
iconProps={
<span>
<StyledTooltipHostBase
calloutProps={
Object {
"iconName": "Heart",
"gapSpace": 0,
}
}
onClick={[Function]}
title="Like"
/>
</StyledTooltipHostBase>
<StyledTooltipHostBase
calloutProps={
Object {
"gapSpace": 0,
}
}
content="Download"
id="TooltipHost-IconButton-Download"
styles={
Object {
"root": Object {
"display": "inline-block",
},
}
}
>
<CustomizedIconButton
ariaLabel="Download"
iconProps={
content="Like"
id="TooltipHost-IconButton-Heart"
styles={
Object {
"iconName": "Download",
"root": Object {
"display": "inline-block",
"float": "left",
},
}
}
onClick={[Function]}
title="Download"
/>
</StyledTooltipHostBase>
<div
style={
Object {
"textAlign": "right",
"width": "100%",
>
<CustomizedIconButton
ariaLabel="Like"
iconProps={
Object {
"iconName": "Heart",
}
}
onClick={[Function]}
title="Like"
/>
</StyledTooltipHostBase>
<StyledTooltipHostBase
calloutProps={
Object {
"gapSpace": 0,
}
}
}
>
content="Download"
id="TooltipHost-IconButton-Download"
styles={
Object {
"root": Object {
"display": "inline-block",
"float": "left",
},
}
}
>
<CustomizedIconButton
ariaLabel="Download"
iconProps={
Object {
"iconName": "Download",
}
}
onClick={[Function]}
title="Download"
/>
</StyledTooltipHostBase>
<StyledTooltipHostBase
calloutProps={
Object {
@@ -246,6 +247,7 @@ exports[`GalleryCardComponent renders 1`] = `
Object {
"root": Object {
"display": "inline-block",
"float": "right",
},
}
}
@@ -261,7 +263,7 @@ exports[`GalleryCardComponent renders 1`] = `
title="Remove"
/>
</StyledTooltipHostBase>
</div>
</span>
</CardSection>
</Card>
`;

View File

@@ -0,0 +1,43 @@
import { shallow } from "enzyme";
import * as sinon from "sinon";
import React from "react";
import { CodeOfConductComponent, CodeOfConductComponentProps } from "./CodeOfConductComponent";
import { IJunoResponse, JunoClient } from "../../../Juno/JunoClient";
import { HttpStatusCodes } from "../../../Common/Constants";
describe("CodeOfConductComponent", () => {
let sandbox: sinon.SinonSandbox;
let codeOfConductProps: CodeOfConductComponentProps;
beforeEach(() => {
sandbox = sinon.sandbox.create();
sandbox.stub(JunoClient.prototype, "acceptCodeOfConduct").returns({
status: HttpStatusCodes.OK,
data: true
} as IJunoResponse<boolean>);
const junoClient = new JunoClient(undefined);
codeOfConductProps = {
junoClient: junoClient,
onAcceptCodeOfConduct: jest.fn()
};
});
afterEach(() => {
sandbox.restore();
});
it("renders", () => {
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />);
expect(wrapper).toMatchSnapshot();
});
it("onAcceptedCodeOfConductCalled", async () => {
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />);
wrapper
.find(".genericPaneSubmitBtn")
.first()
.simulate("click");
await Promise.resolve();
expect(codeOfConductProps.onAcceptCodeOfConduct).toBeCalled();
});
});

View File

@@ -0,0 +1,112 @@
import * as React from "react";
import { JunoClient } from "../../../Juno/JunoClient";
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
import * as Logger from "../../../Common/Logger";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react";
export interface CodeOfConductComponentProps {
junoClient: JunoClient;
onAcceptCodeOfConduct: (result: boolean) => void;
}
interface CodeOfConductComponentState {
readCodeOfConduct: boolean;
}
export class CodeOfConductComponent extends React.Component<CodeOfConductComponentProps, CodeOfConductComponentState> {
private descriptionPara1: string;
private descriptionPara2: string;
private descriptionPara3: string;
private link1: { label: string; url: string };
private link2: { label: string; url: string };
constructor(props: CodeOfConductComponentProps) {
super(props);
this.state = {
readCodeOfConduct: false
};
this.descriptionPara1 = "Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement";
this.descriptionPara2 =
"Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.";
this.descriptionPara3 = "In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the ";
this.link1 = { label: "code of conduct", url: CodeOfConductEndpoints.codeOfConduct };
this.link2 = { label: "privacy statement", url: CodeOfConductEndpoints.privacyStatement };
}
private async acceptCodeOfConduct(): Promise<void> {
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`);
}
this.props.onAcceptCodeOfConduct(response.data);
} catch (error) {
const message = `Failed to accept code of conduct: ${error}`;
Logger.logError(message, "CodeOfConductComponent/acceptCodeOfConduct");
logConsoleError(message);
}
}
private onChangeCheckbox = (): void => {
this.setState({ readCodeOfConduct: !this.state.readCodeOfConduct });
};
public render(): JSX.Element {
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>
{" and "}
<Link href={this.link2.url} target="_blank">
{this.link2.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 accepted the code of conduct and privacy statement"
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

@@ -0,0 +1,114 @@
import * as React from "react";
import { JunoClient, IGalleryItem } from "../../../Juno/JunoClient";
import { GalleryTab, SortBy, GalleryViewerComponentProps, GalleryViewerComponent } from "./GalleryViewerComponent";
import { NotebookViewerComponentProps, NotebookViewerComponent } from "../NotebookViewer/NotebookViewerComponent";
import * as GalleryUtils from "../../../Utils/GalleryUtils";
import Explorer from "../../Explorer";
export interface GalleryAndNotebookViewerComponentProps {
container?: Explorer;
junoClient: JunoClient;
notebookUrl?: string;
galleryItem?: IGalleryItem;
isFavorite?: boolean;
selectedTab: GalleryTab;
sortBy: SortBy;
searchText: string;
}
interface GalleryAndNotebookViewerComponentState {
notebookUrl: string;
galleryItem: IGalleryItem;
isFavorite: boolean;
selectedTab: GalleryTab;
sortBy: SortBy;
searchText: string;
}
export class GalleryAndNotebookViewerComponent extends React.Component<
GalleryAndNotebookViewerComponentProps,
GalleryAndNotebookViewerComponentState
> {
constructor(props: GalleryAndNotebookViewerComponentProps) {
super(props);
this.state = {
notebookUrl: props.notebookUrl,
galleryItem: props.galleryItem,
isFavorite: props.isFavorite,
selectedTab: props.selectedTab,
sortBy: props.sortBy,
searchText: props.searchText
};
}
public render(): JSX.Element {
if (this.state.notebookUrl) {
const props: NotebookViewerComponentProps = {
container: this.props.container,
junoClient: this.props.junoClient,
notebookUrl: this.state.notebookUrl,
galleryItem: this.state.galleryItem,
isFavorite: this.state.isFavorite,
backNavigationText: GalleryUtils.getTabTitle(this.state.selectedTab),
onBackClick: this.onBackClick,
onTagClick: this.loadTaggedItems
};
return <NotebookViewerComponent {...props} />;
}
const props: GalleryViewerComponentProps = {
container: this.props.container,
junoClient: this.props.junoClient,
selectedTab: this.state.selectedTab,
sortBy: this.state.sortBy,
searchText: this.state.searchText,
openNotebook: this.openNotebook,
onSelectedTabChange: this.onSelectedTabChange,
onSortByChange: this.onSortByChange,
onSearchTextChange: this.onSearchTextChange
};
return <GalleryViewerComponent {...props} />;
}
private onBackClick = (): void => {
this.setState({
notebookUrl: undefined
});
};
private loadTaggedItems = (tag: string): void => {
this.setState({
notebookUrl: undefined,
searchText: tag
});
};
private openNotebook = (data: IGalleryItem, isFavorite: boolean): void => {
this.setState({
notebookUrl: this.props.junoClient.getNotebookContentUrl(data.id),
galleryItem: data,
isFavorite
});
};
private onSelectedTabChange = (selectedTab: GalleryTab): void => {
this.setState({
selectedTab
});
};
private onSortByChange = (sortBy: SortBy): void => {
this.setState({
sortBy
});
};
private onSearchTextChange = (searchText: string): void => {
this.setState({
searchText
});
};
}

View File

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

View File

@@ -9,6 +9,7 @@ describe("GalleryViewerComponent", () => {
selectedTab: GalleryTab.OfficialSamples,
sortBy: SortBy.MostViewed,
searchText: undefined,
openNotebook: undefined,
onSelectedTabChange: undefined,
onSortByChange: undefined,
onSearchTextChange: undefined

View File

@@ -15,22 +15,25 @@ import {
} from "office-ui-fabric-react";
import * as React from "react";
import * as Logger from "../../../Common/Logger";
import * as ViewModels from "../../../Contracts/ViewModels";
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/GalleryCardComponent";
import "./GalleryViewerComponent.less";
import { HttpStatusCodes } from "../../../Common/Constants";
import Explorer from "../../Explorer";
import { CodeOfConductComponent } from "./CodeOfConductComponent";
import { InfoComponent } from "./InfoComponent/InfoComponent";
export interface GalleryViewerComponentProps {
container?: ViewModels.Explorer;
container?: Explorer;
junoClient: JunoClient;
selectedTab: GalleryTab;
sortBy: SortBy;
searchText: string;
openNotebook: (data: IGalleryItem, isFavorite: boolean) => void;
onSelectedTabChange: (newTab: GalleryTab) => void;
onSortByChange: (sortBy: SortBy) => void;
onSearchTextChange: (searchText: string) => void;
@@ -59,6 +62,7 @@ interface GalleryViewerComponentState {
sortBy: SortBy;
searchText: string;
dialogProps: DialogProps;
isCodeOfConductAccepted: boolean;
}
interface GalleryTabInfo {
@@ -66,13 +70,14 @@ interface GalleryTabInfo {
content: JSX.Element;
}
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps, GalleryViewerComponentState>
implements GalleryUtils.DialogEnabledComponent {
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps, GalleryViewerComponentState> {
public static readonly OfficialSamplesTitle = "Official samples";
public static readonly PublicGalleryTitle = "Public gallery";
public static readonly FavoritesTitle = "Liked";
public static readonly PublishedTitle = "Your published work";
private static readonly rowsPerPage = 5;
private static readonly mostViewedText = "Most viewed";
private static readonly mostDownloadedText = "Most downloaded";
private static readonly mostFavoritedText = "Most liked";
@@ -84,6 +89,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private publicNotebooks: IGalleryItem[];
private favoriteNotebooks: IGalleryItem[];
private publishedNotebooks: IGalleryItem[];
private isCodeOfConductAccepted: boolean;
private columnCount: number;
private rowCount: number;
@@ -98,7 +104,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
selectedTab: props.selectedTab,
sortBy: props.sortBy,
searchText: props.searchText,
dialogProps: undefined
dialogProps: undefined,
isCodeOfConductAccepted: undefined
};
this.sortingOptions = [
@@ -128,17 +135,24 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}
}
setDialogProps = (dialogProps: DialogProps): void => {
this.setState({ dialogProps });
};
public render(): JSX.Element {
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
if (this.props.container?.isGalleryPublishEnabled()) {
tabs.push(this.createTab(GalleryTab.PublicGallery, this.state.publicNotebooks));
tabs.push(
this.createPublicGalleryTab(
GalleryTab.PublicGallery,
this.state.publicNotebooks,
this.state.isCodeOfConductAccepted
)
);
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks));
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
// Displaying code of conduct component on gallery load should not be the default behavior.
if (this.state.isCodeOfConductAccepted !== false) {
tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks));
}
}
const pivotProps: IPivotProps = {
@@ -169,6 +183,17 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
);
}
private createPublicGalleryTab(
tab: GalleryTab,
data: IGalleryItem[],
acceptedCodeOfConduct: boolean
): GalleryTabInfo {
return {
tab,
content: this.createPublicGalleryTabContent(data, acceptedCodeOfConduct)
};
}
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
return {
tab,
@@ -176,10 +201,23 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
};
}
private createPublicGalleryTabContent(data: IGalleryItem[], acceptedCodeOfConduct: boolean): JSX.Element {
return acceptedCodeOfConduct === false ? (
<CodeOfConductComponent
junoClient={this.props.junoClient}
onAcceptCodeOfConduct={(result: boolean) => {
this.setState({ isCodeOfConductAccepted: result });
}}
/>
) : (
this.createTabContent(data)
);
}
private createTabContent(data: IGalleryItem[]): JSX.Element {
return (
<Stack tokens={{ childrenGap: 20 }}>
<Stack horizontal tokens={{ childrenGap: 20 }}>
<Stack tokens={{ childrenGap: 10 }}>
<Stack horizontal tokens={{ childrenGap: 20, padding: 10 }}>
<Stack.Item grow>
<SearchBox value={this.state.searchText} placeholder="Search" onChange={this.onSearchBoxChange} />
</Stack.Item>
@@ -189,8 +227,12 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
<Stack.Item styles={{ root: { minWidth: 200 } }}>
<Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
</Stack.Item>
{this.props.container?.isGalleryPublishEnabled() && (
<Stack.Item>
<InfoComponent />
</Stack.Item>
)}
</Stack>
{data && this.createCardsTabContent(data)}
</Stack>
);
@@ -256,12 +298,19 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
if (!offline) {
try {
const response = await this.props.junoClient.getPublicNotebooks();
let response: IJunoResponse<IPublicGalleryData> | IJunoResponse<IGalleryItem[]>;
if (this.props.container.isCodeOfConductEnabled()) {
response = await this.props.junoClient.fetchPublicNotebooks();
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
this.publicNotebooks = response.data?.notebooksData;
} else {
response = await this.props.junoClient.getPublicNotebooks();
this.publicNotebooks = response.data;
}
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
}
this.publicNotebooks = response.data;
} catch (error) {
const message = `Failed to load public notebooks: ${error}`;
Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks");
@@ -270,7 +319,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}
this.setState({
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))]
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))],
isCodeOfConductAccepted: this.isCodeOfConductAccepted
});
}
@@ -335,12 +385,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private isGalleryItemPresent(searchText: string, item: IGalleryItem): boolean {
const toSearch = searchText.trim().toUpperCase();
const searchData: string[] = [
item.author.toUpperCase(),
item.description.toUpperCase(),
item.name.toUpperCase(),
...item.tags?.map(tag => tag.toUpperCase())
];
const searchData: string[] = [item.author.toUpperCase(), item.description.toUpperCase(), item.name.toUpperCase()];
if (item.tags) {
searchData.push(...item.tags.map(tag => tag.toUpperCase()));
}
for (const data of searchData) {
if (data?.indexOf(toSearch) !== -1) {
@@ -389,8 +438,10 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}
private getPageSpecification = (itemIndex?: number, visibleRect?: IRectangle): IPageSpecification => {
this.columnCount = Math.floor(visibleRect.width / GalleryCardComponent.CARD_WIDTH);
this.rowCount = Math.floor(visibleRect.height / GalleryCardComponent.CARD_HEIGHT);
if (itemIndex === 0) {
this.columnCount = Math.floor(visibleRect.width / GalleryCardComponent.CARD_WIDTH) || this.columnCount;
this.rowCount = GalleryViewerComponent.rowsPerPage;
}
return {
height: visibleRect.height,
@@ -406,8 +457,9 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
const props: GalleryCardComponentProps = {
data,
isFavorite,
showDownload: !!this.props.container,
showDelete: this.state.selectedTab === GalleryTab.Published,
onClick: () => this.openNotebook(data, isFavorite),
onClick: () => this.props.openNotebook(data, isFavorite),
onTagClick: this.loadTaggedItems,
onFavoriteClick: () => this.favoriteItem(data),
onUnfavoriteClick: () => this.unfavoriteItem(data),
@@ -422,20 +474,6 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
);
};
private openNotebook = (data: IGalleryItem, isFavorite: boolean): void => {
if (this.props.container && this.props.junoClient) {
this.props.container.openGallery(this.props.junoClient.getNotebookContentUrl(data.id), data, isFavorite);
} else {
const params = new URLSearchParams({
[GalleryUtils.NotebookViewerParams.NotebookUrl]: this.props.junoClient.getNotebookContentUrl(data.id),
[GalleryUtils.NotebookViewerParams.GalleryItemId]: data.id
});
const location = new URL("./notebookViewer.html", window.location.href).href;
window.open(`${location}?${params.toString()}`);
}
};
private loadTaggedItems = (tag: string): void => {
const searchText = tag;
this.setState({
@@ -465,9 +503,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
};
private downloadItem = async (data: IGalleryItem): Promise<void> => {
GalleryUtils.downloadItem(this, this.props.container, this.props.junoClient, data, item =>
this.refreshSelectedTab(item)
);
GalleryUtils.downloadItem(this.props.container, this.props.junoClient, data, item => this.refreshSelectedTab(item));
};
private deleteItem = async (data: IGalleryItem): Promise<void> => {

View File

@@ -0,0 +1,26 @@
@import "../../../../../less/Common/Constants.less";
.infoPanel, .infoPanelMain {
display: flex;
align-items: center;
}
.infoPanel {
padding-left: 5px;
padding-right: 5px;
}
.infoLabel, .infoLabelMain {
padding-left: 5px
}
.infoLabel {
font-weight: 400
}
.infoIconMain {
color: @AccentMedium
}
.infoIconMain:hover {
color: @BaseMedium
}

View File

@@ -0,0 +1,10 @@
import { shallow } from "enzyme";
import React from "react";
import { InfoComponent } from "./InfoComponent";
describe("InfoComponent", () => {
it("renders", () => {
const wrapper = shallow(<InfoComponent />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,42 @@
import * as React from "react";
import { Icon, Label, Stack, HoverCard, HoverCardType, Link } from "office-ui-fabric-react";
import { CodeOfConductEndpoints } from "../../../../Common/Constants";
import "./InfoComponent.less";
export class InfoComponent extends React.Component {
private getInfoPanel = (iconName: string, labelText: string, url: string): JSX.Element => {
return (
<Link href={url} target="_blank">
<div className="infoPanel">
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} />
<Label className="infoLabel">{labelText}</Label>
</div>
</Link>
);
};
private onHover = (): JSX.Element => {
return (
<Stack tokens={{ childrenGap: 5, padding: 5 }}>
<Stack.Item>{this.getInfoPanel("Script", "Code of Conduct", CodeOfConductEndpoints.codeOfConduct)}</Stack.Item>
<Stack.Item>
{this.getInfoPanel("RedEye", "Privacy Statement", CodeOfConductEndpoints.privacyStatement)}
</Stack.Item>
<Stack.Item>
{this.getInfoPanel("KnowledgeArticle", "Microsoft Terms of Use", CodeOfConductEndpoints.termsOfUse)}
</Stack.Item>
</Stack>
);
};
public render(): JSX.Element {
return (
<HoverCard plainCardProps={{ onRenderPlainCard: this.onHover }} instantOpenOnClick type={HoverCardType.plain}>
<div className="infoPanelMain">
<Icon className="infoIconMain" iconName="Help" styles={{ root: { verticalAlign: "middle" } }} />
<Label className="infoLabelMain">Help</Label>
</div>
</HoverCard>
);
}
}

View File

@@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InfoComponent renders 1`] = `
<StyledHoverCardBase
instantOpenOnClick={true}
plainCardProps={
Object {
"onRenderPlainCard": [Function],
}
}
type="PlainCard"
>
<div
className="infoPanelMain"
>
<Memo(StyledIconBase)
className="infoIconMain"
iconName="Help"
styles={
Object {
"root": Object {
"verticalAlign": "middle",
},
}
}
/>
<StyledLabelBase
className="infoLabelMain"
>
Help
</StyledLabelBase>
</div>
</StyledHoverCardBase>
`;

View File

@@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CodeOfConductComponent renders 1`] = `
<Stack
tokens={
Object {
"childrenGap": 20,
}
}
>
<StackItem>
<Text
style={
Object {
"fontSize": "20px",
"fontWeight": 500,
}
}
>
Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement
</Text>
</StackItem>
<StackItem>
<Text>
Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.
</Text>
</StackItem>
<StackItem>
<Text>
In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the
<StyledLinkBase
href="https://aka.ms/cosmos-code-of-conduct"
target="_blank"
>
code of conduct
</StyledLinkBase>
and
<StyledLinkBase
href="https://aka.ms/ms-privacy-policy"
target="_blank"
>
privacy statement
</StyledLinkBase>
</Text>
</StackItem>
<StackItem>
<StyledCheckboxBase
label="I have read and accepted the code of conduct and privacy statement"
onChange={[Function]}
styles={
Object {
"label": Object {
"margin": 0,
"padding": "2 0 2 0",
},
"text": Object {
"fontSize": 12,
},
}
}
/>
</StackItem>
<StackItem>
<CustomizedPrimaryButton
ariaLabel="Continue"
className="genericPaneSubmitBtn"
disabled={true}
onClick={[Function]}
tabIndex={0}
text="Continue"
title="Continue"
/>
</StackItem>
</Stack>
`;

View File

@@ -21,7 +21,7 @@ exports[`GalleryViewerComponent renders 1`] = `
<Stack
tokens={
Object {
"childrenGap": 20,
"childrenGap": 10,
}
}
>
@@ -30,6 +30,7 @@ exports[`GalleryViewerComponent renders 1`] = `
tokens={
Object {
"childrenGap": 20,
"padding": 10,
}
}
>

View File

@@ -17,7 +17,8 @@ describe("NotebookMetadataComponent", () => {
isSample: false,
downloads: 0,
favorites: 0,
views: 0
views: 0,
newCellId: undefined
},
isFavorite: false,
downloadButtonText: "Download",
@@ -45,7 +46,8 @@ describe("NotebookMetadataComponent", () => {
isSample: false,
downloads: 0,
favorites: 0,
views: 0
views: 0,
newCellId: undefined
},
isFavorite: true,
downloadButtonText: "Download",

View File

@@ -16,11 +16,12 @@ import * as React from "react";
import { IGalleryItem } from "../../../Juno/JunoClient";
import { FileSystemUtil } from "../../Notebook/FileSystemUtil";
import "./NotebookViewerComponent.less";
import CosmosDBLogo from "../../../../images/CosmosDB-logo.svg";
export interface NotebookMetadataComponentProps {
data: IGalleryItem;
isFavorite: boolean;
downloadButtonText: string;
downloadButtonText?: string;
onTagClick: (tag: string) => void;
onFavoriteClick: () => void;
onUnfavoriteClick: () => void;
@@ -54,11 +55,18 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
</>
)}
</Text>
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
{this.props.downloadButtonText && (
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
)}
</Stack>
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 10 }}>
<Persona text={this.props.data.author} size={PersonaSize.size32} />
<Persona
imageUrl={this.props.data.isSample && CosmosDBLogo}
text={this.props.data.author}
size={PersonaSize.size32}
/>
<Text>{dateString}</Text>
<Text>
<Icon iconName="RedEye" /> {this.props.data.views}

View File

@@ -6,21 +6,3 @@
width: 100%;
overflow-y: auto;
}
.downloadButton {
background-color: #0078D4;
color: @BaseLight;
cursor: pointer;
padding: 5px;
border: none;
text-align: left;
outline: none;
font-size: @mediumFontSize;
border-radius: 5px;
display: "inline-block";
margin: 10px;
}
.active, .downloadButton:hover {
color: @BaseMedium;
}

View File

@@ -3,14 +3,14 @@
*/
import { Notebook } from "@nteract/commutable";
import { createContentRef } from "@nteract/core";
import { Icon, Link } from "office-ui-fabric-react";
import { Icon, Link, ProgressIndicator } from "office-ui-fabric-react";
import * as React from "react";
import { contents } from "rx-jupyter";
import * as Logger from "../../../Common/Logger";
import * as ViewModels from "../../../Contracts/ViewModels";
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
import * as GalleryUtils from "../../../Utils/GalleryUtils";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
@@ -18,14 +18,18 @@ import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookRe
import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComponent";
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
import "./NotebookViewerComponent.less";
import Explorer from "../../Explorer";
import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
export interface NotebookViewerComponentProps {
container?: ViewModels.Explorer;
container?: Explorer;
junoClient?: JunoClient;
notebookUrl: string;
galleryItem?: IGalleryItem;
isFavorite?: boolean;
backNavigationText: string;
hideInputs?: boolean;
onBackClick: () => void;
onTagClick: (tag: string) => void;
}
@@ -35,10 +39,13 @@ interface NotebookViewerComponentState {
galleryItem?: IGalleryItem;
isFavorite?: boolean;
dialogProps: DialogProps;
showProgressBar: boolean;
}
export class NotebookViewerComponent extends React.Component<NotebookViewerComponentProps, NotebookViewerComponentState>
implements GalleryUtils.DialogEnabledComponent {
export class NotebookViewerComponent extends React.Component<
NotebookViewerComponentProps,
NotebookViewerComponentState
> {
private clientManager: NotebookClientV2;
private notebookComponentBootstrapper: NotebookComponentBootstrapper;
@@ -64,46 +71,57 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
content: undefined,
galleryItem: props.galleryItem,
isFavorite: props.isFavorite,
dialogProps: undefined
dialogProps: undefined,
showProgressBar: true
};
this.loadNotebookContent();
}
setDialogProps = (dialogProps: DialogProps): void => {
this.setState({ dialogProps });
};
private async loadNotebookContent(): Promise<void> {
try {
const response = await fetch(this.props.notebookUrl);
if (!response.ok) {
this.setState({ showProgressBar: false });
throw new Error(`Received HTTP ${response.status} while fetching ${this.props.notebookUrl}`);
}
const notebook: Notebook = await response.json();
this.removeNotebookViewerLink(notebook, this.props.galleryItem?.newCellId);
this.notebookComponentBootstrapper.setContent("json", notebook);
this.setState({ content: notebook });
this.setState({ content: notebook, showProgressBar: false });
if (this.props.galleryItem) {
if (this.props.galleryItem && !SessionStorageUtility.getEntry(this.props.galleryItem.id)) {
const response = await this.props.junoClient.increaseNotebookViews(this.props.galleryItem.id);
if (!response.data) {
throw new Error(`Received HTTP ${response.status} while increasing notebook views`);
}
this.setState({ galleryItem: response.data });
SessionStorageUtility.setEntry(this.props.galleryItem?.id, "true");
}
} catch (error) {
this.setState({ showProgressBar: false });
const message = `Failed to load notebook content: ${error}`;
Logger.logError(message, "NotebookViewerComponent/loadNotebookContent");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
}
}
private removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => {
if (!newCellId) {
return;
}
const notebookV4 = notebook as NotebookV4;
if (notebookV4 && notebookV4.cells[0].source[0].search(newCellId)) {
delete notebookV4.cells[0];
notebook = notebookV4;
}
};
public render(): JSX.Element {
return (
<div className="notebookViewerContainer">
{this.props.backNavigationText ? (
{this.props.backNavigationText !== undefined ? (
<Link onClick={this.props.onBackClick}>
<Icon iconName="Back" /> {this.props.backNavigationText}
</Link>
@@ -116,9 +134,7 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
<NotebookMetadataComponent
data={this.state.galleryItem}
isFavorite={this.state.isFavorite}
downloadButtonText={
this.props.container ? "Download to my notebooks" : "Edit/Run in Cosmos DB data explorer"
}
downloadButtonText={this.props.container && "Download to my notebooks"}
onTagClick={this.props.onTagClick}
onFavoriteClick={this.favoriteItem}
onUnfavoriteClick={this.unfavoriteItem}
@@ -129,7 +145,11 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
<></>
)}
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, { hideInputs: false })}
{this.state.showProgressBar && <ProgressIndicator />}
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
hideInputs: this.props.hideInputs
})}
{this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />}
</div>
@@ -170,7 +190,7 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
};
private downloadItem = async (): Promise<void> => {
GalleryUtils.downloadItem(this, this.props.container, this.props.junoClient, this.state.galleryItem, item =>
GalleryUtils.downloadItem(this.props.container, this.props.junoClient, this.state.galleryItem, item =>
this.setState({ galleryItem: item })
);
};

View File

@@ -48,6 +48,7 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
verticalAlign="center"
>
<StyledPersonaBase
imageUrl={false}
size={11}
text="author"
/>
@@ -55,14 +56,14 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
Invalid Date
</Text>
<Text>
<StyledIconBase
<Memo(StyledIconBase)
iconName="RedEye"
/>
0
</Text>
<Text>
<StyledIconBase
<Memo(StyledIconBase)
iconName="Download"
/>
0
@@ -147,6 +148,7 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
verticalAlign="center"
>
<StyledPersonaBase
imageUrl={false}
size={11}
text="author"
/>
@@ -154,14 +156,14 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
Invalid Date
</Text>
<Text>
<StyledIconBase
<Memo(StyledIconBase)
iconName="RedEye"
/>
0
</Text>
<Text>
<StyledIconBase
<Memo(StyledIconBase)
iconName="Download"
/>
0

View File

@@ -27,9 +27,10 @@ import { TextField, ITextFieldProps, ITextField } from "office-ui-fabric-react/l
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
import { QueriesClient } from "../../../Common/QueriesClient";
export interface QueriesGridComponentProps {
queriesClient: ViewModels.QueriesClient;
queriesClient: QueriesClient;
onQuerySelect: (query: DataModels.Query) => void;
containerVisible: boolean;
saveQueryEnabled: boolean;
@@ -217,7 +218,7 @@ export class QueriesGridComponent extends React.Component<QueriesGridComponentPr
menuItem: any
) => {
if (window.confirm("Are you sure you want to delete this query?")) {
const container: ViewModels.Explorer = window.dataExplorer;
const container = window.dataExplorer;
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
databaseAccountName: container && container.databaseAccount().name,
defaultExperience: container && container.defaultExperience(),

View File

@@ -8,11 +8,12 @@ import * as React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { QueriesGridComponent, QueriesGridComponentProps } from "./QueriesGridComponent";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import Explorer from "../../Explorer";
export class QueriesGridComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
constructor(private container: ViewModels.Explorer) {
constructor(private container: Explorer) {
this.parameters = ko.observable<number>(Date.now());
}

View File

@@ -1,6 +1,6 @@
/* Utilities for validation */
export const onValidateValueChange = (newValue: string, minValue?: number, maxValue?: number): number => {
export const onValidateValueChange = (newValue: string, minValue?: number, maxValue?: number): number | undefined => {
let numericValue = parseInt(newValue);
if (!isNaN(numericValue) && isFinite(numericValue)) {
if (minValue !== undefined && numericValue < minValue) {
@@ -16,7 +16,7 @@ export const onValidateValueChange = (newValue: string, minValue?: number, maxVa
return undefined;
};
export const onIncrementValue = (newValue: string, step: number, max?: number): number => {
export const onIncrementValue = (newValue: string, step: number, max?: number): number | undefined => {
const numericValue = parseInt(newValue);
if (!isNaN(numericValue) && isFinite(numericValue)) {
const newValue = numericValue + step;
@@ -25,7 +25,7 @@ export const onIncrementValue = (newValue: string, step: number, max?: number):
return undefined;
};
export const onDecrementValue = (newValue: string, step: number, min?: number): number => {
export const onDecrementValue = (newValue: string, step: number, min?: number): number | undefined => {
const numericValue = parseInt(newValue);
if (!isNaN(numericValue) && isFinite(numericValue)) {
const newValue = numericValue - step;

View File

@@ -1,17 +0,0 @@
@import "../../../../less/Common/Constants";
.labelWithRedAsterisk {
line-height: 18px;
font-size: @DefaultFontSize;
font-family: @DataExplorerFont;
color: @DefaultFontColor;
}
.labelWithRedAsterisk::before {
content: "* ";
color: @SelectionHigh;
}
.clusterSettingsDropdown {
margin-bottom: 10px;
}

View File

@@ -1,159 +0,0 @@
import * as React from "react";
import { Dropdown, IDropdownOption, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
import { Slider, ISliderProps } from "office-ui-fabric-react/lib/Slider";
import { Stack, IStackItemStyles, IStackStyles } from "office-ui-fabric-react/lib/Stack";
import { TextField, ITextFieldProps } from "office-ui-fabric-react/lib/TextField";
import { Spark } from "../../../Common/Constants";
import { SparkCluster } from "../../../Contracts/DataModels";
export interface ClusterSettingsComponentProps {
cluster: SparkCluster;
onClusterSettingsChanged: (cluster: SparkCluster) => void;
}
export class ClusterSettingsComponent extends React.Component<ClusterSettingsComponentProps, {}> {
constructor(props: ClusterSettingsComponentProps) {
super(props);
}
public render(): JSX.Element {
return (
<>
{this.getMasterSizeDropdown()}
{this.getWorkerSizeDropdown()}
{this.getWorkerCountSliderInput()}
</>
);
}
private getMasterSizeDropdown(): JSX.Element {
const driverSize: string =
this.props.cluster && this.props.cluster.properties && this.props.cluster.properties.driverSize;
const masterSizeOptions: IDropdownOption[] = Spark.SKUs.keys().map(sku => ({
key: sku,
text: Spark.SKUs.get(sku)
}));
const masterSizeDropdownProps: IDropdownProps = {
label: "Master Size",
options: masterSizeOptions,
defaultSelectedKey: driverSize,
onChange: this._onDriverSizeChange,
styles: {
root: "clusterSettingsDropdown"
}
};
return <Dropdown {...masterSizeDropdownProps} />;
}
private getWorkerSizeDropdown(): JSX.Element {
const workerSize: string =
this.props.cluster && this.props.cluster.properties && this.props.cluster.properties.workerSize;
const workerSizeOptions: IDropdownOption[] = Spark.SKUs.keys().map(sku => ({
key: sku,
text: Spark.SKUs.get(sku)
}));
const workerSizeDropdownProps: IDropdownProps = {
label: "Worker Size",
options: workerSizeOptions,
defaultSelectedKey: workerSize,
onChange: this._onWorkerSizeChange,
styles: {
label: "labelWithRedAsterisk",
root: "clusterSettingsDropdown"
}
};
return <Dropdown {...workerSizeDropdownProps} />;
}
private getWorkerCountSliderInput(): JSX.Element {
const workerCount: number =
(this.props.cluster &&
this.props.cluster.properties &&
this.props.cluster.properties.workerInstanceCount !== undefined &&
this.props.cluster.properties.workerInstanceCount) ||
0;
const stackStyle: IStackStyles = {
root: {
paddingTop: 5
}
};
const sliderItemStyle: IStackItemStyles = {
root: {
width: "100%",
paddingRight: 20
}
};
const workerCountSliderProps: ISliderProps = {
min: 0,
max: Spark.MaxWorkerCount,
step: 1,
value: workerCount,
showValue: false,
onChange: this._onWorkerCountChange,
styles: {
root: {
width: "100%",
paddingRight: 20
}
}
};
const workerCountTextFieldProps: ITextFieldProps = {
value: workerCount.toString(),
styles: {
fieldGroup: {
width: 45,
height: 25
},
field: {
textAlign: "center"
}
},
onChange: this._onWorkerCountTextFieldChange
};
return (
<Stack styles={stackStyle}>
<span className="labelWithRedAsterisk">Worker Nodes</span>
<Stack horizontal verticalAlign="center">
<Slider {...workerCountSliderProps} />
<TextField {...workerCountTextFieldProps} />
</Stack>
</Stack>
);
}
private _onDriverSizeChange = (_event: React.FormEvent, selectedOption: IDropdownOption) => {
const newValue: string = selectedOption.key as string;
const cluster = this.props.cluster;
if (cluster) {
cluster.properties.driverSize = newValue;
this.props.onClusterSettingsChanged(cluster);
}
};
private _onWorkerSizeChange = (_event: React.FormEvent, selectedOption: IDropdownOption) => {
const newValue: string = selectedOption.key as string;
const cluster = this.props.cluster;
if (cluster) {
cluster.properties.workerSize = newValue;
this.props.onClusterSettingsChanged(cluster);
}
};
private _onWorkerCountChange = (count: number) => {
count = Math.min(count, Spark.MaxWorkerCount);
const cluster = this.props.cluster;
if (cluster) {
cluster.properties.workerInstanceCount = count;
this.props.onClusterSettingsChanged(cluster);
}
};
private _onWorkerCountTextFieldChange = (_event: React.FormEvent, newValue: string) => {
const count = parseInt(newValue);
if (!isNaN(count)) {
this._onWorkerCountChange(count);
}
};
}

View File

@@ -1,11 +0,0 @@
import * as React from "react";
import { ClusterSettingsComponent, ClusterSettingsComponentProps } from "./ClusterSettingsComponent";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
export class ClusterSettingsComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<ClusterSettingsComponentProps>;
public renderComponent(): JSX.Element {
return <ClusterSettingsComponent {...this.parameters()} />;
}
}

View File

@@ -1,12 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import IToolbarDisplayable from "./IToolbarDisplayable";
interface IToolbarAction extends IToolbarDisplayable {
type: "action";
action: () => void;
}
export default IToolbarAction;

View File

@@ -1,18 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
interface IToolbarDisplayable {
id: string;
title: ko.Subscribable<string>;
displayName: ko.Subscribable<string>;
enabled: ko.Subscribable<boolean>;
visible: ko.Observable<boolean>;
focused: ko.Observable<boolean>;
icon: string;
mouseDown: (data: any, event: MouseEvent) => any;
keyUp: (data: any, event: KeyboardEvent) => any;
keyDown: (data: any, event: KeyboardEvent) => any;
}
export default IToolbarDisplayable;

View File

@@ -1,56 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import IToolbarDisplayable from "./IToolbarDisplayable";
interface IToolbarDropDown extends IToolbarDisplayable {
type: "dropdown";
subgroup: IActionConfigItem[];
expanded: ko.Observable<boolean>;
open: () => void;
}
export interface IDropdown {
type: "dropdown";
title: string;
displayName: string;
id: string;
enabled: ko.Observable<boolean>;
visible?: ko.Observable<boolean>;
icon?: string;
subgroup?: IActionConfigItem[];
}
export interface ISeperator {
type: "separator";
visible?: ko.Observable<boolean>;
}
export interface IToggle {
type: "toggle";
title: string;
displayName: string;
checkedTitle: string;
checkedDisplayName: string;
id: string;
checked: ko.Observable<boolean>;
enabled: ko.Observable<boolean>;
visible?: ko.Observable<boolean>;
icon?: string;
}
export interface IAction {
type: "action";
title: string;
displayName: string;
id: string;
action: () => any;
enabled: ko.Subscribable<boolean>;
visible?: ko.Observable<boolean>;
icon?: string;
}
export type IActionConfigItem = ISeperator | IAction | IToggle | IDropdown;
export default IToolbarDropDown;

View File

@@ -1,12 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import IToolbarAction from "./IToolbarAction";
import IToolbarToggle from "./IToolbarToggle";
import IToolbarSeperator from "./IToolbarSeperator";
import IToolbarDropDown from "./IToolbarDropDown";
type IToolbarItem = IToolbarAction | IToolbarToggle | IToolbarSeperator | IToolbarDropDown;
export default IToolbarItem;

View File

@@ -1,10 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
interface IToolbarSeperator {
type: "separator";
visible: ko.Observable<boolean>;
}
export default IToolbarSeperator;

View File

@@ -1,12 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import IToolbarDisplayable from "./IToolbarDisplayable";
interface IToolbarToggle extends IToolbarDisplayable {
type: "toggle";
checked: ko.Observable<boolean>;
toggle: () => void;
}
export default IToolbarToggle;

View File

@@ -1,58 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
var keyCodes = {
RightClick: 3,
Enter: 13,
Esc: 27,
Tab: 9,
LeftArrow: 37,
UpArrow: 38,
RightArrow: 39,
DownArrow: 40,
Delete: 46,
A: 65,
B: 66,
C: 67,
D: 68,
E: 69,
F: 70,
G: 71,
H: 72,
I: 73,
J: 74,
K: 75,
L: 76,
M: 77,
N: 78,
O: 79,
P: 80,
Q: 81,
R: 82,
S: 83,
T: 84,
U: 85,
V: 86,
W: 87,
X: 88,
Y: 89,
Z: 90,
Period: 190,
DecimalPoint: 110,
F1: 112,
F2: 113,
F3: 114,
F4: 115,
F5: 116,
F6: 117,
F7: 118,
F8: 119,
F9: 120,
F10: 121,
F11: 122,
F12: 123,
Dash: 189
};
export default keyCodes;

View File

@@ -1,145 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import { IDropdown } from "./IToolbarDropDown";
import { IActionConfigItem } from "./IToolbarDropDown";
import IToolbarItem from "./IToolbarItem";
import * as ko from "knockout";
import ToolbarDropDown from "./ToolbarDropDown";
import ToolbarAction from "./ToolbarAction";
import ToolbarToggle from "./ToolbarToggle";
import template from "./toolbar.html";
export default class Toolbar {
private _toolbarWidth = ko.observable<number>();
private _actionConfigs: IActionConfigItem[];
private _afterExecute: (id: string) => void;
private _hasFocus: boolean = false;
private _focusedSubscription: ko.Subscription;
constructor(actionItems: IActionConfigItem[], afterExecute?: (id: string) => void) {
this._actionConfigs = actionItems;
this._afterExecute = afterExecute;
this.toolbarItems.subscribe(this._focusFirstEnabledItem);
$(window).resize(() => {
this._toolbarWidth($(".toolbar").width());
});
setTimeout(() => {
this._toolbarWidth($(".toolbar").width());
}, 500);
}
public toolbarItems: ko.PureComputed<IToolbarItem[]> = ko.pureComputed(() => {
var remainingToolbarSpace = this._toolbarWidth();
var toolbarItems: IToolbarItem[] = [];
var moreItem: IDropdown = {
type: "dropdown",
title: "More",
displayName: "More",
id: "more-actions-toggle",
enabled: ko.observable(true),
visible: ko.observable(true),
icon: "images/ASX_More.svg",
subgroup: []
};
var showHasMoreItem = false;
var addSeparator = false;
this._actionConfigs.forEach(actionConfig => {
if (actionConfig.type === "separator") {
addSeparator = true;
} else if (remainingToolbarSpace / 60 > 2) {
if (addSeparator) {
addSeparator = false;
toolbarItems.push(Toolbar._createToolbarItemFromConfig({ type: "separator" }));
remainingToolbarSpace -= 10;
}
toolbarItems.push(Toolbar._createToolbarItemFromConfig(actionConfig));
remainingToolbarSpace -= 60;
} else {
showHasMoreItem = true;
if (addSeparator) {
addSeparator = false;
moreItem.subgroup.push({
type: "separator"
});
}
if (!!actionConfig) {
moreItem.subgroup.push(actionConfig);
}
}
});
if (showHasMoreItem) {
toolbarItems.push(
Toolbar._createToolbarItemFromConfig({ type: "separator" }),
Toolbar._createToolbarItemFromConfig(moreItem)
);
}
return toolbarItems;
});
public focus() {
this._hasFocus = true;
this._focusFirstEnabledItem(this.toolbarItems());
}
private _focusFirstEnabledItem = (items: IToolbarItem[]) => {
if (!!this._focusedSubscription) {
// no memory leaks! :D
this._focusedSubscription.dispose();
}
if (this._hasFocus) {
for (var i = 0; i < items.length; i++) {
if (items[i].type !== "separator" && (<any>items[i]).enabled()) {
(<any>items[i]).focused(true);
this._focusedSubscription = (<any>items[i]).focused.subscribe((newValue: any) => {
if (!newValue) {
this._hasFocus = false;
this._focusedSubscription.dispose();
}
});
break;
}
}
}
};
private static _createToolbarItemFromConfig(
configItem: IActionConfigItem,
afterExecute?: (id: string) => void
): IToolbarItem {
switch (configItem.type) {
case "dropdown":
return new ToolbarDropDown(configItem, afterExecute);
case "action":
return new ToolbarAction(configItem, afterExecute);
case "toggle":
return new ToolbarToggle(configItem, afterExecute);
case "separator":
return {
type: "separator",
visible: ko.observable(true)
};
}
}
}
/**
* Helper class for ko component registration
*/
export class ToolbarComponent {
constructor() {
return {
viewModel: Toolbar,
template
};
}
}

View File

@@ -1,86 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import * as ko from "knockout";
import { IAction } from "./IToolbarDropDown";
import IToolbarAction from "./IToolbarAction";
import KeyCodes from "./KeyCodes";
import Utilities from "./Utilities";
export default class ToolbarAction implements IToolbarAction {
public type: "action" = "action";
public id: string;
public icon: string;
public title: ko.Observable<string>;
public displayName: ko.Observable<string>;
public enabled: ko.Subscribable<boolean>;
public visible: ko.Observable<boolean>;
public focused: ko.Observable<boolean>;
public action: () => void;
private _afterExecute: (id: string) => void;
constructor(actionItem: IAction, afterExecute?: (id: string) => void) {
this.action = actionItem.action;
this.title = ko.observable(actionItem.title);
this.displayName = ko.observable(actionItem.displayName);
this.id = actionItem.id;
this.enabled = actionItem.enabled;
this.visible = actionItem.visible ? actionItem.visible : ko.observable(true);
this.focused = ko.observable(false);
this.icon = actionItem.icon;
this._afterExecute = afterExecute;
}
private _executeAction = () => {
this.action();
if (!!this._afterExecute) {
this._afterExecute(this.id);
}
};
public mouseDown = (data: any, event: MouseEvent): boolean => {
this._executeAction();
return false;
};
public keyUp = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
this._executeAction();
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
return !handled;
};
public keyDown = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
if (!handled) {
// Reset color if [shift-] tabbing, 'up/down arrowing', or 'esc'-aping away from button while holding down 'enter'
Utilities.onKeys(
event,
[KeyCodes.Tab, KeyCodes.UpArrow, KeyCodes.DownArrow, KeyCodes.Esc],
($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
}
);
}
return !handled;
};
}

View File

@@ -1,167 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import * as ko from "knockout";
import { IDropdown } from "./IToolbarDropDown";
import { IActionConfigItem } from "./IToolbarDropDown";
import IToolbarDropDown from "./IToolbarDropDown";
import KeyCodes from "./KeyCodes";
import Utilities from "./Utilities";
interface IMenuItem {
id?: string;
type: "normal" | "separator" | "submenu";
label?: string;
enabled?: boolean;
visible?: boolean;
submenu?: IMenuItem[];
}
export default class ToolbarDropDown implements IToolbarDropDown {
public type: "dropdown" = "dropdown";
public title: ko.Observable<string>;
public displayName: ko.Observable<string>;
public id: string;
public enabled: ko.Observable<boolean>;
public visible: ko.Observable<boolean>;
public focused: ko.Observable<boolean>;
public icon: string;
public subgroup: IActionConfigItem[] = [];
public expanded: ko.Observable<boolean> = ko.observable(false);
private _afterExecute: (id: string) => void;
constructor(dropdown: IDropdown, afterExecute?: (id: string) => void) {
this.subgroup = dropdown.subgroup;
this.title = ko.observable(dropdown.title);
this.displayName = ko.observable(dropdown.displayName);
this.id = dropdown.id;
this.enabled = dropdown.enabled;
this.visible = dropdown.visible ? dropdown.visible : ko.observable(true);
this.focused = ko.observable(false);
this.icon = dropdown.icon;
this._afterExecute = afterExecute;
}
private static _convertToMenuItem = (
actionConfigs: IActionConfigItem[],
actionMap: { [id: string]: () => void } = {}
): { menuItems: IMenuItem[]; actionMap: { [id: string]: () => void } } => {
var returnValue = {
menuItems: actionConfigs.map<IMenuItem>((actionConfig: IActionConfigItem, index, array) => {
var menuItem: IMenuItem;
switch (actionConfig.type) {
case "action":
menuItem = <IMenuItem>{
id: actionConfig.id,
type: "normal",
label: actionConfig.displayName,
enabled: actionConfig.enabled(),
visible: actionConfig.visible ? actionConfig.visible() : true
};
actionMap[actionConfig.id] = actionConfig.action;
break;
case "dropdown":
menuItem = <IMenuItem>{
id: actionConfig.id,
type: "submenu",
label: actionConfig.displayName,
enabled: actionConfig.enabled(),
visible: actionConfig.visible ? actionConfig.visible() : true,
submenu: ToolbarDropDown._convertToMenuItem(actionConfig.subgroup, actionMap).menuItems
};
break;
case "toggle":
menuItem = <IMenuItem>{
id: actionConfig.id,
type: "normal",
label: actionConfig.checked() ? actionConfig.checkedDisplayName : actionConfig.displayName,
enabled: actionConfig.enabled(),
visible: actionConfig.visible ? actionConfig.visible() : true
};
actionMap[actionConfig.id] = () => {
actionConfig.checked(!actionConfig.checked());
};
break;
case "separator":
menuItem = <IMenuItem>{
type: "separator",
visible: true
};
break;
}
return menuItem;
}),
actionMap: actionMap
};
return returnValue;
};
public open = () => {
if (!!(<any>window).host) {
var convertedMenuItem = ToolbarDropDown._convertToMenuItem(this.subgroup);
(<any>window).host
.executeProviderOperation("MenuManager.showMenu", {
iFrameStack: [`#${window.frameElement.id}`],
anchor: `#${this.id}`,
menuItems: convertedMenuItem.menuItems
})
.then((id?: string) => {
if (!!id && !!convertedMenuItem.actionMap[id]) {
convertedMenuItem.actionMap[id]();
}
});
if (!!this._afterExecute) {
this._afterExecute(this.id);
}
}
};
public mouseDown = (data: any, event: MouseEvent): boolean => {
this.open();
return false;
};
public keyUp = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
this.open();
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
return !handled;
};
public keyDown = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
if (!handled) {
// Reset color if [shift-] tabbing, 'up/down arrowing', or 'esc'-aping away from button while holding down 'enter'
Utilities.onKeys(
event,
[KeyCodes.Tab, KeyCodes.UpArrow, KeyCodes.DownArrow, KeyCodes.Esc],
($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
}
);
}
return !handled;
};
}

View File

@@ -1,109 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import * as ko from "knockout";
import { IToggle } from "./IToolbarDropDown";
import IToolbarToggle from "./IToolbarToggle";
import KeyCodes from "./KeyCodes";
import Utilities from "./Utilities";
export default class ToolbarToggle implements IToolbarToggle {
public type: "toggle" = "toggle";
public checked: ko.Observable<boolean>;
public id: string;
public enabled: ko.Observable<boolean>;
public visible: ko.Observable<boolean>;
public focused: ko.Observable<boolean>;
public icon: string;
private _title: string;
private _displayName: string;
private _checkedTitle: string;
private _checkedDisplayName: string;
private _afterExecute: (id: string) => void;
constructor(toggleItem: IToggle, afterExecute?: (id: string) => void) {
this._title = toggleItem.title;
this._displayName = toggleItem.displayName;
this.id = toggleItem.id;
this.enabled = toggleItem.enabled;
this.visible = toggleItem.visible ? toggleItem.visible : ko.observable(true);
this.focused = ko.observable(false);
this.icon = toggleItem.icon;
this.checked = toggleItem.checked;
this._checkedTitle = toggleItem.checkedTitle;
this._checkedDisplayName = toggleItem.checkedDisplayName;
this._afterExecute = afterExecute;
}
public title = ko.pureComputed(() => {
if (this.checked()) {
return this._checkedTitle;
} else {
return this._title;
}
});
public displayName = ko.pureComputed(() => {
if (this.checked()) {
return this._checkedDisplayName;
} else {
return this._displayName;
}
});
public toggle = () => {
this.checked(!this.checked());
if (this.checked() && !!this._afterExecute) {
this._afterExecute(this.id);
}
};
public mouseDown = (data: any, event: MouseEvent): boolean => {
this.toggle();
return false;
};
public keyUp = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
this.toggle();
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
return !handled;
};
public keyDown = (data: any, event: KeyboardEvent): boolean => {
var handled: boolean = false;
handled = Utilities.onEnter(event, ($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
return true;
});
if (!handled) {
// Reset color if [shift-] tabbing, 'up/down arrowing', or 'esc'-aping away from button while holding down 'enter'
Utilities.onKeys(
event,
[KeyCodes.Tab, KeyCodes.UpArrow, KeyCodes.DownArrow, KeyCodes.Esc],
($sourceElement: JQuery) => {
if ($sourceElement.hasClass("active")) {
$sourceElement.removeClass("active");
}
}
);
}
return !handled;
};
}

View File

@@ -1,166 +0,0 @@
/*!---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/
import KeyCodes from "./KeyCodes";
export default class Utilities {
/**
* Executes an action on a keyboard event.
* Modifiers: ctrlKey - control/command key, shiftKey - shift key, altKey - alt/option key;
* pass on 'null' to ignore the modifier (default).
*/
public static onKey(
event: any,
eventKeyCode: number,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
var source: any = event.target || event.srcElement,
keyCode: number = event.keyCode,
$sourceElement = $(source),
handled: boolean = false;
if (
$sourceElement.length &&
keyCode === eventKeyCode &&
$.isFunction(action) &&
(metaKey === null || metaKey === event.metaKey) &&
(shiftKey === null || shiftKey === event.shiftKey) &&
(altKey === null || altKey === event.altKey)
) {
action($sourceElement);
handled = true;
}
return handled;
}
/**
* Executes an action on the first matched keyboard event.
*/
public static onKeys(
event: any,
eventKeyCodes: number[],
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
var handled: boolean = false,
keyCount: number,
i: number;
if ($.isArray(eventKeyCodes)) {
keyCount = eventKeyCodes.length;
for (i = 0; i < keyCount; ++i) {
handled = Utilities.onKey(event, eventKeyCodes[i], action, metaKey, shiftKey, altKey);
if (handled) {
break;
}
}
}
return handled;
}
/**
* Executes an action on an 'enter' keyboard event.
*/
public static onEnter(
event: any,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
return Utilities.onKey(event, KeyCodes.Enter, action, metaKey, shiftKey, altKey);
}
/**
* Executes an action on a 'tab' keyboard event.
*/
public static onTab(
event: any,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
return Utilities.onKey(event, KeyCodes.Tab, action, metaKey, shiftKey, altKey);
}
/**
* Executes an action on an 'Esc' keyboard event.
*/
public static onEsc(
event: any,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
return Utilities.onKey(event, KeyCodes.Esc, action, metaKey, shiftKey, altKey);
}
/**
* Executes an action on an 'UpArrow' keyboard event.
*/
public static onUpArrow(
event: any,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
return Utilities.onKey(event, KeyCodes.UpArrow, action, metaKey, shiftKey, altKey);
}
/**
* Executes an action on a 'DownArrow' keyboard event.
*/
public static onDownArrow(
event: any,
action: ($sourceElement: JQuery) => void,
metaKey: boolean = null,
shiftKey: boolean = null,
altKey: boolean = null
): boolean {
return Utilities.onKey(event, KeyCodes.DownArrow, action, metaKey, shiftKey, altKey);
}
/**
* Executes an action on a mouse event.
*/
public static onButton(event: any, eventButtonCode: number, action: ($sourceElement: JQuery) => void): boolean {
var source: any = event.currentTarget;
var buttonCode: number = event.button;
var $sourceElement = $(source);
var handled: boolean = false;
if ($sourceElement.length && buttonCode === eventButtonCode && $.isFunction(action)) {
action($sourceElement);
handled = true;
}
return handled;
}
/**
* Executes an action on a 'left' mouse event.
*/
public static onLeftButton(event: any, action: ($sourceElement: JQuery) => void): boolean {
return Utilities.onButton(event, buttonCodes.Left, action);
}
}
var buttonCodes = {
None: -1,
Left: 0,
Middle: 1,
Right: 2
};

View File

@@ -1,44 +0,0 @@
<div class="toolbar">
<!-- ko template: { name: 'toolbarItemTemplate', foreach: toolbarItems } -->
<!-- /ko -->
</div>
<script type="text/html" id="toolbarItemTemplate">
<!-- ko if: type === "action" -->
<div class="toolbar-group" data-bind="visible: visible">
<button class="toolbar-group-button" data-bind="hasFocus: focused, attr: {id: id, title: title, 'aria-label': displayName}, event: { mousedown: mouseDown, keydown: keyDown, keyup: keyUp }, enable: enabled">
<div class="toolbar-group-button-icon">
<div class="toolbar_icon" data-bind="icon: icon"></div>
</div>
<span data-bind="text: displayName"></span>
</button>
</div>
<!-- /ko -->
<!-- ko if: type === "toggle" -->
<div class="toolbar-group" data-bind="visible: visible">
<button class="toolbar-group-button toggle-button" data-bind="hasFocus: focused, attr: {id: id, title: title}, event: { mousedown: mouseDown, keydown: keyDown, keyup: keyUp }, enable: enabled">
<div class="toolbar-group-button-icon" data-bind="css: { 'toggle-checked': checked }">
<div class="toolbar_icon" data-bind="icon: icon"></div>
</div>
<span data-bind="text: displayName"></span>
</button>
</div>
<!-- /ko -->
<!-- ko if: type === "dropdown" -->
<div class="toolbar-group" data-bind="visible: visible">
<div class="dropdown" data-bind="attr: {id: (id + '-dropdown')}">
<button role="menu" class="toolbar-group-button" data-bind="hasFocus: focused, attr: {id: id, title: title, 'aria-label': displayName}, event: { mousedown: mouseDown, keydown: keyDown, keyup: keyUp }, enable: enabled">
<div class="toolbar-group-button-icon">
<div class="toolbar_icon" data-bind="icon: icon"></div>
</div>
<span data-bind="text: displayName"></span>
</button>
</div>
</div>
<!-- /ko -->
<!-- ko if: type === "separator" -->
<div class="toolbar-group vertical-separator" data-bind="visible: visible"></div>
<!-- /ko -->
</script>

View File

@@ -1,16 +1,17 @@
jest.mock("../../Common/DocumentClientUtilityBase");
import * as ko from "knockout";
import * as sinon from "sinon";
import * as ViewModels from "../../Contracts/ViewModels";
import DocumentClientUtilityBase from "../../Common/DocumentClientUtilityBase";
import Q from "q";
import { CollectionStub, DatabaseStub, ExplorerStub } from "../OpenActionsStubs";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { CosmosClient } from "../../Common/CosmosClient";
import * as DocumentClientUtility from "../../Common/DocumentClientUtilityBase";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import Explorer from "../Explorer";
import { updateUserContext } from "../../UserContext";
describe("ContainerSampleGenerator", () => {
const createExplorerStub = (database: ViewModels.Database): ExplorerStub => {
const explorerStub = new ExplorerStub();
const createExplorerStub = (database: ViewModels.Database): Explorer => {
const explorerStub = {} as Explorer;
explorerStub.nonSystemDatabases = ko.computed(() => [database]);
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
@@ -53,36 +54,42 @@ describe("ContainerSampleGenerator", () => {
}
]
};
const collection = new CollectionStub({ id: ko.observable(sampleCollectionId) });
const database = new DatabaseStub({
const collection = { id: ko.observable(sampleCollectionId) } as ViewModels.Collection;
const database = {
id: ko.observable(sampleDatabaseId),
collections: ko.observableArray([collection])
});
collections: ko.observableArray<ViewModels.Collection>([collection])
} as ViewModels.Database;
database.findCollectionWithId = () => collection;
const explorerStub = createExplorerStub(database);
explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => true);
const fakeDocumentClientUtility = sinon.createStubInstance(DocumentClientUtilityBase);
fakeDocumentClientUtility.getOrCreateDatabaseAndCollection.returns(Q.resolve(collection));
fakeDocumentClientUtility.createDocument.returns(Q.resolve());
explorerStub.documentClientUtility = fakeDocumentClientUtility;
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
generator.setData(sampleData);
await generator.createSampleContainerAsync();
expect(fakeDocumentClientUtility.createDocument.called).toBe(true);
expect(DocumentClientUtility.createDocument).toHaveBeenCalled();
});
it("should send gremlin queries for Graph API account", async () => {
sinon.stub(GremlinClient.prototype, "initialize").callsFake(() => {});
const executeStub = sinon.stub(GremlinClient.prototype, "execute").returns(Q.resolve());
sinon.stub(CosmosClient, "databaseAccount").returns({
properties: {}
updateUserContext({
databaseAccount: {
id: "foo",
name: "foo",
location: "foo",
type: "foo",
kind: "foo",
tags: [],
properties: {
documentEndpoint: "bar",
gremlinEndpoint: "foo",
tableEndpoint: "foo",
cassandraEndpoint: "foo"
}
}
});
const sampleCollectionId = "SampleCollection";
@@ -98,29 +105,23 @@ describe("ContainerSampleGenerator", () => {
"g.addV('person').property(id, '1').property('_partitionKey','pk').property('name', 'Eva').property('age', 44)"
]
};
const collection = new CollectionStub({ id: ko.observable(sampleCollectionId) });
const database = new DatabaseStub({
const collection = { id: ko.observable(sampleCollectionId) } as ViewModels.Collection;
const database = {
id: ko.observable(sampleDatabaseId),
collections: ko.observableArray([collection])
});
collections: ko.observableArray<ViewModels.Collection>([collection])
} as ViewModels.Database;
database.findCollectionWithId = () => collection;
collection.databaseId = database.id();
const explorerStub = createExplorerStub(database);
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => true);
const fakeDocumentClientUtility = sinon.createStubInstance(DocumentClientUtilityBase);
fakeDocumentClientUtility.getOrCreateDatabaseAndCollection.returns(Q.resolve(collection));
fakeDocumentClientUtility.createDocument.returns(Q.resolve());
explorerStub.documentClientUtility = fakeDocumentClientUtility;
const generator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub);
generator.setData(sampleData);
await generator.createSampleContainerAsync();
expect(fakeDocumentClientUtility.createDocument.called).toBe(false);
expect(DocumentClientUtility.createDocument).toHaveBeenCalled();
expect(executeStub.called).toBe(true);
});

View File

@@ -3,9 +3,11 @@ import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import GraphTab from ".././Tabs/GraphTab";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { CosmosClient } from "../../Common/CosmosClient";
import { GremlinClient } from "../Graph/GraphExplorerComponent/GremlinClient";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer";
import { createDocument, getOrCreateDatabaseAndCollection } from "../../Common/DocumentClientUtilityBase";
import { userContext } from "../../UserContext";
interface SampleDataFile extends DataModels.CreateDatabaseAndCollectionRequest {
data: any[];
@@ -14,12 +16,12 @@ interface SampleDataFile extends DataModels.CreateDatabaseAndCollectionRequest {
export class ContainerSampleGenerator {
private sampleDataFile: SampleDataFile;
private constructor(private container: ViewModels.Explorer) {}
private constructor(private container: Explorer) {}
/**
* Factory function to load the json data file
*/
public static async createSampleGeneratorAsync(container: ViewModels.Explorer): Promise<ContainerSampleGenerator> {
public static async createSampleGeneratorAsync(container: Explorer): Promise<ContainerSampleGenerator> {
const generator = new ContainerSampleGenerator(container);
let dataFileContent: any;
if (container.isPreferredApiGraph()) {
@@ -63,7 +65,7 @@ export class ContainerSampleGenerator {
options.initialHeaders[Constants.HttpHeaders.usePolygonsSmallerThanAHemisphere] = true;
}
await this.container.documentClientUtility.getOrCreateDatabaseAndCollection(createRequest, options);
await getOrCreateDatabaseAndCollection(createRequest, options);
await this.container.refreshAllDatabases();
const database = this.container.findDatabaseWithId(this.sampleDataFile.databaseId);
if (!database) {
@@ -85,14 +87,14 @@ export class ContainerSampleGenerator {
if (!queries || queries.length < 1) {
return;
}
const account = CosmosClient.databaseAccount();
const account = userContext.databaseAccount;
const databaseId = collection.databaseId;
const gremlinClient = new GremlinClient();
gremlinClient.initialize({
endpoint: `wss://${GraphTab.getGremlinEndpoint(account)}`,
databaseId: databaseId,
collectionId: collection.id(),
masterKey: CosmosClient.masterKey() || "",
masterKey: userContext.masterKey || "",
maxResultSize: 100
});
@@ -102,7 +104,7 @@ export class ContainerSampleGenerator {
} else {
// For SQL all queries are executed at the same time
this.sampleDataFile.data.forEach(doc => {
const subPromise = this.container.documentClientUtility.createDocument(collection, doc);
const subPromise = createDocument(collection, doc);
subPromise.catch(reason => NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, reason));
promises.push(subPromise);
});

View File

@@ -1,20 +1,21 @@
import { ExplorerStub, DatabaseStub, CollectionStub } from "../OpenActionsStubs";
import { DataSamplesUtil } from "./DataSamplesUtil";
import * as sinon from "sinon";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import * as ko from "knockout";
import Explorer from "../Explorer";
import { Database, Collection } from "../../Contracts/ViewModels";
describe("DataSampleUtils", () => {
const sampleCollectionId = "sampleCollectionId";
const sampleDatabaseId = "sampleDatabaseId";
it("should not create sample collection if collection already exists", async () => {
const collection = new CollectionStub({ id: ko.observable(sampleCollectionId) });
const database = new DatabaseStub({
const collection = { id: ko.observable(sampleCollectionId) } as Collection;
const database = {
id: ko.observable(sampleDatabaseId),
collections: ko.observableArray([collection])
});
const explorer = new ExplorerStub();
collections: ko.observableArray<Collection>([collection])
} as Database;
const explorer = {} as Explorer;
explorer.nonSystemDatabases = ko.computed(() => [database]);
explorer.showOkModalDialog = () => {};
const dataSamplesUtil = new DataSamplesUtil(explorer);

View File

@@ -1,11 +1,12 @@
import * as ViewModels from "../../Contracts/ViewModels";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import Explorer from "../Explorer";
export class DataSamplesUtil {
private static readonly DialogTitle = "Create Sample Container";
constructor(private container: ViewModels.Explorer) {}
constructor(private container: Explorer) {}
/**
* Check if Database/Container is already there: if so, show modal to delete

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@ import { GraphData, D3Node, D3Link } from "./GraphData";
import { HashMap } from "../../../Common/HashMap";
import { BaseType } from "d3";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { GraphConfig } from "../../Tabs/GraphTab";
import { GraphExplorer } from "./GraphExplorer";
import * as Constants from "../../../Common/Constants";

View File

@@ -1,3 +1,4 @@
jest.mock("../../../Common/DocumentClientUtilityBase");
import React from "react";
import * as sinon from "sinon";
import { mount, ReactWrapper } from "enzyme";
@@ -7,11 +8,11 @@ import { GraphExplorer, GraphExplorerProps, GraphAccessor, GraphHighlightedNodeD
import * as D3ForceGraph from "./D3ForceGraph";
import { GraphData } from "./GraphData";
import { TabComponent } from "../../Controls/Tabs/TabComponent";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as DataModels from "../../../Contracts/DataModels";
import * as StorageUtility from "../../../Shared/StorageUtility";
import GraphTab from "../../Tabs/GraphTab";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
describe("Check whether query result is vertex array", () => {
it("should reject null as vertex array", () => {
@@ -86,13 +87,26 @@ describe("getPkIdFromDocumentId", () => {
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("['pkvalue', 'id']");
});
it("should create pkid pair from partitioned graph (pk as number)", () => {
const doc = createFakeDoc({ id: "id", mypk: 234 });
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("[234, 'id']");
});
it("should create pkid pair from partitioned graph (pk as valid array value)", () => {
const doc = createFakeDoc({ id: "id", mypk: [{ id: "someid", _value: "pkvalue" }] });
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("['pkvalue', 'id']");
});
it("should error if id is not a string", () => {
const doc = createFakeDoc({ id: { foo: 1 } });
it("should error if id is not a string or number", () => {
let doc = createFakeDoc({ id: { foo: 1 } });
try {
GraphExplorer.getPkIdFromDocumentId(doc, undefined);
expect(true).toBe(false);
} catch (e) {
expect(true).toBe(true);
}
doc = createFakeDoc({ id: true });
try {
GraphExplorer.getPkIdFromDocumentId(doc, undefined);
expect(true).toBe(false);
@@ -101,16 +115,8 @@ describe("getPkIdFromDocumentId", () => {
}
});
it("should error if pk not string nor non-empty array", () => {
let doc = createFakeDoc({ mypk: { foo: 1 } });
try {
GraphExplorer.getPkIdFromDocumentId(doc, "mypk");
} catch (e) {
expect(true).toBe(true);
}
doc = createFakeDoc({ mypk: [] });
it("should error if pk is empty array", () => {
let doc = createFakeDoc({ mypk: [] });
try {
GraphExplorer.getPkIdFromDocumentId(doc, "mypk");
expect(true).toBe(false);
@@ -134,7 +140,7 @@ describe("GraphExplorer", () => {
const COLLECTION_SELF_LINK = "collectionSelfLink";
const gremlinRU = 789.12;
const createMockProps = (documentClientUtility?: any): GraphExplorerProps => {
const createMockProps = (): GraphExplorerProps => {
const graphConfig = GraphTab.createGraphConfig();
const graphConfigUi = GraphTab.createGraphConfigUiData(graphConfig);
@@ -149,7 +155,6 @@ describe("GraphExplorer", () => {
onIsValidQueryChange: (isValidQuery: boolean): void => {},
collectionPartitionKeyProperty: "collectionPartitionKeyProperty",
documentClientUtility: documentClientUtility,
collectionRid: COLLECTION_RID,
collectionSelfLink: COLLECTION_SELF_LINK,
graphBackendEndpoint: "graphBackendEndpoint",
@@ -188,7 +193,6 @@ describe("GraphExplorer", () => {
let wrapper: ReactWrapper;
let connectStub: sinon.SinonSpy;
let queryDocStub: sinon.SinonSpy;
let submitToBackendSpy: sinon.SinonSpy;
let renderResultAsJsonStub: sinon.SinonSpy;
let onMiddlePaneInitializedStub: sinon.SinonSpy;
@@ -215,46 +219,6 @@ describe("GraphExplorer", () => {
[query: string]: AjaxResponse;
}
const createDocumentClientUtilityMock = (docDBResponse: AjaxResponse) => {
const mock = {
queryDocuments: () => {},
queryDocumentsPage: (
rid: string,
iterator: any,
firstItemIndex: number,
options: any
): Q.Promise<ViewModels.QueryResults> => {
const qresult = {
hasMoreResults: false,
firstItemIndex: firstItemIndex,
lastItemIndex: 0,
itemCount: 0,
documents: docDBResponse.response,
activityId: "",
headers: [] as any[],
requestCharge: gVRU
};
return Q.resolve(qresult);
}
};
const fakeIterator: any = {
nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {},
hasMoreResults: () => false,
executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {}
};
queryDocStub = sinon.stub(mock, "queryDocuments").callsFake(
(container: ViewModels.DocumentRequestContainer, query: string, options: any): Q.Promise<any> => {
(fakeIterator as any)._query = query;
return Q.resolve(fakeIterator);
}
);
return mock;
};
const setupMocks = (
graphExplorer: GraphExplorer,
backendResponses: BackendResponses,
@@ -333,7 +297,29 @@ describe("GraphExplorer", () => {
done: any,
ignoreD3Update: boolean
): GraphExplorer => {
const props: GraphExplorerProps = createMockProps(createDocumentClientUtilityMock(docDBResponse));
(queryDocuments as jest.Mock).mockImplementation((container: any, query: string, options: any) => {
return Q.resolve({
_query: query,
nextItem: (callback: (error: any, document: DataModels.DocumentId) => void): void => {},
hasMoreResults: () => false,
executeNext: (callback: (error: any, documents: DataModels.DocumentId[], headers: any) => void): void => {}
});
});
(queryDocumentsPage as jest.Mock).mockImplementation(
(rid: string, iterator: any, firstItemIndex: number, options: any) => {
return Q.resolve({
hasMoreResults: false,
firstItemIndex: firstItemIndex,
lastItemIndex: 0,
itemCount: 0,
documents: docDBResponse.response,
activityId: "",
headers: [] as any[],
requestCharge: gVRU
});
}
);
const props: GraphExplorerProps = createMockProps();
wrapper = mount(<GraphExplorer {...props} />);
graphExplorerInstance = wrapper.instance() as GraphExplorer;
setupMocks(graphExplorerInstance, backendResponses, done, ignoreD3Update);
@@ -341,7 +327,7 @@ describe("GraphExplorer", () => {
};
const cleanUpStubsWrapper = () => {
queryDocStub.restore();
jest.resetAllMocks();
connectStub.restore();
submitToBackendSpy.restore();
renderResultAsJsonStub.restore();
@@ -378,22 +364,11 @@ describe("GraphExplorer", () => {
expect((graphExplorerInstance.submitToBackend as sinon.SinonSpy).calledWith("g.V()")).toBe(false);
});
it("should submit g.V() as docdb query with proper query", () => {
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[2]
).toBe(DOCDB_G_DOT_V_QUERY);
});
it("should submit g.V() as docdb query with proper parameters", () => {
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[0]
).toEqual("databaseId");
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[1]
).toEqual("collectionId");
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[3]
).toEqual({ maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE, enableCrossPartitionQuery: true });
expect(queryDocuments).toBeCalledWith("databaseId", "collectionId", DOCDB_G_DOT_V_QUERY, {
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
enableCrossPartitionQuery: true
});
});
it("should call backend thrice (user query, fetch outE, then fetch inE)", () => {
@@ -426,22 +401,11 @@ describe("GraphExplorer", () => {
expect((graphExplorerInstance.submitToBackend as sinon.SinonSpy).calledWith("g.V()")).toBe(false);
});
it("should submit g.V() as docdb query with proper query", () => {
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[2]
).toBe(DOCDB_G_DOT_V_QUERY);
});
it("should submit g.V() as docdb query with proper parameters", () => {
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[0]
).toEqual("databaseId");
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[1]
).toEqual("collectionId");
expect(
(graphExplorerInstance.props.documentClientUtility.queryDocuments as sinon.SinonSpy).getCall(0).args[3]
).toEqual({ maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE, enableCrossPartitionQuery: true });
expect(queryDocuments).toBeCalledWith("databaseId", "collectionId", DOCDB_G_DOT_V_QUERY, {
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
enableCrossPartitionQuery: true
});
});
it("should call backend thrice (user query, fetch outE, then fetch inE)", () => {

View File

@@ -8,7 +8,7 @@ import * as D3ForceGraph from "./D3ForceGraph";
import { GraphVizComponentProps } from "./GraphVizComponent";
import * as GraphData from "./GraphData";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { GraphUtil } from "./GraphUtil";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
@@ -28,7 +28,7 @@ import * as Constants from "../../../Common/Constants";
import { InputProperty } from "../../../Contracts/ViewModels";
import { QueryIterator, ItemDefinition, Resource } from "@azure/cosmos";
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
import DocumentClientUtilityBase from "../../../Common/DocumentClientUtilityBase";
import { queryDocuments, queryDocumentsPage } from "../../../Common/DocumentClientUtilityBase";
export interface GraphAccessor {
applyFilter: () => void;
@@ -47,7 +47,6 @@ export interface GraphExplorerProps {
onIsValidQueryChange: (isValidQuery: boolean) => void;
collectionPartitionKeyProperty: string;
documentClientUtility: DocumentClientUtilityBase;
collectionRid: string;
collectionSelfLink: string;
graphBackendEndpoint: string;
@@ -697,7 +696,6 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
* @param cmd
*/
public submitToBackend(cmd: string): Q.Promise<GremlinClient.GremlinRequestResult> {
console.log("submit:", cmd);
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${cmd}`);
this.setExecuteCounter(this.executeCounter + 1);
@@ -730,26 +728,24 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
*/
public executeNonPagedDocDbQuery(query: string): Q.Promise<DataModels.DocumentId[]> {
// TODO maxItemCount: this reduces throttling, but won't cap the # of results
return this.props.documentClientUtility
.queryDocuments(this.props.databaseId, this.props.collectionId, query, {
maxItemCount: GraphExplorer.PAGE_ALL,
enableCrossPartitionQuery:
StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.IsCrossPartitionQueryEnabled) ===
"true"
})
.then(
(iterator: QueryIterator<ItemDefinition & Resource>) => {
return iterator.fetchNext().then(response => response.resources);
},
(reason: any) => {
GraphExplorer.reportToConsole(
ConsoleDataType.Error,
`Failed to execute non-paged query ${query}. Reason:${reason}`,
reason
);
return null;
}
);
return queryDocuments(this.props.databaseId, this.props.collectionId, query, {
maxItemCount: GraphExplorer.PAGE_ALL,
enableCrossPartitionQuery:
StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.IsCrossPartitionQueryEnabled) ===
"true"
}).then(
(iterator: QueryIterator<ItemDefinition & Resource>) => {
return iterator.fetchNext().then(response => response.resources);
},
(reason: any) => {
GraphExplorer.reportToConsole(
ConsoleDataType.Error,
`Failed to execute non-paged query ${query}. Reason:${reason}`,
reason
);
return null;
}
);
}
/**
@@ -1375,7 +1371,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
if (collectionPartitionKeyProperty && d.hasOwnProperty(collectionPartitionKeyProperty)) {
let pk = (d as any)[collectionPartitionKeyProperty];
if (typeof pk !== "string") {
if (typeof pk !== "string" && typeof pk !== "number") {
if (Array.isArray(pk) && pk.length > 0) {
// pk is [{ id: 'id', _value: 'value' }]
pk = pk[0]["_value"];
@@ -1732,12 +1728,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
query = `select root.id, root.${this.props.collectionPartitionKeyProperty} from root where IS_DEFINED(root._isEdge) = false order by root._ts asc`;
}
return this.props.documentClientUtility
.queryDocuments(this.props.databaseId, this.props.collectionId, query, {
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
enableCrossPartitionQuery:
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
})
return queryDocuments(this.props.databaseId, this.props.collectionId, query, {
maxItemCount: GraphExplorer.ROOT_LIST_PAGE_SIZE,
enableCrossPartitionQuery: LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
})
.then(
(iterator: QueryIterator<ItemDefinition & Resource>) => {
this.currentDocDBQueryInfo = {
@@ -1766,16 +1760,15 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
.currentDocDBQueryInfo.index + GraphExplorer.ROOT_LIST_PAGE_SIZE})`;
const id = GraphExplorer.reportToConsole(ConsoleDataType.InProgress, `Executing: ${queryInfoStr}`);
return this.props.documentClientUtility
.queryDocumentsPage(
this.props.collectionRid,
this.currentDocDBQueryInfo.iterator,
this.currentDocDBQueryInfo.index,
{
enableCrossPartitionQuery:
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
}
)
return queryDocumentsPage(
this.props.collectionRid,
this.currentDocDBQueryInfo.iterator,
this.currentDocDBQueryInfo.index,
{
enableCrossPartitionQuery:
LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
}
)
.then((results: ViewModels.QueryResults) => {
GraphExplorer.clearConsoleProgress(id);
this.currentDocDBQueryInfo.index = results.lastItemIndex + 1;

View File

@@ -3,7 +3,6 @@ import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { GraphConfig } from "../../Tabs/GraphTab";
import * as ViewModels from "../../../Contracts/ViewModels";
import { GraphExplorer, GraphAccessor } from "./GraphExplorer";
import DocumentClientUtilityBase from "../../../Common/DocumentClientUtilityBase";
interface Parameter {
onIsNewVertexDisabledChange: (isEnabled: boolean) => void;
@@ -18,7 +17,6 @@ interface Parameter {
graphConfig?: GraphConfig;
collectionPartitionKeyProperty: string;
documentClientUtility: DocumentClientUtilityBase;
collectionRid: string;
collectionSelfLink: string;
graphBackendEndpoint: string;
@@ -51,7 +49,6 @@ export class GraphExplorerAdapter implements ReactAdapter {
onIsGraphDisplayed={this.params.onIsGraphDisplayed}
onResetDefaultGraphConfigValues={this.params.onResetDefaultGraphConfigValues}
collectionPartitionKeyProperty={this.params.collectionPartitionKeyProperty}
documentClientUtility={this.params.documentClientUtility}
collectionRid={this.params.collectionRid}
collectionSelfLink={this.params.collectionSelfLink}
graphBackendEndpoint={this.params.graphBackendEndpoint}

View File

@@ -1,6 +1,6 @@
import * as sinon from "sinon";
import { GremlinClient, GremlinClientParameters } from "./GremlinClient";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import * as Logger from "../../../Common/Logger";
describe("Gremlin Client", () => {

View File

@@ -4,7 +4,7 @@
import * as Q from "q";
import { GremlinSimpleClient, Result } from "./GremlinSimpleClient";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { HashMap } from "../../../Common/HashMap";
import * as Logger from "../../../Common/Logger";

View File

@@ -11,15 +11,21 @@ import { CommandBarComponentButtonFactory } from "./CommandBarComponentButtonFac
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { StyleConstants } from "../../../Common/Constants";
import { CommandBarUtil } from "./CommandBarUtil";
import Explorer from "../../Explorer";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
export class CommandBarComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
public container: ViewModels.Explorer;
private tabsButtons: ViewModels.NavbarButtonConfig[];
public container: Explorer;
private tabsButtons: CommandButtonComponentProps[];
private isNotebookTabActive: ko.Computed<boolean>;
constructor(container: ViewModels.Explorer) {
constructor(container: Explorer) {
this.container = container;
this.tabsButtons = [];
this.isNotebookTabActive = ko.computed(() =>
container.tabsManager.isTabActive(ViewModels.CollectionTabKind.NotebookV2)
);
// These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates
const toWatch = [
@@ -39,14 +45,15 @@ export class CommandBarComponentAdapter implements ReactAdapter {
container.isHostedDataExplorerEnabled,
container.isSynapseLinkUpdating,
container.databaseAccount,
container.isNotebookTabActive
this.isNotebookTabActive,
container.isServerlessEnabled
];
ko.computed(() => ko.toJSON(toWatch)).subscribe(() => this.triggerRender());
this.parameters = ko.observable(Date.now());
}
public onUpdateTabsButtons(buttons: ViewModels.NavbarButtonConfig[]): void {
public onUpdateTabsButtons(buttons: CommandButtonComponentProps[]): void {
this.tabsButtons = buttons;
this.triggerRender();
}
@@ -74,7 +81,7 @@ export class CommandBarComponentAdapter implements ReactAdapter {
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
if (this.container && this.container.isNotebookTabActive()) {
if (this.isNotebookTabActive()) {
uiFabricControlButtons.unshift(
CommandBarUtil.createMemoryTracker("memoryTracker", this.container.memoryUsageInfo)
);

View File

@@ -1,27 +1,70 @@
import * as ko from "knockout";
import * as ViewModels from "../../../Contracts/ViewModels";
import { CommandBarComponentButtonFactory } from "./CommandBarComponentButtonFactory";
import { ExplorerStub } from "../../OpenActionsStubs";
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
import NotebookManager from "../../Notebook/NotebookManager";
import Explorer from "../../Explorer";
describe("CommandBarComponentButtonFactory tests", () => {
let mockExplorer: ViewModels.Explorer;
let mockExplorer: Explorer;
describe("Enable notebook button", () => {
const enableNotebookBtnLabel = "Enable Notebooks (Preview)";
describe("Enable Azure Synapse Link Button", () => {
const enableAzureSynapseLinkBtnLabel = "Enable Azure Synapse Link (Preview)";
beforeAll(() => {
mockExplorer = new ExplorerStub();
mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isNotebookEnabled = ko.observable(false);
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
mockExplorer.isRunningOnNationalCloud = () => false;
});
it("Account is not serverless - button should be visible", () => {
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const enableAzureSynapseLinkBtn = buttons.find(
button => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
expect(enableAzureSynapseLinkBtn).toBeDefined();
});
it("Account is serverless - button should be hidden", () => {
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => true);
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
const enableAzureSynapseLinkBtn = buttons.find(
button => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
);
expect(enableAzureSynapseLinkBtn).toBeUndefined();
});
});
describe("Enable notebook button", () => {
const enableNotebookBtnLabel = "Enable Notebooks (Preview)";
beforeAll(() => {
mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
});
it("Notebooks is already enabled - button should be hidden", () => {
@@ -75,15 +118,17 @@ describe("CommandBarComponentButtonFactory tests", () => {
const openMongoShellBtnLabel = "Open Mongo Shell";
beforeAll(() => {
mockExplorer = new ExplorerStub();
mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
});
beforeEach(() => {
@@ -155,15 +200,17 @@ describe("CommandBarComponentButtonFactory tests", () => {
const openCassandraShellBtnLabel = "Open Cassandra Shell";
beforeAll(() => {
mockExplorer = new ExplorerStub();
mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
});
beforeEach(() => {
@@ -236,13 +283,14 @@ describe("CommandBarComponentButtonFactory tests", () => {
const manageGitHubSettingsBtnLabel = "Manage GitHub settings";
beforeAll(() => {
mockExplorer = new ExplorerStub();
mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(false);
mockExplorer.isPreferredApiTable = ko.computed(() => true);
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
mockExplorer.isPreferredApiCassandra = ko.computed<boolean>(() => false);
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
mockExplorer.isSparkEnabled = ko.observable(true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
@@ -250,6 +298,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
mockExplorer.isGalleryPublishEnabled = ko.computed<boolean>(() => false);
mockExplorer.notebookManager = new NotebookManager();
mockExplorer.notebookManager.gitHubOAuthService = new GitHubOAuthService(undefined);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
});
beforeEach(() => {
@@ -294,13 +343,14 @@ describe("CommandBarComponentButtonFactory tests", () => {
describe("Resource token", () => {
beforeAll(() => {
mockExplorer = new ExplorerStub();
mockExplorer = {} as Explorer;
mockExplorer.addCollectionText = ko.observable("mockText");
mockExplorer.isAuthWithResourceToken = ko.observable(true);
mockExplorer.isPreferredApiDocumentDB = ko.computed(() => true);
mockExplorer.isDatabaseNodeOrNoneSelected = () => true;
mockExplorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
mockExplorer.isResourceTokenCollectionNodeSelected = ko.computed(() => true);
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
});
it("should only show New SQL Query and Open Query buttons", () => {

View File

@@ -4,14 +4,11 @@ import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryCons
import { Areas } from "../../../Common/Constants";
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import ApacheSparkIcon from "../../../../images/notebook/Apache-spark.svg";
import AddDatabaseIcon from "../../../../images/AddDatabase.svg";
import AddCollectionIcon from "../../../../images/AddCollection.svg";
import AddSqlQueryIcon from "../../../../images/AddSqlQuery_16x16.svg";
import BrowseQueriesIcon from "../../../../images/BrowseQuery.svg";
import * as Constants from "../../../Common/Constants";
import DeleteIcon from "../../../../images/delete.svg";
import EditIcon from "../../../../images/edit.svg";
import OpenInTabIcon from "../../../../images/open-in-tab.svg";
import OpenQueryFromDiskIcon from "../../../../images/OpenQueryFromDisk.svg";
import CosmosTerminalIcon from "../../../../images/Cosmos-Terminal.svg";
@@ -25,21 +22,22 @@ import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import EnableNotebooksIcon from "../../../../images/notebook/Notebook-enable.svg";
import NewNotebookIcon from "../../../../images/notebook/Notebook-new.svg";
import ResetWorkspaceIcon from "../../../../images/notebook/Notebook-reset-workspace.svg";
import LibraryManageIcon from "../../../../images/notebook/Spark-library-manage.svg";
import GitHubIcon from "../../../../images/github.svg";
import SynapseIcon from "../../../../images/synapse-link.svg";
import { config, Platform } from "../../../Config";
import { configContext, Platform } from "../../../ConfigContext";
import Explorer from "../../Explorer";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
export class CommandBarComponentButtonFactory {
private static counter: number = 0;
public static createStaticCommandBarButtons(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig[] {
public static createStaticCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
if (container.isAuthWithResourceToken()) {
return CommandBarComponentButtonFactory.createStaticCommandBarButtonsForResourceToken(container);
}
const newCollectionBtn = CommandBarComponentButtonFactory.createNewCollectionGroup(container);
const buttons: ViewModels.NavbarButtonConfig[] = [newCollectionBtn];
const buttons: CommandButtonComponentProps[] = [newCollectionBtn];
const addSynapseLink = CommandBarComponentButtonFactory.createOpenSynapseLinkDialogButton(container);
if (addSynapseLink) {
@@ -88,25 +86,6 @@ export class CommandBarComponentButtonFactory {
buttons.push(CommandBarComponentButtonFactory.createNotebookWorkspaceResetButton(container));
}
// TODO: Should be replaced with the create arcadia spark pool button
// if (!container.isSparkEnabled() && container.isSparkEnabledForAccount()) {
// const createSparkClusterButton = CommandBarComponentButtonFactory.createSparkClusterButton(container);
// buttons.push(createSparkClusterButton);
// }
// TODO: Should be replaced with the edit/manage/delete arcadia spark pool button
// if (container.isSparkEnabled()) {
// const manageSparkClusterButton = CommandBarComponentButtonFactory.createMonitorClusterButton(container);
// manageSparkClusterButton.children = [
// CommandBarComponentButtonFactory.createMonitorClusterButton(container),
// CommandBarComponentButtonFactory.createEditClusterButton(container),
// CommandBarComponentButtonFactory.createDeleteClusterButton(container),
// CommandBarComponentButtonFactory.createLibraryManageButton(container),
// CommandBarComponentButtonFactory.createClusterLibraryButton(container)
// ];
// buttons.push(manageSparkClusterButton);
// }
if (!container.isDatabaseNodeOrNoneSelected()) {
if (container.isNotebookEnabled()) {
buttons.push(CommandBarComponentButtonFactory.createDivider());
@@ -134,7 +113,7 @@ export class CommandBarComponentButtonFactory {
if (CommandBarComponentButtonFactory.areScriptsSupported(container)) {
const label = "New Stored Procedure";
const newStoredProcedureBtn: ViewModels.NavbarButtonConfig = {
const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
@@ -155,12 +134,12 @@ export class CommandBarComponentButtonFactory {
return buttons;
}
public static createContextCommandBarButtons(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig[] {
const buttons: ViewModels.NavbarButtonConfig[] = [];
public static createContextCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (!container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()) {
const label = "New Shell";
const newMongoShellBtn: ViewModels.NavbarButtonConfig = {
const newMongoShellBtn: CommandButtonComponentProps = {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
@@ -178,15 +157,15 @@ export class CommandBarComponentButtonFactory {
return buttons;
}
public static createControlCommandBarButtons(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig[] {
const buttons: ViewModels.NavbarButtonConfig[] = [];
public static createControlCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
if (window.dataExplorerPlatform === PlatformType.Hosted) {
return buttons;
}
if (!container.isPreferredApiCassandra()) {
const label = "Settings";
const settingsPaneButton: ViewModels.NavbarButtonConfig = {
const settingsPaneButton: CommandButtonComponentProps = {
iconSrc: SettingsIcon,
iconAlt: label,
onCommandClick: () => container.settingsPane.open(),
@@ -201,7 +180,7 @@ export class CommandBarComponentButtonFactory {
if (container.isHostedDataExplorerEnabled()) {
const label = "Open Full Screen";
const fullScreenButton: ViewModels.NavbarButtonConfig = {
const fullScreenButton: CommandButtonComponentProps = {
iconSrc: OpenInTabIcon,
iconAlt: label,
onCommandClick: () => container.generateSharedAccessData(),
@@ -217,7 +196,7 @@ export class CommandBarComponentButtonFactory {
if (!container.hasOwnProperty("isEmulator") || !container.isEmulator) {
const label = "Feedback";
const feedbackButtonOptions: ViewModels.NavbarButtonConfig = {
const feedbackButtonOptions: CommandButtonComponentProps = {
iconSrc: FeedbackIcon,
iconAlt: label,
onCommandClick: () => container.provideFeedbackEmail(),
@@ -233,7 +212,7 @@ export class CommandBarComponentButtonFactory {
return buttons;
}
public static createDivider(): ViewModels.NavbarButtonConfig {
public static createDivider(): CommandButtonComponentProps {
const label = `divider${CommandBarComponentButtonFactory.counter++}`;
return {
isDivider: true,
@@ -246,11 +225,11 @@ export class CommandBarComponentButtonFactory {
};
}
private static areScriptsSupported(container: ViewModels.Explorer): boolean {
private static areScriptsSupported(container: Explorer): boolean {
return container.isPreferredApiDocumentDB() || container.isPreferredApiGraph();
}
private static createNewCollectionGroup(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
private static createNewCollectionGroup(container: Explorer): CommandButtonComponentProps {
const label = container.addCollectionText();
return {
iconSrc: AddCollectionIcon,
@@ -263,10 +242,15 @@ export class CommandBarComponentButtonFactory {
};
}
private static createOpenSynapseLinkDialogButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
if (config.platform === Platform.Emulator) {
private static createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
return null;
}
if (container.isServerlessEnabled()) {
return null;
}
if (
container.databaseAccount &&
container.databaseAccount() &&
@@ -298,7 +282,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createNewDatabase(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
private static createNewDatabase(container: Explorer): CommandButtonComponentProps {
const label = container.addDatabaseText();
return {
iconSrc: AddDatabaseIcon,
@@ -313,7 +297,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createNewSQLQueryButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
private static createNewSQLQueryButton(container: Explorer): CommandButtonComponentProps {
if (container.isPreferredApiDocumentDB() || container.isPreferredApiGraph()) {
const label = "New SQL Query";
return {
@@ -347,15 +331,15 @@ export class CommandBarComponentButtonFactory {
return null;
}
public static createScriptCommandButtons(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig[] {
const buttons: ViewModels.NavbarButtonConfig[] = [];
public static createScriptCommandButtons(container: Explorer): CommandButtonComponentProps[] {
const buttons: CommandButtonComponentProps[] = [];
const shouldEnableScriptsCommands: boolean =
!container.isDatabaseNodeOrNoneSelected() && CommandBarComponentButtonFactory.areScriptsSupported(container);
if (shouldEnableScriptsCommands) {
const label = "New Stored Procedure";
const newStoredProcedureBtn: ViewModels.NavbarButtonConfig = {
const newStoredProcedureBtn: CommandButtonComponentProps = {
iconSrc: AddStoredProcedureIcon,
iconAlt: label,
onCommandClick: () => {
@@ -372,7 +356,7 @@ export class CommandBarComponentButtonFactory {
if (shouldEnableScriptsCommands) {
const label = "New UDF";
const newUserDefinedFunctionBtn: ViewModels.NavbarButtonConfig = {
const newUserDefinedFunctionBtn: CommandButtonComponentProps = {
iconSrc: AddUdfIcon,
iconAlt: label,
onCommandClick: () => {
@@ -389,7 +373,7 @@ export class CommandBarComponentButtonFactory {
if (shouldEnableScriptsCommands) {
const label = "New Trigger";
const newTriggerBtn: ViewModels.NavbarButtonConfig = {
const newTriggerBtn: CommandButtonComponentProps = {
iconSrc: AddTriggerIcon,
iconAlt: label,
onCommandClick: () => {
@@ -407,7 +391,7 @@ export class CommandBarComponentButtonFactory {
return buttons;
}
private static createScaleAndSettingsButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
private static createScaleAndSettingsButton(container: Explorer): CommandButtonComponentProps {
let isShared = false;
if (container.isDatabaseNodeSelected()) {
isShared = container.findSelectedDatabase().isDatabaseShared();
@@ -432,7 +416,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createNewNotebookButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
private static createNewNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "New Notebook";
return {
iconSrc: NewNotebookIcon,
@@ -445,7 +429,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createuploadNotebookButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
private static createuploadNotebookButton(container: Explorer): CommandButtonComponentProps {
const label = "Upload to Notebook Server";
return {
iconSrc: NewNotebookIcon,
@@ -458,7 +442,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createOpenQueryButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
private static createOpenQueryButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Query";
return {
iconSrc: BrowseQueriesIcon,
@@ -471,7 +455,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createOpenQueryFromDiskButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
private static createOpenQueryFromDiskButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Query From Disk";
return {
iconSrc: OpenQueryFromDiskIcon,
@@ -484,8 +468,8 @@ export class CommandBarComponentButtonFactory {
};
}
private static createEnableNotebooksButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
if (config.platform === Platform.Emulator) {
private static createEnableNotebooksButton(container: Explorer): CommandButtonComponentProps {
if (configContext.platform === Platform.Emulator) {
return null;
}
const label = "Enable Notebooks (Preview)";
@@ -505,59 +489,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createSparkClusterButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
const label = "Enable Spark";
return {
iconSrc: ApacheSparkIcon,
iconAlt: "Enable spark icon",
onCommandClick: () => container.setupSparkClusterPane.open(),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label
};
}
private static createEditClusterButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
const label = "Edit Cluster";
return {
iconSrc: EditIcon,
iconAlt: "Edit cluster icon",
onCommandClick: () => container.manageSparkClusterPane.open(),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label
};
}
private static createDeleteClusterButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
const label = "Delete Cluster";
return {
iconSrc: DeleteIcon,
iconAlt: "Delete cluster icon",
onCommandClick: () => container.deleteCluster(),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label
};
}
private static createMonitorClusterButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
const label = "Monitor Cluster";
return {
iconSrc: ApacheSparkIcon,
iconAlt: "Monitor cluster icon",
onCommandClick: () => container.openSparkMasterTab(),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label
};
}
private static createOpenTerminalButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
private static createOpenTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Terminal";
return {
iconSrc: CosmosTerminalIcon,
@@ -570,7 +502,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createOpenMongoTerminalButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
private static createOpenMongoTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Mongo Shell";
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
@@ -596,7 +528,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createOpenCassandraTerminalButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
private static createOpenCassandraTerminalButton(container: Explorer): CommandButtonComponentProps {
const label = "Open Cassandra Shell";
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
@@ -622,7 +554,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createNotebookWorkspaceResetButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
private static createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
const label = "Reset Workspace";
return {
iconSrc: ResetWorkspaceIcon,
@@ -635,7 +567,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createManageGitHubAccountButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
private static createManageGitHubAccountButton(container: Explorer): CommandButtonComponentProps {
let connectedToGitHub: boolean = container.notebookManager?.gitHubOAuthService.isLoggedIn();
const label = connectedToGitHub ? "Manage GitHub settings" : "Connect to GitHub";
return {
@@ -658,35 +590,7 @@ export class CommandBarComponentButtonFactory {
};
}
private static createLibraryManageButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
const label = "Manage Libraries";
return {
iconSrc: LibraryManageIcon,
iconAlt: label,
onCommandClick: () => container.libraryManagePane.open(),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label
};
}
private static createClusterLibraryButton(container: ViewModels.Explorer): ViewModels.NavbarButtonConfig {
const label = "Manage Cluster Libraries";
return {
iconSrc: LibraryManageIcon,
iconAlt: label,
onCommandClick: () => container.clusterLibraryPane.open(),
commandButtonLabel: label,
hasPopup: false,
disabled: false,
ariaLabel: label
};
}
private static createStaticCommandBarButtonsForResourceToken(
container: ViewModels.Explorer
): ViewModels.NavbarButtonConfig[] {
private static createStaticCommandBarButtonsForResourceToken(container: Explorer): CommandButtonComponentProps[] {
const newSqlQueryBtn = CommandBarComponentButtonFactory.createNewSQLQueryButton(container);
const openQueryBtn = CommandBarComponentButtonFactory.createOpenQueryButton(container);

View File

@@ -1,9 +1,10 @@
import { CommandBarUtil } from "./CommandBarUtil";
import * as ViewModels from "../../../Contracts/ViewModels";
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
describe("CommandBarUtil tests", () => {
const createButton = (): ViewModels.NavbarButtonConfig => {
const createButton = (): CommandButtonComponentProps => {
return {
iconSrc: "icon",
iconAlt: "label",
@@ -54,7 +55,7 @@ describe("CommandBarUtil tests", () => {
});
it("should create buttons with unique keys", () => {
const btns: ViewModels.NavbarButtonConfig[] = [];
const btns: CommandButtonComponentProps[] = [];
for (let i = 0; i < 5; i++) {
btns.push(createButton());
}

View File

@@ -1,12 +1,11 @@
import _ from "underscore";
import * as React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { Observable } from "knockout";
import { IconType } from "office-ui-fabric-react/lib/Icon";
import { IComponentAsProps } from "office-ui-fabric-react/lib/Utilities";
import { KeyCodes, StyleConstants } from "../../../Common/Constants";
import { StyleConstants } from "../../../Common/Constants";
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { Dropdown, DropdownMenuItemType, IDropdownStyles, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { Dropdown, IDropdownStyles, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import ChevronDownIcon from "../../../../images/Chevron_down.svg";
import { ArcadiaMenuPicker } from "../../Controls/Arcadia/ArcadiaMenuPicker";
@@ -21,13 +20,13 @@ export class CommandBarUtil {
* Convert our NavbarButtonConfig to UI Fabric buttons
* @param btns
*/
public static convertButton(btns: ViewModels.NavbarButtonConfig[], backgroundColor: string): ICommandBarItemProps[] {
public static convertButton(btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] {
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
return btns
.filter(btn => btn)
.map(
(btn: ViewModels.NavbarButtonConfig, index: number): ICommandBarItemProps => {
(btn: CommandButtonComponentProps, index: number): ICommandBarItemProps => {
if (btn.isDivider) {
return CommandBarUtil.createDivider(btn.commandButtonLabel);
}

View File

@@ -3,17 +3,19 @@
*/
import * as React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { CommandButtonComponent } from "../../Controls/CommandButton/CommandButtonComponent";
import {
CommandButtonComponent,
CommandButtonComponentProps
} from "../../Controls/CommandButton/CommandButtonComponent";
export interface ControlBarComponentProps {
buttons: ViewModels.NavbarButtonConfig[];
buttons: CommandButtonComponentProps[];
}
export class ControlBarComponent extends React.Component<ControlBarComponentProps> {
private static renderButtons(commandButtonOptions: ViewModels.NavbarButtonConfig[]): JSX.Element[] {
private static renderButtons(commandButtonOptions: CommandButtonComponentProps[]): JSX.Element[] {
return commandButtonOptions.map(
(btn: ViewModels.NavbarButtonConfig, index: number): JSX.Element => {
(btn: CommandButtonComponentProps, index: number): JSX.Element => {
// Remove label
btn.commandButtonLabel = null;
return CommandButtonComponent.renderButton(btn, `${index}`);

View File

@@ -8,12 +8,12 @@ import * as ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { ControlBarComponent } from "./ControlBarComponent";
import * as ViewModels from "../../../Contracts/ViewModels";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
export class ControlBarComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
constructor(private buttons: ko.ObservableArray<ViewModels.NavbarButtonConfig>) {
constructor(private buttons: ko.ObservableArray<CommandButtonComponentProps>) {
this.buttons.subscribe(() => this.forceRender());
this.parameters = ko.observable<number>(Date.now());
}

View File

@@ -28,7 +28,7 @@ exports[`test render renders signed in with full info 1`] = `
<FocusZone
direction={2}
isCircularNavigation={false}
preventDefaultWhenHandled={true}
shouldRaiseClicks={true}
>
<CustomizedDefaultButton
className="mecontrolHeaderButton"

View File

@@ -4,13 +4,14 @@ import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import * as ViewModels from "../../../Contracts/ViewModels";
import { NotificationConsoleComponent } from "./NotificationConsoleComponent";
import { ConsoleData } from "./NotificationConsoleComponent";
import Explorer from "../../Explorer";
export class NotificationConsoleComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
public container: ViewModels.Explorer;
public container: Explorer;
private consoleData: ko.ObservableArray<ConsoleData>;
constructor(container: ViewModels.Explorer) {
constructor(container: Explorer) {
this.container = container;
this.consoleData = container.notificationConsoleData;

View File

@@ -3,6 +3,7 @@ import { StorageKey, LocalStorageUtility } from "../../Shared/StorageUtility";
import CollectionIcon from "../../../images/tree-collection.svg";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import Explorer from "../Explorer";
export enum Type {
OpenCollection,
@@ -36,7 +37,7 @@ export class MostRecentActivity {
private static readonly schemaVersion: string = "1";
private static itemsMaxNumber: number = 5;
private storedData: StoredData;
constructor(private container: ViewModels.Explorer) {
constructor(private container: Explorer) {
// Retrieve from local storage
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity);

View File

@@ -8,6 +8,7 @@ import { actions, createContentRef, createKernelRef, selectors } from "@nteract/
import VirtualCommandBarComponent from "./VirtualCommandBarComponent";
import { NotebookContentItem } from "../NotebookContentItem";
import { NotebookComponentBootstrapper } from "./NotebookComponentBootstrapper";
import { CdbAppState } from "./types";
export interface NotebookComponentAdapterOptions {
contentItem: NotebookContentItem;
@@ -18,6 +19,7 @@ export interface NotebookComponentAdapterOptions {
export class NotebookComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
private onUpdateKernelInfo: () => void;
public getNotebookParentElement: () => HTMLElement;
public parameters: any;
constructor(options: NotebookComponentAdapterOptions) {
@@ -44,6 +46,11 @@ export class NotebookComponentAdapter extends NotebookComponentBootstrapper impl
})
);
}
this.getNotebookParentElement = () => {
const cdbAppState = this.getStore().getState() as CdbAppState;
return cdbAppState.cdb.currentNotebookParentElements.get(this.contentRef);
};
}
protected renderExtraComponent = (): JSX.Element => {

View File

@@ -17,7 +17,7 @@ import {
} from "@nteract/core";
import * as Immutable from "immutable";
import { Provider } from "react-redux";
import { CellType, CellId, toJS } from "@nteract/commutable";
import { CellType, CellId, ImmutableNotebook } from "@nteract/commutable";
import { Store, AnyAction } from "redux";
import "./NotebookComponent.less";
@@ -71,14 +71,14 @@ export class NotebookComponentBootstrapper {
);
}
public getContent(): { name: string; content: string } {
public getContent(): { name: string; content: string | ImmutableNotebook } {
const record = this.getStore()
.getState()
.core.entities.contents.byRef.get(this.contentRef);
let content: string;
let content: string | ImmutableNotebook;
switch (record.model.type) {
case "notebook":
content = JSON.stringify(toJS(record.model.notebook));
content = record.model.notebook;
break;
case "file":
content = record.model.text;

View File

@@ -84,3 +84,22 @@ export const traceNotebookTelemetry = (payload: {
payload
};
};
export const UPDATE_NOTEBOOK_PARENT_DOM_ELTS = "UPDATE_NOTEBOOK_PARENT_DOM_ELTS";
export interface UpdateNotebookParentDomEltAction {
type: "UPDATE_NOTEBOOK_PARENT_DOM_ELTS";
payload: {
contentRef: ContentRef;
parentElt: HTMLElement;
};
}
export const UpdateNotebookParentDomElt = (payload: {
contentRef: ContentRef;
parentElt: HTMLElement;
}): UpdateNotebookParentDomEltAction => {
return {
type: UPDATE_NOTEBOOK_PARENT_DOM_ELTS,
payload
};
};

View File

@@ -34,7 +34,7 @@ import { sessions, kernels } from "rx-jupyter";
import { RecordOf } from "immutable";
import * as Constants from "../../../Common/Constants";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import * as CdbActions from "./actions";
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
@@ -43,6 +43,7 @@ import { CdbAppState } from "./types";
import { decryptJWTToken } from "../../../Utils/AuthorizationUtils";
import * as TextFile from "./contents/file/text-file";
import { NotebookUtil } from "../NotebookUtil";
import { FileSystemUtil } from "../FileSystemUtil";
interface NotebookServiceConfig extends JupyterServerConfig {
userPuid?: string;
@@ -806,7 +807,9 @@ const closeUnsupportedMimetypesEpic = (
if (explorer && !TextFile.handles(mimetype)) {
const filepath = action.payload.filepath;
// Close tab and show error message
explorer.closeNotebookTab(filepath);
explorer.tabsManager.closeTabsByComparator(
tab => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
);
const msg = `${filepath} cannot be rendered. Please download the file, in order to view it outside of Data Explorer.`;
explorer.showOkModalDialog("File cannot be rendered", msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
@@ -832,7 +835,9 @@ const closeContentFailedToFetchEpic = (
if (explorer) {
const filepath = action.payload.filepath;
// Close tab and show error message
explorer.closeNotebookTab(filepath);
explorer.tabsManager.closeTabsByComparator(
tab => (tab as any).notebookPath && FileSystemUtil.isPathEqual((tab as any).notebookPath(), filepath)
);
const msg = `Failed to load file: ${filepath}.`;
explorer.showOkModalDialog("Failure to load", msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);

View File

@@ -82,6 +82,19 @@ export const cdbReducer = (state: CdbRecord, action: Action) => {
});
return state;
}
case cdbActions.UPDATE_NOTEBOOK_PARENT_DOM_ELTS: {
const typedAction = action as cdbActions.UpdateNotebookParentDomEltAction;
var parentEltsMap = state.get("currentNotebookParentElements");
const contentRef = typedAction.payload.contentRef;
const parentElt = typedAction.payload.parentElt;
if (parentElt) {
parentEltsMap.set(contentRef, parentElt);
} else {
parentEltsMap.delete(contentRef);
}
return state.set("currentNotebookParentElements", parentEltsMap);
}
}
return state;
};

View File

@@ -1,5 +1,5 @@
import * as Immutable from "immutable";
import { AppState } from "@nteract/core";
import { AppState, ContentRef } from "@nteract/core";
import { Notebook } from "../../../Common/Constants";
import { CellId } from "@nteract/commutable";
@@ -9,6 +9,7 @@ export interface CdbRecordProps {
defaultExperience: string | undefined;
kernelRestartDelayMs: number;
hoveredCellId: CellId | undefined;
currentNotebookParentElements: Map<ContentRef, HTMLElement>;
}
export type CdbRecord = Immutable.RecordOf<CdbRecordProps>;
@@ -21,5 +22,6 @@ export const makeCdbRecord = Immutable.Record<CdbRecordProps>({
databaseAccountName: undefined,
defaultExperience: undefined,
kernelRestartDelayMs: Notebook.kernelRestartInitialDelayMs,
hoveredCellId: undefined
hoveredCellId: undefined,
currentNotebookParentElements: new Map<ContentRef, HTMLElement>()
});

View File

@@ -2,13 +2,12 @@
* Notebook container related stuff
*/
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import * as Constants from "../../Common/Constants";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as Logger from "../../Common/Logger";
export class NotebookContainerClient implements ViewModels.INotebookContainerClient {
export class NotebookContainerClient {
private reconnectingNotificationId: string;
private isResettingWorkspace: boolean;
@@ -131,7 +130,7 @@ export class NotebookContainerClient implements ViewModels.INotebookContainerCli
}
private async recreateNotebookWorkspaceAsync(): Promise<void> {
const explorer = window.dataExplorer as ViewModels.Explorer;
const explorer = window.dataExplorer;
if (!explorer || !explorer.databaseAccount() || !explorer.databaseAccount().id) {
throw new Error("DataExplorer not initialized");
}

View File

@@ -1,5 +1,4 @@
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import { StringUtils } from "../../Utils/StringUtils";
import { FileSystemUtil } from "./FileSystemUtil";
@@ -9,7 +8,7 @@ import { ServerConfig, IContent, IContentProvider, FileType, IEmptyContent } fro
import { AjaxResponse } from "rxjs/ajax";
import { stringifyNotebook } from "@nteract/commutable";
export class NotebookContentClient implements ViewModels.INotebookContentClient {
export class NotebookContentClient {
constructor(
private notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>,
private notebookBasePath: ko.Observable<string>,
@@ -90,7 +89,7 @@ export class NotebookContentClient implements ViewModels.INotebookContentClient
throw new Error(`Parent must be a directory: ${parent}`);
}
const filepath = `${parent.path}/${name}`;
const filepath = NotebookUtil.getFilePath(parent.path, name);
if (await this.checkIfFilepathExists(filepath)) {
throw new Error(`File already exists: ${filepath}`);
}
@@ -117,12 +116,7 @@ export class NotebookContentClient implements ViewModels.INotebookContentClient
}
private async checkIfFilepathExists(filepath: string): Promise<boolean> {
const basename = filepath.split("/").pop();
let parentDirPath = filepath
.split(basename)
.shift()
.replace(/\/$/, ""); // no trailling slash
const parentDirPath = NotebookUtil.getParentPath(filepath);
const items = await this.fetchNotebookFiles(parentDirPath);
return items.some(value => FileSystemUtil.isPathEqual(value.path, filepath));
}

View File

@@ -3,7 +3,6 @@
*/
import { JunoClient } from "../../Juno/JunoClient";
import * as ViewModels from "../../Contracts/ViewModels";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { GitHubClient } from "../../GitHub/GitHubClient";
import * as Logger from "../../Common/Logger";
@@ -23,9 +22,13 @@ import { DialogProps } from "../Controls/DialogReactComponent/DialogComponent";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
import { getFullName } from "../../Utils/UserUtils";
import { ImmutableNotebook } from "@nteract/commutable";
import Explorer from "../Explorer";
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
import { CopyNotebookPaneAdapter } from "../Panes/CopyNotebookPane";
export interface NotebookManagerOptions {
container: ViewModels.Explorer;
container: Explorer;
notebookBasePath: ko.Observable<string>;
dialogProps: ko.Observable<DialogProps>;
resourceTree: ResourceTreeAdapter;
@@ -38,15 +41,16 @@ export default class NotebookManager {
public junoClient: JunoClient;
public notebookContentProvider: IContentProvider;
public notebookClient: ViewModels.INotebookContainerClient;
public notebookContentClient: ViewModels.INotebookContentClient;
public notebookClient: NotebookContainerClient;
public notebookContentClient: NotebookContentClient;
private gitHubContentProvider: GitHubContentProvider;
public gitHubOAuthService: GitHubOAuthService;
private gitHubClient: GitHubClient;
public gitHubReposPane: ViewModels.ContextualPane;
public gitHubReposPane: ContextualPaneBase;
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
public copyNotebookPaneAdapter: CopyNotebookPaneAdapter;
public initialize(params: NotebookManagerOptions): void {
this.params = params;
@@ -55,7 +59,6 @@ export default class NotebookManager {
this.gitHubOAuthService = new GitHubOAuthService(this.junoClient);
this.gitHubClient = new GitHubClient(this.onGitHubClientError);
this.gitHubReposPane = new GitHubReposPane({
documentClientUtility: this.params.container.documentClientUtility,
id: "gitHubReposPane",
visible: ko.observable<boolean>(false),
container: this.params.container,
@@ -89,6 +92,12 @@ export default class NotebookManager {
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
}
this.copyNotebookPaneAdapter = new CopyNotebookPaneAdapter(
this.params.container,
this.junoClient,
this.gitHubOAuthService
);
this.gitHubOAuthService.getTokenObservable().subscribe(token => {
this.gitHubClient.setToken(token?.access_token);
@@ -107,8 +116,29 @@ export default class NotebookManager {
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
}
public openPublishNotebookPane(name: string, content: string): void {
this.publishNotebookPaneAdapter.open(name, getFullName(), content);
public refreshPinnedRepos(): void {
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
}
public async openPublishNotebookPane(
name: string,
content: string | ImmutableNotebook,
parentDomElement: HTMLElement,
isCodeOfConductEnabled: boolean,
isLinkInjectionEnabled: boolean
): Promise<void> {
await this.publishNotebookPaneAdapter.open(
name,
getFullName(),
content,
parentDomElement,
isCodeOfConductEnabled,
isLinkInjectionEnabled
);
}
public openCopyNotebookPane(name: string, content: string): void {
this.copyNotebookPaneAdapter.open(name, content);
}
// Octokit's error handler uses any

View File

@@ -49,8 +49,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
editor: {
codemirror: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} readOnly={"nocursor"} />
},
prompt: ({ id, contentRef }) => <></>
}
}}
</CodeCell>
),

View File

@@ -30,12 +30,22 @@ import { CellType } from "@nteract/commutable/src";
import "./NotebookRenderer.less";
import HoverableCell from "./decorators/HoverableCell";
import CellLabeler from "./decorators/CellLabeler";
<<<<<<< HEAD
import MonacoEditor from "../MonacoEditor/MonacoEditor";
=======
import * as cdbActions from "../NotebookComponent/actions";
>>>>>>> master
export interface NotebookRendererProps {
export interface NotebookRendererBaseProps {
contentRef: any;
}
interface NotebookRendererDispatchProps {
updateNotebookParentDomElt: (contentRef: ContentRef, parentElt: HTMLElement) => void;
}
type NotebookRendererProps = NotebookRendererBaseProps & NotebookRendererDispatchProps;
interface PassedEditorProps {
id: string;
contentRef: ContentRef;
@@ -69,6 +79,8 @@ const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, child
};
class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
private notebookRendererRef = React.createRef<HTMLDivElement>();
constructor(props: NotebookRendererProps) {
super(props);
@@ -79,13 +91,22 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
componentDidMount() {
loadTransform(this.props as any);
this.props.updateNotebookParentDomElt(this.props.contentRef, this.notebookRendererRef.current);
}
componentDidUpdate() {
this.props.updateNotebookParentDomElt(this.props.contentRef, this.notebookRendererRef.current);
}
componentWillUnmount() {
this.props.updateNotebookParentDomElt(this.props.contentRef, undefined);
}
render(): JSX.Element {
return (
<>
<div className="NotebookRendererContainer">
<div className="NotebookRenderer">
<div className="NotebookRenderer" ref={this.notebookRendererRef}>
<DndProvider backend={HTML5Backend}>
<KeyboardShortcuts contentRef={this.props.contentRef}>
<Cells contentRef={this.props.contentRef}>
@@ -148,7 +169,7 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
}
}
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererProps) => {
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererBaseProps) => {
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
@@ -158,6 +179,14 @@ const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: Noteboo
component: transform
})
);
},
updateNotebookParentDomElt: (contentRef: ContentRef, parentElt: HTMLElement) => {
return dispatch(
cdbActions.UpdateNotebookParentDomElt({
contentRef,
parentElt
})
);
}
};
};

View File

@@ -1,12 +1,73 @@
import { NotebookUtil } from "./NotebookUtil";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import {
ImmutableNotebook,
MediaBundle,
CodeCellParams,
MarkdownCellParams,
makeCodeCell,
makeMarkdownCell,
makeNotebookRecord
} from "@nteract/commutable";
import { List, Map } from "immutable";
const fileName = "file";
const notebookName = "file.ipynb";
const filePath = `folder/${fileName}`;
const notebookPath = `folder/${notebookName}`;
const folderPath = "folder";
const filePath = `${folderPath}/${fileName}`;
const notebookPath = `${folderPath}/${notebookName}`;
const gitHubFolderUri = GitHubUtils.toContentUri("owner", "repo", "branch", folderPath);
const gitHubFileUri = GitHubUtils.toContentUri("owner", "repo", "branch", filePath);
const gitHubNotebookUri = GitHubUtils.toContentUri("owner", "repo", "branch", notebookPath);
const notebookRecord = makeNotebookRecord({
cellOrder: List.of("0", "1", "2", "3"),
cellMap: Map({
"0": makeMarkdownCell({
cell_type: "markdown",
source: "abc",
metadata: undefined
} as MarkdownCellParams),
"1": makeCodeCell({
cell_type: "code",
execution_count: undefined,
metadata: undefined,
source: "print(5)",
outputs: List.of({
name: "stdout",
output_type: "stream",
text: "5"
})
} as CodeCellParams),
"2": makeCodeCell({
cell_type: "code",
execution_count: undefined,
metadata: undefined,
source: 'display(HTML("<h1>Sample html</h1>"))',
outputs: List.of({
data: Object.freeze({
"text/html": "<h1>Sample output</h1>",
"text/plain": "<IPython.core.display.HTML object>"
} as MediaBundle),
output_type: "display_data",
metadata: undefined
})
} as CodeCellParams),
"3": makeCodeCell({
cell_type: "code",
execution_count: undefined,
metadata: undefined,
source: 'print("hello world")',
outputs: List.of({
name: "stdout",
output_type: "stream",
text: "hello world"
})
} as CodeCellParams)
}),
nbformat_minor: 2,
nbformat: 2,
metadata: undefined
});
describe("NotebookUtil", () => {
describe("isNotebookFile", () => {
@@ -21,6 +82,26 @@ describe("NotebookUtil", () => {
});
});
describe("getFilePath", () => {
it("works for jupyter file paths", () => {
expect(NotebookUtil.getFilePath(folderPath, fileName)).toEqual(filePath);
});
it("works for github file uris", () => {
expect(NotebookUtil.getFilePath(gitHubFolderUri, fileName)).toEqual(gitHubFileUri);
});
});
describe("getParentPath", () => {
it("works for jupyter file paths", () => {
expect(NotebookUtil.getParentPath(filePath)).toEqual(folderPath);
});
it("works for github file uris", () => {
expect(NotebookUtil.getParentPath(gitHubFileUri)).toEqual(gitHubFolderUri);
});
});
describe("getName", () => {
it("works for jupyter file paths", () => {
expect(NotebookUtil.getName(filePath)).toEqual(fileName);
@@ -46,4 +127,11 @@ describe("NotebookUtil", () => {
);
});
});
describe("findFirstCodeCellWithDisplay", () => {
it("works for Notebook file", () => {
const notebookObject = notebookRecord as ImmutableNotebook;
expect(NotebookUtil.findFirstCodeCellWithDisplay(notebookObject)).toEqual(1);
});
});
});

View File

@@ -1,5 +1,5 @@
import path from "path";
import { ImmutableNotebook } from "@nteract/commutable";
import { ImmutableNotebook, ImmutableCodeCell } from "@nteract/commutable";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import { StringUtils } from "../../Utils/StringUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils";
@@ -70,6 +70,46 @@ export class NotebookUtil {
};
}
public static getFilePath(path: string, fileName: string): string {
const contentInfo = GitHubUtils.fromContentUri(path);
if (contentInfo) {
let path = fileName;
if (contentInfo.path) {
path = `${contentInfo.path}/${path}`;
}
return GitHubUtils.toContentUri(contentInfo.owner, contentInfo.repo, contentInfo.branch, path);
}
return `${path}/${fileName}`;
}
public static getParentPath(filepath: string): undefined | string {
const basename = NotebookUtil.getName(filepath);
if (basename) {
const contentInfo = GitHubUtils.fromContentUri(filepath);
if (contentInfo) {
const parentPath = contentInfo.path.split(basename).shift();
if (parentPath === undefined) {
return undefined;
}
return GitHubUtils.toContentUri(
contentInfo.owner,
contentInfo.repo,
contentInfo.branch,
parentPath.replace(/\/$/, "") // no trailling slash
);
}
const parentPath = filepath.split(basename).shift();
if (parentPath) {
return parentPath.replace(/\/$/, ""); // no trailling slash
}
}
return undefined;
}
public static getName(path: string): undefined | string {
let relativePath: string = path;
const contentInfo = GitHubUtils.fromContentUri(path);
@@ -100,4 +140,24 @@ export class NotebookUtil {
const basePath = path.split(contentName).shift();
return `${basePath}${newName}`;
}
public static findFirstCodeCellWithDisplay(notebookObject: ImmutableNotebook): number {
let codeCellIndex = 0;
for (let i = 0; i < notebookObject.cellOrder.size; i++) {
const cellId = notebookObject.cellOrder.get(i);
if (cellId) {
const cell = notebookObject.cellMap.get(cellId);
if (cell?.cell_type === "code") {
const displayOutput = (cell as ImmutableCodeCell)?.outputs?.find(
output => output.output_type === "display_data" || output.output_type === "execute_result"
);
if (displayOutput) {
return codeCellIndex;
}
codeCellIndex++;
}
}
}
throw new Error("Output does not exist for any of the cells.");
}
}

View File

@@ -1,55 +1,43 @@
import * as ko from "knockout";
import { handleOpenAction } from "./OpenActions";
import * as ViewModels from "../Contracts/ViewModels";
import {
ExplorerStub,
DatabaseStub,
CollectionStub,
AddCollectionPaneStub,
CassandraAddCollectionPane
} from "./OpenActionsStubs";
import { ActionContracts } from "../Contracts/ExplorerContracts";
import Explorer from "./Explorer";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import AddCollectionPane from "./Panes/AddCollectionPane";
describe("OpenActions", () => {
describe("handleOpenAction", () => {
let explorer: ViewModels.Explorer;
let explorer: Explorer;
let database: ViewModels.Database;
let collection: ViewModels.Collection;
let databases: ViewModels.Database[];
let expandCollection: jasmine.Spy;
let onDocumentDBDocumentsClick: jasmine.Spy;
let onMongoDBDocumentsClick: jasmine.Spy;
let onTableEntitiesClick: jasmine.Spy;
let onGraphDocumentsClick: jasmine.Spy;
let onNewQueryClick: jasmine.Spy;
let onSettingsClick: jasmine.Spy;
let openAddCollectionPane: jasmine.Spy;
let openCassandraAddCollectionPane: jasmine.Spy;
beforeEach(() => {
explorer = new ExplorerStub();
explorer.addCollectionPane = new AddCollectionPaneStub();
explorer.cassandraAddCollectionPane = new CassandraAddCollectionPane();
explorer = {} as Explorer;
explorer.addCollectionPane = {} as AddCollectionPane;
explorer.addCollectionPane.open = jest.fn();
explorer.cassandraAddCollectionPane = {} as CassandraAddCollectionPane;
explorer.cassandraAddCollectionPane.open = jest.fn();
explorer.closeAllPanes = () => {};
explorer.isConnectExplorerVisible = () => false;
database = new DatabaseStub({
database = {
id: ko.observable("db"),
collections: ko.observableArray<ViewModels.Collection>([])
});
} as ViewModels.Database;
databases = [database];
collection = new CollectionStub({
collection = {
id: ko.observable("coll")
});
} as ViewModels.Collection;
expandCollection = spyOn(collection, "expandCollection");
onDocumentDBDocumentsClick = spyOn(collection, "onDocumentDBDocumentsClick");
onMongoDBDocumentsClick = spyOn(collection, "onMongoDBDocumentsClick");
onTableEntitiesClick = spyOn(collection, "onTableEntitiesClick");
onGraphDocumentsClick = spyOn(collection, "onGraphDocumentsClick");
onNewQueryClick = spyOn(collection, "onNewQueryClick");
onSettingsClick = spyOn(collection, "onSettingsClick");
openAddCollectionPane = spyOn(explorer.addCollectionPane, "open");
openCassandraAddCollectionPane = spyOn(explorer.cassandraAddCollectionPane, "open");
collection.expandCollection = jest.fn();
collection.onDocumentDBDocumentsClick = jest.fn();
collection.onMongoDBDocumentsClick = jest.fn();
collection.onTableEntitiesClick = jest.fn();
collection.onGraphDocumentsClick = jest.fn();
collection.onNewQueryClick = jest.fn();
collection.onSettingsClick = jest.fn();
});
describe("unknown action type", () => {
@@ -87,7 +75,7 @@ describe("OpenActions", () => {
};
const actionHandled = handleOpenAction(action, [], explorer);
expect(openCassandraAddCollectionPane).toHaveBeenCalled();
expect(explorer.cassandraAddCollectionPane.open).toHaveBeenCalled();
});
it("enum value should call cassandraAddCollectionPane.open", () => {
@@ -97,7 +85,7 @@ describe("OpenActions", () => {
};
const actionHandled = handleOpenAction(action, [], explorer);
expect(openCassandraAddCollectionPane).toHaveBeenCalled();
expect(explorer.cassandraAddCollectionPane.open).toHaveBeenCalled();
});
});
@@ -109,7 +97,7 @@ describe("OpenActions", () => {
};
const actionHandled = handleOpenAction(action, [], explorer);
expect(openAddCollectionPane).toHaveBeenCalled();
expect(explorer.addCollectionPane.open).toHaveBeenCalled();
});
it("enum value should call addCollectionPane.open", () => {
@@ -119,7 +107,7 @@ describe("OpenActions", () => {
};
const actionHandled = handleOpenAction(action, [], explorer);
expect(openAddCollectionPane).toHaveBeenCalled();
expect(explorer.addCollectionPane.open).toHaveBeenCalled();
});
});
});
@@ -149,10 +137,10 @@ describe("OpenActions", () => {
};
handleOpenAction(action, [database], explorer);
expect(expandCollection).not.toHaveBeenCalled();
expect(collection.expandCollection).not.toHaveBeenCalled();
database.collections([collection]);
expect(expandCollection).toHaveBeenCalled();
expect(collection.expandCollection).toHaveBeenCalled();
});
it("should expand collection node when handleOpenAction is called", () => {
@@ -164,7 +152,7 @@ describe("OpenActions", () => {
database.collections([collection]);
handleOpenAction(action, [database], explorer);
expect(expandCollection).toHaveBeenCalled();
expect(collection.expandCollection).toHaveBeenCalled();
});
describe("SQLDocuments tab kind", () => {
@@ -177,10 +165,10 @@ describe("OpenActions", () => {
};
handleOpenAction(action, [database], explorer);
expect(onDocumentDBDocumentsClick).not.toHaveBeenCalled();
expect(collection.onDocumentDBDocumentsClick).not.toHaveBeenCalled();
database.collections([collection]);
expect(onDocumentDBDocumentsClick).toHaveBeenCalled();
expect(collection.onDocumentDBDocumentsClick).toHaveBeenCalled();
});
it("string value should call onDocumentDBDocumentsClick", () => {
@@ -193,7 +181,7 @@ describe("OpenActions", () => {
database.collections([collection]);
handleOpenAction(action, [database], explorer);
expect(onDocumentDBDocumentsClick).toHaveBeenCalled();
expect(collection.onDocumentDBDocumentsClick).toHaveBeenCalled();
});
it("enum value should call onDocumentDBDocumentsClick before collections are fetched", () => {
@@ -205,10 +193,10 @@ describe("OpenActions", () => {
};
handleOpenAction(action, [database], explorer);
expect(onDocumentDBDocumentsClick).not.toHaveBeenCalled();
expect(collection.onDocumentDBDocumentsClick).not.toHaveBeenCalled();
database.collections([collection]);
expect(onDocumentDBDocumentsClick).toHaveBeenCalled();
expect(collection.onDocumentDBDocumentsClick).toHaveBeenCalled();
});
it("enum value should call onDocumentDBDocumentsClick", () => {
@@ -221,7 +209,7 @@ describe("OpenActions", () => {
database.collections([collection]);
handleOpenAction(action, [database], explorer);
expect(onDocumentDBDocumentsClick).toHaveBeenCalled();
expect(collection.onDocumentDBDocumentsClick).toHaveBeenCalled();
});
});
@@ -235,10 +223,10 @@ describe("OpenActions", () => {
};
handleOpenAction(action, [database], explorer);
expect(onMongoDBDocumentsClick).not.toHaveBeenCalled();
expect(collection.onMongoDBDocumentsClick).not.toHaveBeenCalled();
database.collections([collection]);
expect(onMongoDBDocumentsClick).toHaveBeenCalled();
expect(collection.onMongoDBDocumentsClick).toHaveBeenCalled();
});
it("string value should call onMongoDBDocumentsClick", () => {
@@ -251,7 +239,7 @@ describe("OpenActions", () => {
database.collections([collection]);
handleOpenAction(action, [database], explorer);
expect(onMongoDBDocumentsClick).toHaveBeenCalled();
expect(collection.onMongoDBDocumentsClick).toHaveBeenCalled();
});
it("enum value should call onMongoDBDocumentsClick before collections are fetched", () => {
@@ -263,10 +251,10 @@ describe("OpenActions", () => {
};
handleOpenAction(action, [database], explorer);
expect(onMongoDBDocumentsClick).not.toHaveBeenCalled();
expect(collection.onMongoDBDocumentsClick).not.toHaveBeenCalled();
database.collections([collection]);
expect(onMongoDBDocumentsClick).toHaveBeenCalled();
expect(collection.onMongoDBDocumentsClick).toHaveBeenCalled();
});
it("enum value should call onMongoDBDocumentsClick", () => {
@@ -279,7 +267,7 @@ describe("OpenActions", () => {
database.collections([collection]);
handleOpenAction(action, [database], explorer);
expect(onMongoDBDocumentsClick).toHaveBeenCalled();
expect(collection.onMongoDBDocumentsClick).toHaveBeenCalled();
});
});
@@ -293,10 +281,10 @@ describe("OpenActions", () => {
};
handleOpenAction(action, [database], explorer);
expect(onTableEntitiesClick).not.toHaveBeenCalled();
expect(collection.onTableEntitiesClick).not.toHaveBeenCalled();
database.collections([collection]);
expect(onTableEntitiesClick).toHaveBeenCalled();
expect(collection.onTableEntitiesClick).toHaveBeenCalled();
});
it("string value should call onTableEntitiesClick", () => {
@@ -309,7 +297,7 @@ describe("OpenActions", () => {
database.collections([collection]);
handleOpenAction(action, [database], explorer);
expect(onTableEntitiesClick).toHaveBeenCalled();
expect(collection.onTableEntitiesClick).toHaveBeenCalled();
});
it("enum value should call onTableEntitiesClick before collections are fetched", () => {
@@ -322,7 +310,7 @@ describe("OpenActions", () => {
database.collections([collection]);
handleOpenAction(action, [database], explorer);
expect(onTableEntitiesClick).toHaveBeenCalled();
expect(collection.onTableEntitiesClick).toHaveBeenCalled();
});
it("enum value should call onTableEntitiesClick", () => {
@@ -334,10 +322,10 @@ describe("OpenActions", () => {
};
handleOpenAction(action, [database], explorer);
expect(onTableEntitiesClick).not.toHaveBeenCalled();
expect(collection.onTableEntitiesClick).not.toHaveBeenCalled();
database.collections([collection]);
expect(onTableEntitiesClick).toHaveBeenCalled();
expect(collection.onTableEntitiesClick).toHaveBeenCalled();
});
});
@@ -351,10 +339,10 @@ describe("OpenActions", () => {
};
handleOpenAction(action, [database], explorer);
expect(onGraphDocumentsClick).not.toHaveBeenCalled();
expect(collection.onGraphDocumentsClick).not.toHaveBeenCalled();
database.collections([collection]);
expect(onGraphDocumentsClick).toHaveBeenCalled();
expect(collection.onGraphDocumentsClick).toHaveBeenCalled();
});
it("string value should call onGraphDocumentsClick", () => {
@@ -367,7 +355,7 @@ describe("OpenActions", () => {
database.collections([collection]);
handleOpenAction(action, [database], explorer);
expect(onGraphDocumentsClick).toHaveBeenCalled();
expect(collection.onGraphDocumentsClick).toHaveBeenCalled();
});
it("enum value should call onGraphDocumentsClick before collections are fetched", () => {
@@ -379,10 +367,10 @@ describe("OpenActions", () => {
};
handleOpenAction(action, [database], explorer);
expect(onGraphDocumentsClick).not.toHaveBeenCalled();
expect(collection.onGraphDocumentsClick).not.toHaveBeenCalled();
database.collections([collection]);
expect(onGraphDocumentsClick).toHaveBeenCalled();
expect(collection.onGraphDocumentsClick).toHaveBeenCalled();
});
it("enum value should call onGraphDocumentsClick", () => {
@@ -395,7 +383,7 @@ describe("OpenActions", () => {
database.collections([collection]);
handleOpenAction(action, [database], explorer);
expect(onGraphDocumentsClick).toHaveBeenCalled();
expect(collection.onGraphDocumentsClick).toHaveBeenCalled();
});
});
@@ -409,10 +397,10 @@ describe("OpenActions", () => {
};
handleOpenAction(action, [database], explorer);
expect(onNewQueryClick).not.toHaveBeenCalled();
expect(collection.onNewQueryClick).not.toHaveBeenCalled();
database.collections([collection]);
expect(onNewQueryClick).toHaveBeenCalled();
expect(collection.onNewQueryClick).toHaveBeenCalled();
});
it("string value should call onNewQueryClick", () => {
@@ -425,7 +413,7 @@ describe("OpenActions", () => {
database.collections([collection]);
handleOpenAction(action, [database], explorer);
expect(onNewQueryClick).toHaveBeenCalled();
expect(collection.onNewQueryClick).toHaveBeenCalled();
});
it("enum value should call onNewQueryClick before collections are fetched", () => {
@@ -437,10 +425,10 @@ describe("OpenActions", () => {
};
handleOpenAction(action, [database], explorer);
expect(onNewQueryClick).not.toHaveBeenCalled();
expect(collection.onNewQueryClick).not.toHaveBeenCalled();
database.collections([collection]);
expect(onNewQueryClick).toHaveBeenCalled();
expect(collection.onNewQueryClick).toHaveBeenCalled();
});
it("enum value should call onNewQueryClick", () => {
@@ -453,7 +441,7 @@ describe("OpenActions", () => {
database.collections([collection]);
handleOpenAction(action, [database], explorer);
expect(onNewQueryClick).toHaveBeenCalled();
expect(collection.onNewQueryClick).toHaveBeenCalled();
});
});
@@ -467,10 +455,10 @@ describe("OpenActions", () => {
};
handleOpenAction(action, [database], explorer);
expect(onSettingsClick).not.toHaveBeenCalled();
expect(collection.onSettingsClick).not.toHaveBeenCalled();
database.collections([collection]);
expect(onSettingsClick).toHaveBeenCalled();
expect(collection.onSettingsClick).toHaveBeenCalled();
});
it("string value should call onSettingsClick", () => {
@@ -483,7 +471,7 @@ describe("OpenActions", () => {
database.collections([collection]);
handleOpenAction(action, [database], explorer);
expect(onSettingsClick).toHaveBeenCalled();
expect(collection.onSettingsClick).toHaveBeenCalled();
});
it("enum value should call onSettingsClick before collections are fetched", () => {
@@ -495,10 +483,10 @@ describe("OpenActions", () => {
};
handleOpenAction(action, [database], explorer);
expect(onSettingsClick).not.toHaveBeenCalled();
expect(collection.onSettingsClick).not.toHaveBeenCalled();
database.collections([collection]);
expect(onSettingsClick).toHaveBeenCalled();
expect(collection.onSettingsClick).toHaveBeenCalled();
});
it("enum value should call onSettingsClick", () => {
@@ -511,7 +499,7 @@ describe("OpenActions", () => {
database.collections([collection]);
handleOpenAction(action, [database], explorer);
expect(onSettingsClick).toHaveBeenCalled();
expect(collection.onSettingsClick).toHaveBeenCalled();
});
});
});

View File

@@ -2,11 +2,12 @@
import * as ViewModels from "../Contracts/ViewModels";
import { ActionContracts } from "../Contracts/ExplorerContracts";
import Explorer from "./Explorer";
export function handleOpenAction(
action: ActionContracts.DataExplorerAction,
databases: ViewModels.Database[],
explorer: ViewModels.Explorer
explorer: Explorer
): boolean {
if (
action.actionType === ActionContracts.ActionType.OpenCollectionTab ||
@@ -126,7 +127,7 @@ function openCollectionTab(
}
}
function openPane(action: ActionContracts.OpenPane, explorer: ViewModels.Explorer) {
function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
if (
action.paneKind === ActionContracts.PaneKind.AddCollection ||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection]
@@ -154,7 +155,7 @@ function openPane(action: ActionContracts.OpenPane, explorer: ViewModels.Explore
}
}
function openFile(action: ActionContracts.OpenSampleNotebook, explorer: ViewModels.Explorer) {
function openFile(action: ActionContracts.OpenSampleNotebook, explorer: Explorer) {
explorer.handleOpenFileAction(decodeURIComponent(action.path));
}

View File

@@ -1,873 +0,0 @@
import * as DataModels from "../../src/Contracts/DataModels";
import * as ko from "knockout";
import * as ViewModels from "../../src/Contracts/ViewModels";
import DocumentClientUtilityBase from "../Common/DocumentClientUtilityBase";
import Q from "q";
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
import { CassandraTableKey, CassandraTableKeys, TableDataClient } from "../../src/Explorer/Tables/TableDataClient";
import { ConsoleData } from "../../src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { MostRecentActivity } from "./MostRecentActivity/MostRecentActivity";
import { NotebookContentItem } from "./Notebook/NotebookContentItem";
import { PlatformType } from "../../src/PlatformType";
import { QuerySelectPane } from "../../src/Explorer/Panes/Tables/QuerySelectPane";
import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane";
import { Splitter } from "../../src/Common/Splitter";
import { StringInputPane } from "./Panes/StringInputPane";
import { TableColumnOptionsPane } from "../../src/Explorer/Panes/Tables/TableColumnOptionsPane";
import { TextFieldProps } from "./Controls/DialogReactComponent/DialogComponent";
import { UploadDetails } from "../workers/upload/definitions";
import { UploadFilePane } from "./Panes/UploadFilePane";
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
import { Versions } from "../../src/Contracts/ExplorerContracts";
import { CollectionCreationDefaults } from "../Shared/Constants";
import { IGalleryItem } from "../Juno/JunoClient";
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
export class ExplorerStub implements ViewModels.Explorer {
public flight: ko.Observable<string>;
public addCollectionText: ko.Observable<string>;
public hasAutoPilotV2FeatureFlag: ko.Computed<boolean>;
public addDatabaseText: ko.Observable<string>;
public collectionTitle: ko.Observable<string>;
public deleteCollectionText: ko.Observable<string>;
public deleteDatabaseText: ko.Observable<string>;
public collectionTreeNodeAltText: ko.Observable<string>;
public refreshTreeTitle: ko.Observable<string>;
public collapsedResourceTreeWidth: number;
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = CollectionCreationDefaults;
public hasWriteAccess: ko.Observable<boolean> = ko.observable<boolean>(false);
public databaseAccount: ko.Observable<ViewModels.DatabaseAccount>;
public subscriptionType: ko.Observable<ViewModels.SubscriptionType>;
public quotaId: ko.Observable<string>;
public defaultExperience: ko.Observable<string>;
public isPreferredApiDocumentDB: ko.Computed<boolean>;
public isPreferredApiCassandra: ko.Computed<boolean>;
public isPreferredApiMongoDB: ko.Computed<boolean>;
public isPreferredApiGraph: ko.Computed<boolean>;
public isPreferredApiTable: ko.Computed<boolean>;
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
public isEmulator: boolean;
public isAccountReady: ko.Observable<boolean>;
public canSaveQueries: ko.Computed<boolean>;
public features: ko.Observable<any>;
public serverId: ko.Observable<string>;
public extensionEndpoint: ko.Observable<string> = ko.observable<string>(undefined);
public armEndpoint: ko.Observable<string>;
public isTryCosmosDBSubscription: ko.Observable<boolean>;
public documentClientUtility: DocumentClientUtilityBase;
public notificationsClient: ViewModels.NotificationsClient;
public queriesClient: ViewModels.QueriesClient;
public tableDataClient: TableDataClient;
public splitter: Splitter;
public notificationConsoleData: ko.ObservableArray<ConsoleData>;
public isNotificationConsoleExpanded: ko.Observable<boolean>;
public contextPanes: ViewModels.ContextualPane[];
public databases: ko.ObservableArray<ViewModels.Database>;
public nonSystemDatabases: ko.Computed<ViewModels.Database[]>;
public selectedDatabaseId: ko.Computed<string>;
public selectedCollectionId: ko.Computed<string>;
public isLeftPaneExpanded: ko.Observable<boolean>;
public selectedNode: ko.Observable<ViewModels.TreeNode>;
public isRefreshingExplorer: ko.Observable<boolean>;
public openedTabs: ko.ObservableArray<ViewModels.Tab>;
public isTabsContentExpanded: ko.Observable<boolean>;
public addCollectionPane: ViewModels.AddCollectionPane;
public addDatabasePane: ViewModels.AddDatabasePane;
public deleteCollectionConfirmationPane: ViewModels.DeleteCollectionConfirmationPane;
public deleteDatabaseConfirmationPane: ViewModels.DeleteDatabaseConfirmationPane;
public graphStylingPane: ViewModels.GraphStylingPane;
public addTableEntityPane: ViewModels.AddTableEntityPane;
public editTableEntityPane: ViewModels.EditTableEntityPane;
public tableColumnOptionsPane: TableColumnOptionsPane;
public querySelectPane: QuerySelectPane;
public newVertexPane: ViewModels.NewVertexPane;
public cassandraAddCollectionPane: ViewModels.CassandraAddCollectionPane;
public renewAdHocAccessPane: ViewModels.RenewAdHocAccessPane;
public renewExplorerShareAccess: (explorer: ViewModels.Explorer, token: string) => Q.Promise<void>;
public settingsPane: ViewModels.SettingsPane;
public executeSprocParamsPane: ViewModels.ExecuteSprocParamsPane;
public uploadItemsPane: ViewModels.UploadItemsPane;
public uploadItemsPaneAdapter: UploadItemsPaneAdapter;
public loadQueryPane: ViewModels.LoadQueryPane;
public saveQueryPane: ViewModels.ContextualPane;
public browseQueriesPane: ViewModels.BrowseQueriesPane;
public uploadFilePane: UploadFilePane;
public stringInputPane: StringInputPane;
public setupNotebooksPane: SetupNotebooksPane;
public setupSparkClusterPane: ViewModels.ContextualPane;
public manageSparkClusterPane: ViewModels.ContextualPane;
public isGalleryPublishEnabled: ko.Computed<boolean>;
public isGitHubPaneEnabled: ko.Observable<boolean>;
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
public isRightPanelV2Enabled: ko.Computed<boolean>;
public canExceedMaximumValue: ko.Computed<boolean>;
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
public parentFrameDataExplorerVersion: ko.Observable<string> = ko.observable<string>(Versions.DataExplorer);
public activeTab: ko.Observable<ViewModels.Tab>;
public mostRecentActivity: MostRecentActivity;
public isNotebookEnabled: ko.Observable<boolean>;
public isSparkEnabled: ko.Observable<boolean>;
public isNotebooksEnabledForAccount: ko.Observable<boolean>;
public isSparkEnabledForAccount: ko.Observable<boolean>;
public arcadiaToken: ko.Observable<string>;
public notebookWorkspaceManager: ViewModels.NotebookWorkspaceManager;
public sparkClusterManager: ViewModels.SparkClusterManager;
public notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>;
public sparkClusterConnectionInfo: ko.Observable<DataModels.SparkClusterConnectionInfo>;
public libraryManagePane: ViewModels.ContextualPane;
public clusterLibraryPane: ViewModels.ContextualPane;
public gitHubReposPane: ViewModels.ContextualPane;
public publishNotebookPaneAdapter: ReactAdapter;
public arcadiaWorkspaces: ko.ObservableArray<ArcadiaWorkspaceItem>;
public hasStorageAnalyticsAfecFeature: ko.Observable<boolean>;
public isSynapseLinkUpdating: ko.Observable<boolean>;
public isNotebookTabActive: ko.Computed<boolean>;
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
public notebookManager?: any;
public openGallery: (notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean) => void;
public openNotebookViewer: (notebookUrl: string) => void;
public resourceTokenDatabaseId: ko.Observable<string>;
public resourceTokenCollectionId: ko.Observable<string>;
public resourceTokenCollection: ko.Observable<ViewModels.CollectionBase>;
public resourceTokenPartitionKey: ko.Observable<string>;
public isAuthWithResourceToken: ko.Observable<boolean>;
public isResourceTokenCollectionNodeSelected: ko.Computed<boolean>;
private _featureEnabledReturnValue: boolean;
constructor(options?: any) {
options = options || {};
this._featureEnabledReturnValue = options.featureEnabledReturnValue || false;
this.isSynapseLinkUpdating = ko.observable<boolean>(options.isSynapseLinkUpdating || false);
}
public openEnableSynapseLinkDialog() {
throw new Error("Not implemented");
}
public createWorkspace(): Promise<string> {
throw new Error("Not implemented");
}
public createSparkPool(workspaceId: string): Promise<string> {
throw new Error("Not implemented");
}
public isDatabaseNodeOrNoneSelected(): boolean {
throw new Error("Not implemented");
}
public isDatabaseNodeSelected(): boolean {
throw new Error("Not implemented");
}
public isNodeKindSelected(nodeKind: string): boolean {
throw new Error("Not implemented");
}
public isNoneSelected(): boolean {
throw new Error("Not implemented");
}
public isFeatureEnabled(feature: string): boolean {
return this._featureEnabledReturnValue;
}
public isSelectedDatabaseShared(): boolean {
throw new Error("Not implemented");
}
public logConsoleData(consoleData: ConsoleData): void {
throw new Error("Not implemented");
}
public deleteInProgressConsoleDataWithId(id: string): void {
throw new Error("Not implemented");
}
public toggleLeftPaneExpanded() {
throw new Error("Not implemented");
}
public refreshAllDatabases(): Q.Promise<any> {
throw new Error("Not implemented");
}
public refreshDatabaseForResourceToken(): Q.Promise<void> {
throw new Error("Note impplemented");
}
public onRefreshDatabasesKeyPress = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onRefreshResourcesClick = (source: any, event: MouseEvent): boolean => {
throw new Error("Not implemented");
};
public toggleLeftPaneExpandedKeyPress = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
// Facade
public provideFeedbackEmail = () => {
throw new Error("Not implemented");
};
public handleMessage(event: MessageEvent) {
throw new Error("Not implemented");
}
public findSelectedDatabase(): ViewModels.Database {
throw new Error("Not implemented");
}
public findDatabaseWithId(databaseId: string): ViewModels.Database {
throw new Error("Not implemented");
}
public isLastDatabase(): boolean {
throw new Error("Not implemented");
}
public isLastNonEmptyDatabase(): boolean {
throw new Error("Not implemented");
}
public initDataExplorerWithFrameInputs(inputs: ViewModels.DataExplorerInputsFrame): Q.Promise<void> {
throw new Error("Not implemented");
}
public findSelectedCollection(): ViewModels.Collection {
throw new Error("Not implemented");
}
public findCollection(rid: string): ViewModels.Collection {
throw new Error("Not implemented");
}
public isLastCollection(): boolean {
throw new Error("Not implemented");
}
public findActiveTab(): ViewModels.Tab {
throw new Error("Not implemented");
}
public findSelectedStoredProcedure(): ViewModels.StoredProcedure {
throw new Error("Not implemented");
}
public findSelectedUDF(): ViewModels.UserDefinedFunction {
throw new Error("Not implemented");
}
public findSelectedTrigger(): ViewModels.Trigger {
throw new Error("Not implemented");
}
public generateSharedAccessData(): void {
throw new Error("Not implemented");
}
public displayConnectExplorerForm(): void {
throw new Error("Not implemented");
}
public displayContextSwitchPromptForConnectionString(connectionString: string): void {
throw new Error("Not implemented");
}
public hideConnectExplorerForm(): void {
throw new Error("Not implemented");
}
public displayGuestAccessTokenRenewalPrompt(): void {
throw new Error("Not implemented");
}
public expandConsole(): void {
throw new Error("Not implemented");
}
public collapseConsole(): void {
throw new Error("Not implemented");
}
public rebindDocumentClientUtility(documentClientUtility: any) {
throw new Error("Not implemented");
}
public renewShareAccess(token: string): Q.Promise<void> {
throw new Error("Not implemented");
}
public closeAllTabsForResource(resourceId: string): void {
throw new Error("Not implemented");
}
public getPlatformType(): PlatformType {
throw new Error("Not implemented");
}
public isRunningOnNationalCloud(): boolean {
throw new Error("Not implemented");
}
public isConnectExplorerVisible(): boolean {
return false;
}
public closeAllPanes(): void {
// return for now so tests dont break
// TODO: implement once we start testing pane close
return;
}
public onUpdateTabsButtons(buttons: ViewModels.NavbarButtonConfig[]): void {
throw new Error("Not implemented");
}
public importAndOpen(path: string): Promise<boolean> {
throw new Error("Not implemented");
}
public importAndOpenContent(name: string, content: string): Promise<boolean> {
throw new Error("Not implemented");
}
public publishNotebook(name: string, content: string): void {
throw new Error("Not implemented");
}
public async openNotebook(notebookContentItem: NotebookContentItem): Promise<boolean> {
throw new Error("Not implemented");
}
public deleteNotebookFile(item: NotebookContentItem): Promise<void> {
throw new Error("Not implemented");
}
public onCreateDirectory(parent: NotebookContentItem): Q.Promise<NotebookContentItem> {
throw new Error("Not implemented");
}
public onNewNotebookClicked(parent?: NotebookContentItem): void {
throw new Error("Not implemented");
}
public openNotebookTerminal(): void {
throw new Error("Not implemented");
}
public resetNotebookWorkspace(): void {
throw new Error("Not implemented");
}
public onNewCollectionClicked(): void {
throw new Error("Not implemented");
}
public onUploadToNotebookServerClicked(parent?: NotebookContentItem): void {
throw new Error("Not implemented");
}
public renameNotebook(notebookFile: NotebookContentItem): Q.Promise<NotebookContentItem> {
throw new Error("Not implemented");
}
public readFile(notebookFile: NotebookContentItem): Promise<string> {
throw new Error("Not implemented");
}
public downloadFile(notebookFile: NotebookContentItem): Promise<void> {
throw new Error("Not implemented");
}
public initNotebooks(databaseAccount: DataModels.DatabaseAccount): Promise<void> {
throw new Error("Not implemented");
}
public showOkModalDialog(title: string, msg: string): void {
throw new Error("Not implemented");
}
public showOkCancelModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void
): void {
throw new Error("Not implemented");
}
public showOkCancelTextFieldModalDialog(
title: string,
msg: string,
okLabel: string,
onOk: () => void,
cancelLabel: string,
onCancel: () => void,
textFieldProps: TextFieldProps,
isPrimaryButtonDisabled?: boolean
): void {
throw new Error("Not implemented");
}
public deleteCluster(): void {
throw new Error("Not implemented");
}
public async openSparkMasterTab(): Promise<void> {
throw new Error("Not implemented");
}
public createNotebookContentItemFile(name: string, filepath: string): NotebookContentItem {
throw new Error("Not implemented");
}
public closeNotebookTab(filepath: string): void {
throw new Error("Not implemented");
}
public refreshContentItem(item: NotebookContentItem): Promise<void> {
throw new Error("Not implemented");
}
public getNotebookBasePath(): string {
throw new Error("Not implemented");
}
public handleOpenFileAction(): Promise<void> {
throw new Error("Not implemented");
}
}
export class DatabaseStub implements ViewModels.Database {
public nodeKind: string;
public container: ViewModels.Explorer;
public self: string;
public rid: string;
public id: ko.Observable<string>;
public collections: ko.ObservableArray<ViewModels.Collection>;
public isDatabaseExpanded: ko.Observable<boolean>;
public isDatabaseShared: ko.Computed<boolean>;
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
public offer: ko.Observable<DataModels.Offer>;
constructor(options?: any) {
this.nodeKind = options.nodeKind;
this.container = options.container;
this.self = options.self;
this.rid = options.rid;
this.id = options.id;
this.collections = options.collections;
this.isDatabaseExpanded = options.isDatabaseExpanded;
this.offer = options.offer;
this.selectedSubnodeKind = options.selectedSubnodeKind;
}
public onKeyPress = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onKeyDown = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onMenuKeyDown = (source: any, event: KeyboardEvent): boolean => {
throw new Error("Not implemented");
};
public onDeleteDatabaseContextMenuClick(source: ViewModels.Database, event: MouseEvent | KeyboardEvent) {
throw new Error("Not implemented");
}
public selectDatabase() {
throw new Error("Not implemented");
}
public expandCollapseDatabase() {
throw new Error("Not implemented");
}
public expandDatabase() {
throw new Error("Not implemented");
}
public collapseDatabase() {
throw new Error("Not implemented");
}
public loadCollections(): Q.Promise<void> {
throw new Error("Not implemented");
}
public findCollectionWithId(collectionId: string): ViewModels.Collection {
throw new Error("Not implemented");
}
public openAddCollection(database: ViewModels.Database, event: MouseEvent) {
throw new Error("Not implemented");
}
public refreshTabSelectedState(): void {
throw new Error("Not implemented");
}
public readSettings() {
throw new Error("Not implemented");
}
public onSettingsClick(): void {
throw new Error("Not implemented");
}
}
export class CollectionStub implements ViewModels.Collection {
public nodeKind: string;
public container: ViewModels.Explorer;
public rawDataModel: DataModels.Collection;
public self: string;
public rid: string;
public databaseId: string;
public partitionKey: DataModels.PartitionKey;
public partitionKeyPropertyHeader: string;
public partitionKeyProperty: string;
public id: ko.Observable<string>;
public defaultTtl: ko.Observable<number>;
public analyticalStorageTtl: ko.Observable<number>;
public indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
public uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
public quotaInfo: ko.Observable<DataModels.CollectionQuotaInfo>;
public offer: ko.Observable<DataModels.Offer>;
public partitions: ko.Computed<number>;
public throughput: ko.Computed<number>;
public cassandraKeys: CassandraTableKeys;
public cassandraSchema: CassandraTableKey[];
public documentIds: ko.ObservableArray<ViewModels.DocumentId>;
public children: ko.ObservableArray<ViewModels.TreeNode>;
public storedProcedures: ko.Computed<ViewModels.StoredProcedure[]>;
public userDefinedFunctions: ko.Computed<ViewModels.UserDefinedFunction[]>;
public triggers: ko.Computed<ViewModels.Trigger[]>;
public showStoredProcedures: ko.Observable<boolean>;
public showTriggers: ko.Observable<boolean>;
public showUserDefinedFunctions: ko.Observable<boolean>;
public selectedDocumentContent: ViewModels.Editable<any>;
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
public focusedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
public isCollectionExpanded: ko.Observable<boolean>;
public isStoredProceduresExpanded: ko.Observable<boolean>;
public isUserDefinedFunctionsExpanded: ko.Observable<boolean>;
public isTriggersExpanded: ko.Observable<boolean>;
public documentsFocused: ko.Observable<boolean>;
public settingsFocused: ko.Observable<boolean>;
public storedProceduresFocused: ko.Observable<boolean>;
public userDefinedFunctionsFocused: ko.Observable<boolean>;
public triggersFocused: ko.Observable<boolean>;
public conflictResolutionPolicy: ko.Observable<DataModels.ConflictResolutionPolicy>;
public changeFeedPolicy: ko.Observable<DataModels.ChangeFeedPolicy>;
public geospatialConfig: ko.Observable<DataModels.GeospatialConfig>;
constructor(options: any) {
this.nodeKind = options.nodeKind;
this.container = options.container;
this.self = options.self;
this.rid = options.rid;
this.databaseId = options.databaseId;
this.partitionKey = options.partitionKey;
this.partitionKeyPropertyHeader = options.partitionKeyPropertyHeader;
this.partitionKeyProperty = options.partitionKeyProperty;
this.id = options.id;
this.defaultTtl = options.defaultTtl;
this.analyticalStorageTtl = options.analyticalStorageTtl;
this.indexingPolicy = options.indexingPolicy;
this.uniqueKeyPolicy = options.uniqueKeyPolicy;
this.quotaInfo = options.quotaInfo;
this.offer = options.offer;
this.partitions = options.partitions;
this.throughput = options.throughput;
this.cassandraKeys = options.cassandraKeys;
this.cassandraSchema = options.cassandraSchema;
this.documentIds = options.documentIds;
this.children = options.children;
this.storedProcedures = options.storedProcedures;
this.userDefinedFunctions = options.userDefinedFunctions;
this.triggers = options.triggers;
this.showStoredProcedures = options.showStoredProcedures;
this.showTriggers = options.showTriggers;
this.showUserDefinedFunctions = options.showUserDefinedFunctions;
this.selectedDocumentContent = options.selectedDocumentContent;
this.selectedSubnodeKind = options.selectedSubnodeKind;
this.focusedSubnodeKind = options.focusedSubnodeKind;
this.isCollectionExpanded = options.isCollectionExpanded;
this.isStoredProceduresExpanded = options.isStoredProceduresExpanded;
this.isUserDefinedFunctionsExpanded = options.isUserDefinedFunctionsExpanded;
this.isTriggersExpanded = options.isTriggersExpanded;
this.documentsFocused = options.documentsFocused;
this.settingsFocused = options.settingsFocused;
this.storedProceduresFocused = options.storedProceduresFocused;
this.userDefinedFunctionsFocused = options.userDefinedFunctionsFocused;
this.triggersFocused = options.triggersFocused;
}
public expandCollapseCollection() {
throw new Error("Not implemented");
}
public collapseCollection() {
throw new Error("Not implemented");
}
public expandCollection(): Q.Promise<void> {
throw new Error("Not implemented");
}
public onDocumentDBDocumentsClick() {
throw new Error("onDocumentDBDocumentsClick");
}
public onTableEntitiesClick() {
throw new Error("Not implemented");
}
public onGraphDocumentsClick() {
throw new Error("Not implemented");
}
public onMongoDBDocumentsClick = () => {
throw new Error("Not implemented");
};
public openTab = () => {
throw new Error("Not implemented");
};
public onSettingsClick() {
throw new Error("Not implemented");
}
public onConflictsClick() {
throw new Error("Not implemented");
}
public readSettings(): Q.Promise<void> {
throw new Error("Not implemented");
}
public onNewQueryClick(source: any, event: MouseEvent, queryText?: string) {
throw new Error("Not implemented");
}
public onNewMongoQueryClick(source: any, event: MouseEvent, queryText?: string) {
throw new Error("Not implemented");
}
public onNewGraphClick() {
throw new Error("Not implemented");
}
public onNewMongoShellClick() {
throw new Error("Not implemented");
}
public onNewStoredProcedureClick(source: ViewModels.Collection, event: MouseEvent) {
throw new Error("Not implemented");
}
public onNewUserDefinedFunctionClick(source: ViewModels.Collection, event: MouseEvent) {
throw new Error("Not implemented");
}
public onNewTriggerClick(source: ViewModels.Collection, event: MouseEvent) {
throw new Error("Not implemented");
}
public createStoredProcedureNode(data: DataModels.StoredProcedure): ViewModels.StoredProcedure {
throw new Error("Not implemented");
}
public createUserDefinedFunctionNode(data: DataModels.UserDefinedFunction): ViewModels.UserDefinedFunction {
throw new Error("Not implemented");
}
public createTriggerNode(data: DataModels.Trigger): ViewModels.Trigger {
throw new Error("Not implemented");
}
public expandCollapseStoredProcedures() {
throw new Error("Not implemented");
}
public expandStoredProcedures() {
throw new Error("Not implemented");
}
public collapseStoredProcedures() {
throw new Error("Not implemented");
}
public expandCollapseUserDefinedFunctions() {
throw new Error("Not implemented");
}
public expandUserDefinedFunctions() {
throw new Error("Not implemented");
}
public collapseUserDefinedFunctions() {
throw new Error("Not implemented");
}
public expandCollapseTriggers() {
throw new Error("Not implemented");
}
public expandTriggers() {
throw new Error("Not implemented");
}
public collapseTriggers() {
throw new Error("Not implemented");
}
public loadStoredProcedures(): Q.Promise<any> {
throw new Error("Not implemented");
}
public loadUserDefinedFunctions(): Q.Promise<any> {
throw new Error("Not implemented");
}
public loadTriggers(): Q.Promise<any> {
throw new Error("Not implemented");
}
public onDragOver(source: ViewModels.Collection, event: { originalEvent: DragEvent }) {
throw new Error("Not implemented");
}
public onDrop(source: ViewModels.Collection, event: { originalEvent: DragEvent }) {
throw new Error("Not implemented");
}
public isCollectionNodeSelected(): boolean {
throw new Error("Not implemented");
}
public isSubNodeSelected(nodeKind: ViewModels.CollectionTabKind): boolean {
throw new Error("Not implemented");
}
public onDeleteCollectionContextMenuClick(source: ViewModels.Collection, event: MouseEvent | KeyboardEvent) {
throw new Error("Not implemented");
}
public findStoredProcedureWithId(sprocId: string): ViewModels.StoredProcedure {
throw new Error("Not implemented");
}
public findTriggerWithId(triggerId: string): ViewModels.Trigger {
throw new Error("Not implemented");
}
public findUserDefinedFunctionWithId(userDefinedFunctionId: string): ViewModels.UserDefinedFunction {
throw new Error("Not implemented");
}
public uploadFiles = (fileList: FileList): Q.Promise<UploadDetails> => {
throw new Error("Not implemented");
};
public refreshActiveTab = (): void => {
throw new Error("Not implemented");
};
public getLabel(): string {
throw new Error("Not implemented");
}
public getDatabase(): ViewModels.Database {
throw new Error("Not implemented");
}
}
class ContextualPaneStub implements ViewModels.ContextualPane {
public documentClientUtility: DocumentClientUtilityBase;
public formErrors: ko.Observable<string>;
public formErrorsDetails: ko.Observable<string>;
public id: string;
public title: ko.Observable<string>;
public visible: ko.Observable<boolean>;
public firstFieldHasFocus: ko.Observable<boolean>;
public isExecuting: ko.Observable<boolean>;
public submit() {
throw new Error("Not implemented");
}
public cancel() {
throw new Error("Not implemented");
}
public open() {
throw new Error("Not implemented");
}
public close() {
throw new Error("Not implemented");
}
public hideErrorDetails() {
throw new Error("Not implemented");
}
public resetData() {
throw new Error("Not implemented");
}
public showErrorDetails() {
throw new Error("Not implemented");
}
public onCloseKeyPress(source: any, event: KeyboardEvent): void {
throw new Error("Not implemented");
}
public onPaneKeyDown(source: any, event: KeyboardEvent): boolean {
throw new Error("Not implemented");
}
}
export class AddCollectionPaneStub extends ContextualPaneStub implements ViewModels.AddCollectionPane {
public collectionIdTitle: ko.Observable<string>;
public databaseId: ko.Observable<string>;
public partitionKey: ko.Observable<string>;
public storage: ko.Observable<string>;
public throughputSinglePartition: ko.Observable<number>;
public throughputMultiPartition: ko.Observable<number>;
public collectionMaxSharedThroughputTitle: ko.Observable<string>;
public collectionWithThroughputInSharedTitle: ko.Observable<string>;
public onEnableSynapseLinkButtonClicked() {
throw new Error("Not implemented");
}
public onStorageOptionsKeyDown(source: any, event: KeyboardEvent): boolean {
throw new Error("Not implemented");
}
public onRupmOptionsKeyDown(source: any, event: KeyboardEvent): void {
throw new Error("Not implemented");
}
}
export class AddDatabasePaneStub extends ContextualPaneStub implements ViewModels.AddDatabasePane {}
export class CassandraAddCollectionPane extends ContextualPaneStub implements ViewModels.CassandraAddCollectionPane {
public createTableQuery: ko.Observable<string>;
public keyspaceId: ko.Observable<string>;
public userTableQuery: ko.Observable<string>;
}

View File

@@ -56,14 +56,13 @@
<!-- Add collection errors - End -->
<!-- upsell message - start -->
<div class="infoBoxContainer" aria-live="assertive" data-bind="visible: formErrors && !formErrors()">
<div class="infoBoxContainer" aria-live="assertive" data-bind="visible: showUpsellMessage && showUpsellMessage() && formErrors && !formErrors()">
<div class="infoBoxContent">
<span><img class="infoBoxIcon" src="/info_color.svg" alt="Promo"></span>
<span class="infoBoxDetails">
<span class="infoBoxMessage" data-bind="text: upsellMessage, attr: { title: upsellMessage }"></span>
<a class="underlinedLink" id="linkAddCollection" data-bind="attr: { 'aria-label': upsellMessageAriaLabel }"
target="_blank" href="https://aka.ms/azure-cosmos-db-pricing" tabindex="0">
More details</a>
<a class="underlinedLink" id="linkAddCollection" data-bind="text: upsellAnchorText, attr: { 'href': upsellAnchorUrl, 'aria-label': upsellMessageAriaLabel }"
target="_blank" href="" tabindex="0"></a>
</span>
</div>
</div>
@@ -113,7 +112,9 @@
<datalist id="databasesList" data-bind="foreach: databaseIds" data-bind="visible: databaseCreateNew">
<option data-bind="value: $data">
</datalist>
<!-- Database provisioned throughput - Start -->
<!-- ko if: canConfigureThroughput -->
<div class="databaseProvision" aria-label="New database provision support"
data-bind="visible: databaseCreateNew">
<input tabindex="0" type="checkbox" data-test="addCollectionPane-databaseSharedThroughput"
@@ -188,6 +189,7 @@
</throughput-input>
</div>
<!-- /ko -->
<!-- /ko -->
<!-- Database provisioned throughput - End -->
</div>
@@ -415,7 +417,10 @@
more</a></p>
</div>
<!-- large parition key - end -->
<!-- Provision collection throughput checkox - start -->
<!-- Provision collection throughput - start -->
<!-- ko if: canConfigureThroughput -->
<!-- Provision collection throughput checkbox - start -->
<div class="pkPadding" data-bind="visible: databaseHasSharedOffer() && !databaseCreateNew()">
<input type="checkbox" id="collectionSharedThroughput"
data-bind="checked: collectionWithThroughputInShared, attr: {title:collectionWithThroughputInSharedTitle}" />
@@ -435,7 +440,8 @@
level.</span>
</span>
</div>
<!-- Provision collection throughput checkox - end -->
<!-- Provision collection throughput checkbox - end -->
<!-- Provision collection throughput spinner - start -->
<!-- ko if: hasAutoPilotV2FeatureFlag && !hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: displayCollectionThroughput" data-test="addCollection-displayCollectionThroughput">
@@ -468,6 +474,7 @@
</throughput-input-autopilot-v3>
</div>
<!-- /ko -->
<!-- ko if: hasAutoPilotV2FeatureFlag && hasAutoPilotV2FeatureFlag() -->
<div data-bind="visible: displayCollectionThroughput" data-test="addCollection-displayCollectionThroughput">
<!-- 3 -->
@@ -500,8 +507,10 @@
</throughput-input>
</div>
<!-- /ko -->
<!-- Provision collection throughput spinner - end -->
<!-- /ko -->
<!-- Provision collection throughput - end -->
<!-- Enable analytical storage - start -->
<div class="enableAnalyticalStorage pkPadding" aria-label="Enable Analytical Store"
data-bind="visible: isSynapseLinkSupported">

View File

@@ -1,25 +1,46 @@
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import AddCollectionPane from "./AddCollectionPane";
import Explorer from "../Explorer";
import ko from "knockout";
import { AutopilotTier } from "../../Contracts/DataModels";
import { AutopilotTier, DatabaseAccount } from "../../Contracts/DataModels";
describe("Add Collection Pane", () => {
describe("isValid()", () => {
let explorer: ViewModels.Explorer;
const mockDatabaseAccount: ViewModels.DatabaseAccount = {
let explorer: Explorer;
const mockDatabaseAccount: DatabaseAccount = {
id: "mock",
kind: "DocumentDB",
location: "",
name: "mock",
properties: undefined,
properties: {
documentEndpoint: "",
cassandraEndpoint: "",
gremlinEndpoint: "",
tableEndpoint: "",
enableFreeTier: false
},
type: undefined,
tags: []
};
const mockFreeTierDatabaseAccount: DatabaseAccount = {
id: "mock",
kind: "DocumentDB",
location: "",
name: "mock",
properties: {
documentEndpoint: "",
cassandraEndpoint: "",
gremlinEndpoint: "",
tableEndpoint: "",
enableFreeTier: true
},
type: undefined,
tags: []
};
beforeEach(() => {
explorer = new Explorer({ documentClientUtility: null, notificationsClient: null, isEmulator: false });
explorer = new Explorer({ notificationsClient: null, isEmulator: false });
explorer.hasAutoPilotV2FeatureFlag = ko.computed<boolean>(() => true);
});
@@ -68,5 +89,23 @@ describe("Add Collection Pane", () => {
addCollectionPane.partitionKey("/label");
expect(addCollectionPane.isValid()).toBe(true);
});
it("should display free tier text in upsell messaging", () => {
explorer.databaseAccount(mockFreeTierDatabaseAccount);
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
expect(addCollectionPane.isFreeTierAccount()).toBe(true);
expect(addCollectionPane.upsellMessage()).toContain("With free tier discount");
expect(addCollectionPane.upsellAnchorUrl()).toBe(Constants.Urls.freeTierInformation);
expect(addCollectionPane.upsellAnchorText()).toBe("Learn more");
});
it("should display standard texr in upsell messaging", () => {
explorer.databaseAccount(mockDatabaseAccount);
const addCollectionPane = explorer.addCollectionPane as AddCollectionPane;
expect(addCollectionPane.isFreeTierAccount()).toBe(false);
expect(addCollectionPane.upsellMessage()).toContain("Start at");
expect(addCollectionPane.upsellAnchorUrl()).toBe(Constants.Urls.cosmosPricing);
expect(addCollectionPane.upsellAnchorText()).toBe("More details");
});
});
});

View File

@@ -13,15 +13,22 @@ import EnvironmentUtility from "../../Common/EnvironmentUtility";
import Q from "q";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { config, Platform } from "../../Config";
import { configContext, Platform } from "../../ConfigContext";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { CosmosClient } from "../../Common/CosmosClient";
import { createMongoCollectionWithARM, createMongoCollectionWithProxy } from "../../Common/MongoProxyClient";
import { DynamicListItem } from "../Controls/DynamicList/DynamicListComponent";
import { HashMap } from "../../Common/HashMap";
import { PlatformType } from "../../PlatformType";
import { refreshCachedResources, getOrCreateDatabaseAndCollection } from "../../Common/DocumentClientUtilityBase";
import { userContext } from "../../UserContext";
export default class AddCollectionPane extends ContextualPaneBase implements ViewModels.AddCollectionPane {
export interface AddCollectionPaneOptions extends ViewModels.PaneOptions {
isPreferredApiTable: ko.Computed<boolean>;
databaseId?: string;
databaseSelfLink?: string;
}
export default class AddCollectionPane extends ContextualPaneBase {
public defaultExperience: ko.Computed<string>;
public databaseIds: ko.ObservableArray<string>;
public collectionId: ko.Observable<string>;
@@ -69,6 +76,8 @@ export default class AddCollectionPane extends ContextualPaneBase implements Vie
public uniqueKeysPlaceholder: ko.Computed<string>;
public upsellMessage: ko.PureComputed<string>;
public upsellMessageAriaLabel: ko.PureComputed<string>;
public upsellAnchorUrl: ko.PureComputed<string>;
public upsellAnchorText: ko.PureComputed<string>;
public debugstring: ko.Computed<string>;
public displayCollectionThroughput: ko.Computed<boolean>;
public isAutoPilotSelected: ko.Observable<boolean>;
@@ -91,15 +100,19 @@ export default class AddCollectionPane extends ContextualPaneBase implements Vie
public canExceedMaximumValue: ko.PureComputed<boolean>;
public hasAutoPilotV2FeatureFlag: ko.PureComputed<boolean>;
public ruToolTipText: ko.Computed<string>;
public canConfigureThroughput: ko.PureComputed<boolean>;
public showUpsellMessage: ko.PureComputed<boolean>;
private _databaseOffers: HashMap<DataModels.Offer>;
private _isSynapseLinkEnabled: ko.Computed<boolean>;
constructor(options: ViewModels.AddCollectionPaneOptions) {
constructor(options: AddCollectionPaneOptions) {
super(options);
this._databaseOffers = new HashMap<DataModels.Offer>();
this.hasAutoPilotV2FeatureFlag = ko.pureComputed(() => this.container.hasAutoPilotV2FeatureFlag());
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText(this.hasAutoPilotV2FeatureFlag()));
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.formWarnings = ko.observable<string>();
this.collectionId = ko.observable<string>();
this.databaseId = ko.observable<string>();
@@ -508,11 +521,19 @@ export default class AddCollectionPane extends ContextualPaneBase implements Vie
});
this.upsellMessage = ko.pureComputed<string>(() => {
return PricingUtils.getUpsellMessage(this.container.serverId());
return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount());
});
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
return `${this.upsellMessage()}. Click for more details`;
return `${this.upsellMessage()}. Click ${this.isFreeTierAccount() ? "to learn more" : "for more details"}`;
});
this.upsellAnchorUrl = ko.pureComputed<string>(() => {
return this.isFreeTierAccount() ? Constants.Urls.freeTierInformation : Constants.Urls.cosmosPricing;
});
this.upsellAnchorText = ko.pureComputed<string>(() => {
return this.isFreeTierAccount() ? "Learn more" : "More details";
});
this.displayCollectionThroughput = ko.computed<boolean>(() => {
@@ -578,9 +599,14 @@ export default class AddCollectionPane extends ContextualPaneBase implements Vie
});
this.isSynapseLinkSupported = ko.computed(() => {
if (config.platform === Platform.Emulator) {
if (configContext.platform === Platform.Emulator) {
return false;
}
if (this.container.isServerlessEnabled()) {
return false;
}
if (this.container.isPreferredApiDocumentDB()) {
return true;
}
@@ -599,7 +625,7 @@ export default class AddCollectionPane extends ContextualPaneBase implements Vie
this._isSynapseLinkEnabled = ko.computed(() => {
const databaseAccount =
(this.container && this.container.databaseAccount && this.container.databaseAccount()) ||
({} as ViewModels.DatabaseAccount);
({} as DataModels.DatabaseAccount);
const properties = databaseAccount.properties || ({} as DataModels.DatabaseAccountExtendedProperties);
// TODO: remove check for capability once all accounts have been migrated
@@ -713,10 +739,19 @@ export default class AddCollectionPane extends ContextualPaneBase implements Vie
}
private _computeOfferThroughput(): number {
if (this.databaseCreateNewShared()) {
return this.isSharedAutoPilotSelected() ? undefined : this._getThroughput();
if (!this.canConfigureThroughput()) {
return undefined;
}
return this.isAutoPilotSelected() ? undefined : this._getThroughput();
if (this.isAutoPilotSelected()) {
return undefined;
}
if (this.databaseCreateNewShared() && this.isSharedAutoPilotSelected()) {
return undefined;
}
return this._getThroughput();
}
public submit() {
@@ -879,14 +914,15 @@ export default class AddCollectionPane extends ContextualPaneBase implements Vie
this.container.armEndpoint(),
databaseId,
collectionId,
indexingPolicy,
offerThroughput,
partitionKeyPath,
partitionKey.version,
databaseCreateNew,
useDatabaseSharedOffer,
CosmosClient.subscriptionId(),
CosmosClient.resourceGroup(),
CosmosClient.databaseAccount().name,
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
autopilotSettings
)
);
@@ -898,21 +934,21 @@ export default class AddCollectionPane extends ContextualPaneBase implements Vie
databaseId,
this._getAnalyticalStorageTtl(),
collectionId,
indexingPolicy,
offerThroughput,
partitionKeyPath,
partitionKey.version,
databaseCreateNew,
useDatabaseSharedOffer,
CosmosClient.subscriptionId(),
CosmosClient.resourceGroup(),
CosmosClient.databaseAccount().name,
userContext.subscriptionId,
userContext.resourceGroup,
userContext.databaseAccount.name,
uniqueKeyPolicy,
autopilotSettings
)
);
} else {
createCollectionFunc = () =>
this.container.documentClientUtility.getOrCreateDatabaseAndCollection(createRequest, options);
createCollectionFunc = () => getOrCreateDatabaseAndCollection(createRequest, options);
}
createCollectionFunc().then(
@@ -948,7 +984,7 @@ export default class AddCollectionPane extends ContextualPaneBase implements Vie
};
TelemetryProcessor.traceSuccess(Action.CreateCollection, addCollectionPaneSuccessMessage, startKey);
this.resetData();
return this.container.documentClientUtility.refreshCachedResources().then(() => {
return refreshCachedResources().then(() => {
this.container.refreshAllDatabases();
});
},
@@ -1236,10 +1272,7 @@ export default class AddCollectionPane extends ContextualPaneBase implements Vie
: SharedConstants.CollectionCreation.NumberOfPartitionsInUnlimitedCollection;
}
private _convertShardKeyToPartitionKey(
shardKey: string,
configurationOverrides: ViewModels.ConfigurationOverrides
): string {
private _convertShardKeyToPartitionKey(shardKey: string): string {
if (!shardKey) {
return shardKey;
}
@@ -1301,10 +1334,7 @@ export default class AddCollectionPane extends ContextualPaneBase implements Vie
};
if (this.container.isPreferredApiMongoDB()) {
transform = (value: string) => {
return this._convertShardKeyToPartitionKey(
value,
this.container.databaseAccount().properties.configurationOverrides
);
return this._convertShardKeyToPartitionKey(value);
};
}

View File

@@ -48,14 +48,13 @@
<!-- Add database errors - End -->
<!-- upsell message - start -->
<div class="infoBoxContainer" aria-live="assertive" data-bind="visible: formErrors && !formErrors()">
<div class="infoBoxContainer" aria-live="assertive" data-bind="visible: showUpsellMessage && showUpsellMessage() && formErrors && !formErrors()">
<div class="infoBoxContent">
<span><img class="infoBoxIcon" src="/info_color.svg" alt="Promo"></span>
<span class="infoBoxDetails">
<span class="infoBoxMessage" data-bind="text: upsellMessage, attr: { title: upsellMessage }"></span>
<a class="underlinedLink" id="linkAddDatabase" data-bind="attr: { 'aria-label': upsellMessageAriaLabel }"
target="_blank" href="https://aka.ms/azure-cosmos-db-pricing" tabindex="0">
More details</a>
<a class="underlinedLink" id="linkAddDatabase" data-bind="text: upsellAnchorText, attr: { 'href': upsellAnchorUrl, 'aria-label': upsellMessageAriaLabel }"
target="_blank" href="" tabindex="0"></a>
</span>
</div>
</div>
@@ -77,6 +76,9 @@
title="May not end with space nor contain characters '\' '/' '#' '?'" placeholder="Type a new database id"
size="40" class="collid" data-bind="textInput: databaseId, hasFocus: firstFieldHasFocus"
aria-label="Database id" autofocus>
<!-- Database provisioned throughput - Start -->
<!-- ko if: canConfigureThroughput -->
<div class="databaseProvision" aria-label="New database provision support">
<input tabindex="0" type="checkbox" id="addDatabasePane-databaseSharedThroughput"
title="Provision shared throughput" data-bind="checked: databaseCreateNewShared" />
@@ -157,6 +159,7 @@
support</a> for more than <span data-bind="text: maxThroughputRUText"></span> RU/s.</p>
</div>
<!-- /ko -->
<!-- /ko -->
<!-- Database provisioned throughput - End -->
</div>
</div>

View File

@@ -1,14 +1,46 @@
import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
import AddDatabasePane from "./AddDatabasePane";
import { DatabaseAccount } from "../../Contracts/DataModels";
describe("Add Database Pane", () => {
describe("getSharedThroughputDefault()", () => {
let explorer: ViewModels.Explorer;
let explorer: Explorer;
const mockDatabaseAccount: DatabaseAccount = {
id: "mock",
kind: "DocumentDB",
location: "",
name: "mock",
properties: {
documentEndpoint: "",
cassandraEndpoint: "",
gremlinEndpoint: "",
tableEndpoint: "",
enableFreeTier: false
},
type: undefined,
tags: []
};
const mockFreeTierDatabaseAccount: DatabaseAccount = {
id: "mock",
kind: "DocumentDB",
location: "",
name: "mock",
properties: {
documentEndpoint: "",
cassandraEndpoint: "",
gremlinEndpoint: "",
tableEndpoint: "",
enableFreeTier: true
},
type: undefined,
tags: []
};
beforeEach(() => {
explorer = new Explorer({
documentClientUtility: null,
notificationsClient: null,
isEmulator: false
});
@@ -43,5 +75,23 @@ describe("Add Database Pane", () => {
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.getSharedThroughputDefault()).toBe(true);
});
it("should display free tier text in upsell messaging", () => {
explorer.databaseAccount(mockFreeTierDatabaseAccount);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.isFreeTierAccount()).toBe(true);
expect(addDatabasePane.upsellMessage()).toContain("With free tier discount");
expect(addDatabasePane.upsellAnchorUrl()).toBe(Constants.Urls.freeTierInformation);
expect(addDatabasePane.upsellAnchorText()).toBe("Learn more");
});
it("should display standard texr in upsell messaging", () => {
explorer.databaseAccount(mockDatabaseAccount);
const addDatabasePane = explorer.addDatabasePane as AddDatabasePane;
expect(addDatabasePane.isFreeTierAccount()).toBe(false);
expect(addDatabasePane.upsellMessage()).toContain("Start at");
expect(addDatabasePane.upsellAnchorUrl()).toBe(Constants.Urls.cosmosPricing);
expect(addDatabasePane.upsellAnchorText()).toBe("More details");
});
});
});

View File

@@ -14,10 +14,11 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import { AddDbUtilities } from "../../Shared/AddDatabaseUtility";
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
import { ContextualPaneBase } from "./ContextualPaneBase";
import { CosmosClient } from "../../Common/CosmosClient";
import { PlatformType } from "../../PlatformType";
import { refreshCachedOffers, refreshCachedResources, createDatabase } from "../../Common/DocumentClientUtilityBase";
import { userContext } from "../../UserContext";
export default class AddDatabasePane extends ContextualPaneBase implements ViewModels.AddDatabasePane {
export default class AddDatabasePane extends ContextualPaneBase {
public defaultExperience: ko.Computed<string>;
public databaseIdLabel: ko.Computed<string>;
public databaseId: ko.Observable<string>;
@@ -38,6 +39,8 @@ export default class AddDatabasePane extends ContextualPaneBase implements ViewM
public costsVisible: ko.PureComputed<boolean>;
public upsellMessage: ko.PureComputed<string>;
public upsellMessageAriaLabel: ko.PureComputed<string>;
public upsellAnchorUrl: ko.PureComputed<string>;
public upsellAnchorText: ko.PureComputed<string>;
public isAutoPilotSelected: ko.Observable<boolean>;
public selectedAutoPilotTier: ko.Observable<DataModels.AutopilotTier>;
public autoPilotTiersList: ko.ObservableArray<ViewModels.DropdownOption<DataModels.AutopilotTier>>;
@@ -47,6 +50,8 @@ export default class AddDatabasePane extends ContextualPaneBase implements ViewM
public hasAutoPilotV2FeatureFlag: ko.PureComputed<boolean>;
public ruToolTipText: ko.Computed<string>;
public isFreeTierAccount: ko.Computed<boolean>;
public canConfigureThroughput: ko.PureComputed<boolean>;
public showUpsellMessage: ko.PureComputed<boolean>;
constructor(options: ViewModels.PaneOptions) {
super(options);
@@ -54,6 +59,8 @@ export default class AddDatabasePane extends ContextualPaneBase implements ViewM
this.databaseId = ko.observable<string>();
this.hasAutoPilotV2FeatureFlag = ko.pureComputed(() => this.container.hasAutoPilotV2FeatureFlag());
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText(this.hasAutoPilotV2FeatureFlag()));
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.showUpsellMessage = ko.pureComputed(() => !this.container.isServerlessEnabled());
this.canExceedMaximumValue = ko.pureComputed(() => this.container.canExceedMaximumValue());
@@ -228,11 +235,19 @@ export default class AddDatabasePane extends ContextualPaneBase implements ViewM
});
this.upsellMessage = ko.pureComputed<string>(() => {
return PricingUtils.getUpsellMessage(this.container.serverId());
return PricingUtils.getUpsellMessage(this.container.serverId(), this.isFreeTierAccount());
});
this.upsellMessageAriaLabel = ko.pureComputed<string>(() => {
return `${this.upsellMessage()}. Click for more details`;
return `${this.upsellMessage()}. Click ${this.isFreeTierAccount() ? "to learn more" : "for more details"}`;
});
this.upsellAnchorUrl = ko.pureComputed<string>(() => {
return this.isFreeTierAccount() ? Constants.Urls.freeTierInformation : Constants.Urls.cosmosPricing;
});
this.upsellAnchorText = ko.pureComputed<string>(() => {
return this.isFreeTierAccount() ? "Learn more" : "More details";
});
}
@@ -293,8 +308,8 @@ export default class AddDatabasePane extends ContextualPaneBase implements ViewM
db: addDatabasePaneStartMessage.database.id,
st: addDatabasePaneStartMessage.database.shared,
offerThroughput: addDatabasePaneStartMessage.offerThroughput,
sid: CosmosClient.subscriptionId(),
rg: CosmosClient.resourceGroup(),
sid: userContext.subscriptionId,
rg: userContext.resourceGroup,
dba: addDatabasePaneStartMessage.databaseAccountName
};
@@ -320,10 +335,7 @@ export default class AddDatabasePane extends ContextualPaneBase implements ViewM
) {
AddDbUtilities.createSqlDatabase(this.container.armEndpoint(), createDatabaseParameters, autoPilotSettings).then(
() => {
Promise.all([
this.container.documentClientUtility.refreshCachedOffers(),
this.container.documentClientUtility.refreshCachedResources()
]).then(() => {
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
});
}
@@ -340,10 +352,7 @@ export default class AddDatabasePane extends ContextualPaneBase implements ViewM
createDatabaseParameters,
autoPilotSettings
).then(() => {
Promise.all([
this.container.documentClientUtility.refreshCachedOffers(),
this.container.documentClientUtility.refreshCachedResources()
]).then(() => {
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
});
});
@@ -359,10 +368,7 @@ export default class AddDatabasePane extends ContextualPaneBase implements ViewM
createDatabaseParameters,
autoPilotSettings
).then(() => {
Promise.all([
this.container.documentClientUtility.refreshCachedOffers(),
this.container.documentClientUtility.refreshCachedResources()
]).then(() => {
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
});
});
@@ -399,7 +405,7 @@ export default class AddDatabasePane extends ContextualPaneBase implements ViewM
autoPilot,
hasAutoPilotV2FeatureFlag: this.hasAutoPilotV2FeatureFlag()
};
this.container.documentClientUtility.createDatabase(createRequest).then(
createDatabase(createRequest).then(
(database: DataModels.Database) => {
this._onCreateDatabaseSuccess(offerThroughput, telemetryStartKey);
},
@@ -450,10 +456,7 @@ export default class AddDatabasePane extends ContextualPaneBase implements ViewM
startKey: number
): void {
AddDbUtilities.createCassandraKeyspace(armEndpoint, createKeyspaceParameters, autoPilotSettings).then(() => {
Promise.all([
this.container.documentClientUtility.refreshCachedOffers(),
this.container.documentClientUtility.refreshCachedResources()
]).then(() => {
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
this._onCreateDatabaseSuccess(createKeyspaceParameters.offerThroughput, startKey);
});
});
@@ -512,7 +515,15 @@ export default class AddDatabasePane extends ContextualPaneBase implements ViewM
}
private _computeOfferThroughput(): number {
return this.isAutoPilotSelected() ? undefined : this._getThroughput();
if (!this.canConfigureThroughput()) {
return undefined;
}
if (this.isAutoPilotSelected()) {
return undefined;
}
return this._getThroughput();
}
private _isValid(): boolean {

View File

@@ -6,8 +6,9 @@ import { ContextualPaneBase } from "./ContextualPaneBase";
import * as Logger from "../../Common/Logger";
import { QueriesGridComponentAdapter } from "../Controls/QueriesGridReactComponent/QueriesGridComponentAdapter";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import QueryTab from "../Tabs/QueryTab";
export class BrowseQueriesPane extends ContextualPaneBase implements ViewModels.BrowseQueriesPane {
export class BrowseQueriesPane extends ContextualPaneBase {
public queriesGridComponentAdapter: QueriesGridComponentAdapter;
public canSaveQueries: ko.Computed<boolean>;
@@ -87,7 +88,7 @@ export class BrowseQueriesPane extends ContextualPaneBase implements ViewModels.
} else {
selectedCollection.onNewQueryClick(selectedCollection, null);
}
const queryTab: ViewModels.QueryTab = this.container.findActiveTab() as ViewModels.QueryTab;
const queryTab = this.container.tabsManager.activeTab() as QueryTab;
queryTab.tabTitle(savedQuery.queryName);
queryTab.tabPath(`${selectedCollection.databaseId}>${selectedCollection.id()}>${savedQuery.queryName}`);
queryTab.initialEditorContent(savedQuery.query);

View File

@@ -118,6 +118,8 @@
<option data-bind="value: $data.id"> </option>
</datalist>
<!-- Database provisioned throughput - Start -->
<!-- ko if: canConfigureThroughput -->
<div
class="databaseProvision"
aria-label="New database provision support"
@@ -202,6 +204,8 @@
</throughput-input>
</div>
<!-- /ko -->
<!-- /ko -->
<!-- Database provisioned throughput - End -->
</div>
<div class="seconddivpadding">
<p>
@@ -231,6 +235,9 @@
style="height:125px; width: calc(100% - 80px); resize: vertical;"
></textarea>
</div>
<!-- Provision table throughput - start -->
<!-- ko if: canConfigureThroughput -->
<div class="seconddivpadding" data-bind="visible: keyspaceHasSharedOffer() && !keyspaceCreateNew()">
<input
type="checkbox"
@@ -315,6 +322,8 @@
</throughput-input>
</div>
<!-- /ko -->
<!-- /ko -->
<!-- Provision table throughput - end -->
</div>
<div class="paneFooter">
<div class="leftpanel-okbut">

Some files were not shown because too many files have changed in this diff Show More