Merge branch 'master' into remove_explorer.defaultExperience

This commit is contained in:
Steve Faulkner
2021-04-27 19:51:06 -05:00
66 changed files with 5774 additions and 2043 deletions

View File

@@ -5,7 +5,6 @@ src/Api/Apis.ts
src/AuthType.ts
src/Bindings/BindingHandlersRegisterer.ts
src/Bindings/ReactBindingHandler.ts
src/Common/ArrayHashMap.ts
src/Common/Constants.ts
src/Common/CosmosClient.test.ts
src/Common/CosmosClient.ts
@@ -13,15 +12,12 @@ src/Common/DataAccessUtilityBase.test.ts
src/Common/DataAccessUtilityBase.ts
src/Common/EditableUtility.ts
src/Common/HashMap.test.ts
src/Common/HashMap.ts
src/Common/Logger.test.ts
src/Common/MessageHandler.test.ts
src/Common/MessageHandler.ts
src/Common/MongoProxyClient.test.ts
src/Common/MongoUtility.ts
src/Common/NotificationsClientBase.ts
src/Common/ObjectCache.test.ts
src/Common/ObjectCache.ts
src/Common/QueriesClient.ts
src/Common/Splitter.ts
src/Config.ts
@@ -125,7 +121,7 @@ src/Explorer/Panes/GraphStylingPane.ts
# src/Explorer/Panes/NewVertexPane.ts
src/Explorer/Panes/PaneComponents.ts
src/Explorer/Panes/RenewAdHocAccessPane.ts
src/Explorer/Panes/StringInputPane.ts
src/Explorer/Panes/SetupNotebooksPane.ts
src/Explorer/Panes/SwitchDirectoryPane.ts
src/Explorer/Panes/Tables/EditTableEntityPane.ts
src/Explorer/Panes/Tables/EntityPropertyViewModel.ts

View File

@@ -22,13 +22,7 @@ export interface ReactAdapter {
export class Registerer {
public static register(): void {
ko.bindingHandlers.react = {
init: (
element: any,
wrappedValueAccessor: () => any,
allBindings?: ko.AllBindings,
viewModel?: any,
bindingContext?: ko.BindingContext
) => {
init: (element: any, wrappedValueAccessor: () => any) => {
const adapter: ReactAdapter = wrappedValueAccessor();
if (adapter.setElement) {

View File

@@ -1,49 +1,9 @@
import { HashMap } from "./HashMap";
/**
* Hash map of arrays which allows to:
* - push an item by key: add to array and create array if needed
* - remove item by key: remove from array and delete array if needed
*/
export class ArrayHashMap<T> {
private store: HashMap<T[]>;
constructor() {
this.store = new HashMap();
}
public has(key: string): boolean {
return this.store.has(key);
}
public get(key: string): T[] {
return this.store.get(key);
}
public size(): number {
return this.store.size();
}
public clear(): void {
this.store.clear();
}
public keys(): string[] {
return this.store.keys();
}
public delete(key: string): boolean {
return this.store.delete(key);
}
public forEach(key: string, iteratorFct: (value: T) => void) {
const values = this.store.get(key);
if (values) {
values.forEach((value) => iteratorFct(value));
}
}
export class ArrayHashMap<T> extends Map<string, T[]> {
/**
* Insert item into array.
* If no array, create one.
@@ -52,16 +12,8 @@ export class ArrayHashMap<T> {
* @param item
*/
public push(key: string, item: T): void {
let itemsArray: T[] = this.store.get(key);
if (!itemsArray) {
itemsArray = [item];
this.store.set(key, itemsArray);
return;
}
if (itemsArray.indexOf(item) === -1) {
itemsArray.push(item);
}
const array = this.get(key);
array ? array.includes(item) || array.push(item) : this.set(key, [item]);
}
/**
@@ -70,18 +22,11 @@ export class ArrayHashMap<T> {
* @param key
* @param itemToRemove
*/
public remove(key: string, itemToRemove: T) {
if (!this.store.has(key)) {
return;
}
const itemsArray = this.store.get(key);
const index = itemsArray.indexOf(itemToRemove);
if (index >= 0) {
itemsArray.splice(index, 1);
if (itemsArray.length === 0) {
this.store.delete(key);
}
public remove(key: string, itemToRemove: T): void {
const array = this.get(key);
if (array) {
const remaining = array.filter((item) => item !== itemToRemove);
remaining.length ? this.set(key, remaining) : this.delete(key);
}
}
}

View File

@@ -1,70 +0,0 @@
import { HashMap } from "./HashMap";
describe("HashMap", () => {
it("should test if key/val exists", () => {
const map = new HashMap<number>();
map.set("a", 123);
expect(map.has("a")).toBe(true);
expect(map.has("b")).toBe(false);
});
it("should get object back", () => {
const map = new HashMap<string>();
map.set("a", "123");
map.set("a", "456");
expect(map.get("a")).toBe("456");
expect(map.get("a")).not.toBe("123");
});
it("should return the right size", () => {
const map = new HashMap<string>();
map.set("a", "123");
map.set("b", "456");
expect(map.size()).toBe(2);
});
it("should be iterable", () => {
const map = new HashMap<number>();
map.set("a", 1);
map.set("b", 10);
map.set("c", 100);
map.set("d", 1000);
let i = 0;
map.forEach((key: string, value: number) => {
i += value;
});
expect(i).toBe(1111);
});
it("should be deleted", () => {
const map = new HashMap<number>();
map.set("a", 1);
map.set("b", 10);
expect(map.delete("a")).toBe(true);
expect(map.delete("c")).toBe(false);
expect(map.has("a")).toBe(false);
expect(map.has("b")).toBe(true);
});
it("should clear", () => {
const map = new HashMap<number>();
map.set("a", 1);
map.clear();
expect(map.size()).toBe(0);
expect(map.has("a")).toBe(false);
});
it("should return all keys", () => {
const map = new HashMap<number>();
map.set("a", 1);
map.set("b", 1);
expect(map.keys()).toEqual(["a", "b"]);
map.clear();
expect(map.keys().length).toBe(0);
});
});

View File

@@ -1,45 +0,0 @@
/**
* Simple hashmap implementation that doesn't rely on ES6 Map nor polyfills
*/
export class HashMap<T> {
constructor(private container: { [key: string]: T } = {}) {}
public has(key: string): boolean {
return this.container.hasOwnProperty(key);
}
public set(key: string, value: T): void {
this.container[key] = value;
}
public get(key: string): T {
return this.container[key];
}
public size(): number {
return Object.keys(this.container).length;
}
public delete(key: string): boolean {
if (this.has(key)) {
delete this.container[key];
return true;
}
return false;
}
public clear(): void {
this.container = {};
}
public keys(): string[] {
return Object.keys(this.container);
}
public forEach(iteratorFct: (key: string, value: T) => void) {
for (const k in this.container) {
iteratorFct(k, this.container[k]);
}
}
}

View File

@@ -7,7 +7,7 @@ describe("Object cache", () => {
cache.set("b", 2);
cache.set("c", 3);
cache.set("d", 4);
expect(cache.size()).toBe(2);
expect(cache.size).toBe(2);
});
it("should remove first added element to keep size at limit", () => {

View File

@@ -1,56 +1,27 @@
import { HashMap } from "./HashMap";
export class ObjectCache<T> extends HashMap<T> {
private keyQueue: string[]; // Last touched key FIFO to purge cache if too big.
private maxNbElements: number;
public constructor(maxNbElements: number) {
export class ObjectCache<T> extends Map<string, T> {
constructor(private limit: number) {
super();
this.keyQueue = [];
this.maxNbElements = maxNbElements;
this.clear();
}
public clear(): void {
super.clear();
this.keyQueue = [];
public get(key: string): T | undefined {
return this.touch(key);
}
public get(key: string): T {
this.markKeyAsTouched(key);
return super.get(key);
}
public set(key: string, value: T): void {
super.set(key, value);
this.markKeyAsTouched(key);
if (super.size() > this.maxNbElements && key !== this.keyQueue[0]) {
this.reduceCacheSize();
public set(key: string, value: T): this {
if (this.size === this.limit) {
this.delete(this.keys().next().value);
}
return this.touch(key, value), this;
}
/**
* Invalidate elements to keep the total number below the limit
*/
private reduceCacheSize(): void {
// remove a key
const oldKey = this.keyQueue.shift();
if (oldKey) {
super.delete(oldKey);
private touch(key: string, value = super.get(key)) {
// Map keeps (re) insertion order according to ES6 spec
if (value) {
this.delete(key);
super.set(key, value);
}
}
/**
* Bubble up this key as new.
* @param key
*/
private markKeyAsTouched(key: string) {
const n = this.keyQueue.indexOf(key);
if (n > -1) {
this.keyQueue.splice(n, 1);
}
this.keyQueue.push(key);
return value;
}
}

View File

@@ -73,7 +73,7 @@ export class Splitter {
$(this.leftSide).resizable(splitterOptions);
}
private onResizeStart: JQueryUI.ResizableEvent = (e: Event, ui: JQueryUI.ResizableUIParams) => {
private onResizeStart: JQueryUI.ResizableEvent = () => {
if (this.direction === SplitterDirection.Vertical) {
$(".ui-resizable-helper").height("100%");
} else {
@@ -82,9 +82,7 @@ export class Splitter {
$("iframe").css("pointer-events", "none");
};
private onResizeStop: JQueryUI.ResizableEvent = (e: Event, ui: JQueryUI.ResizableUIParams) => {
$("iframe").css("pointer-events", "auto");
};
private onResizeStop: JQueryUI.ResizableEvent = () => $("iframe").css("pointer-events", "auto");
public collapseLeft() {
this.lastX = $(this.splitter).position().left;

View File

@@ -28,10 +28,6 @@ describe("Component Registerer", () => {
expect(ko.components.isRegistered("graph-styling-pane")).toBe(true);
});
it("should register string-input-pane component", () => {
expect(ko.components.isRegistered("string-input-pane")).toBe(true);
});
it("should register dynamic-list component", () => {
expect(ko.components.isRegistered("dynamic-list")).toBe(true);
});

View File

@@ -25,5 +25,4 @@ ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPane
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent());
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent());
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -199,9 +199,7 @@ exports[`SettingsComponent renders 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -224,20 +222,6 @@ exports[`SettingsComponent renders 1`] = `
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -392,9 +376,7 @@ exports[`SettingsComponent renders 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -507,21 +489,9 @@ exports[`SettingsComponent renders 1`] = `
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -547,20 +517,6 @@ exports[`SettingsComponent renders 1`] = `
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
@@ -758,9 +714,7 @@ exports[`SettingsComponent renders 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -783,20 +737,6 @@ exports[`SettingsComponent renders 1`] = `
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -951,9 +891,7 @@ exports[`SettingsComponent renders 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -1066,21 +1004,9 @@ exports[`SettingsComponent renders 1`] = `
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -1106,20 +1032,6 @@ exports[`SettingsComponent renders 1`] = `
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
@@ -1330,9 +1242,7 @@ exports[`SettingsComponent renders 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -1355,20 +1265,6 @@ exports[`SettingsComponent renders 1`] = `
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -1523,9 +1419,7 @@ exports[`SettingsComponent renders 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -1638,21 +1532,9 @@ exports[`SettingsComponent renders 1`] = `
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -1678,20 +1560,6 @@ exports[`SettingsComponent renders 1`] = `
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
@@ -1889,9 +1757,7 @@ exports[`SettingsComponent renders 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -1914,20 +1780,6 @@ exports[`SettingsComponent renders 1`] = `
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -2082,9 +1934,7 @@ exports[`SettingsComponent renders 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -2197,21 +2047,9 @@ exports[`SettingsComponent renders 1`] = `
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -2237,20 +2075,6 @@ exports[`SettingsComponent renders 1`] = `
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],

View File

@@ -6,7 +6,6 @@ import React from "react";
import _ from "underscore";
import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
import * as Constants from "../Common/Constants";
import { ExplorerMetrics } from "../Common/Constants";
import { readCollection } from "../Common/dataAccess/readCollection";
@@ -64,7 +63,7 @@ import { LoadQueryPane } from "./Panes/LoadQueryPane/LoadQueryPane";
import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane";
import { SettingsPane } from "./Panes/SettingsPane/SettingsPane";
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
import { StringInputPane } from "./Panes/StringInputPane";
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel";
import { EditTableEntityPanel } from "./Panes/Tables/EditTableEntityPanel";
import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel";
@@ -73,7 +72,6 @@ import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel";
import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import type { GalleryTabOptions } from "./Tabs/GalleryTab";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import QueryTablesTab from "./Tabs/QueryTablesTab";
import { TabsManager } from "./Tabs/TabsManager";
@@ -170,9 +168,7 @@ export default class Explorer {
public addCollectionPane: AddCollectionPane;
public graphStylingPane: GraphStylingPane;
public cassandraAddCollectionPane: CassandraAddCollectionPane;
public stringInputPane: StringInputPane;
public gitHubReposPane: ContextualPaneBase;
public publishNotebookPaneAdapter: ReactAdapter;
// features
public isGitHubPaneEnabled: ko.Observable<boolean>;
@@ -500,13 +496,6 @@ export default class Explorer {
container: this,
});
this.stringInputPane = new StringInputPane({
id: "stringinputpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.tabsManager = params?.tabsManager ?? new TabsManager();
this.tabsManager.openedTabs.subscribe((tabs) => {
if (tabs.length === 0) {
@@ -520,7 +509,6 @@ export default class Explorer {
this.addCollectionPane,
this.graphStylingPane,
this.cassandraAddCollectionPane,
this.stringInputPane,
];
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
this.isTabsContentExpanded = ko.observable(false);
@@ -1414,7 +1402,6 @@ export default class Explorer {
): Promise<void> {
if (this.notebookManager) {
await this.notebookManager.openPublishNotebookPane(name, content, parentDomElement);
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
this.isPublishNotebookPaneEnabled(true);
}
}
@@ -1527,7 +1514,7 @@ export default class Explorer {
return true;
}
public renameNotebook(notebookFile: NotebookContentItem): Q.Promise<NotebookContentItem> {
public renameNotebook(notebookFile: NotebookContentItem): void {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to rename notebook, but notebook is not enabled";
handleError(error, "Explorer/renameNotebook");
@@ -1543,57 +1530,59 @@ export default class Explorer {
);
if (openedNotebookTabs.length > 0) {
this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
return Q.reject();
} else {
this.openSidePanel(
"",
<StringInputPane
explorer={this}
closePanel={() => {
this.closeSidePanel();
this.resourceTree.triggerRender();
}}
inputLabel="Enter new notebook name"
submitButtonLabel="Rename"
errorMessage="Could not rename notebook"
inProgressMessage="Renaming notebook to"
successMessage="Renamed notebook to"
paneTitle="Rename Notebook"
defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")}
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input)
}
notebookFile={notebookFile}
/>
);
}
const originalPath = notebookFile.path;
const result = this.stringInputPane
.openWithOptions<NotebookContentItem>({
errorMessage: "Could not rename notebook",
inProgressMessage: "Renaming notebook to",
successMessage: "Renamed notebook to",
inputLabel: "Enter new notebook name",
paneTitle: "Rename Notebook",
submitButtonLabel: "Rename",
defaultInput: FileSystemUtil.stripExtension(notebookFile.name, "ipynb"),
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input),
})
.then((newNotebookFile) => {
const notebookTabs = this.tabsManager.getTabs(
ViewModels.CollectionTabKind.NotebookV2,
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
);
notebookTabs.forEach((tab) => {
tab.tabTitle(newNotebookFile.name);
tab.tabPath(newNotebookFile.path);
(tab as NotebookV2Tab).notebookPath(newNotebookFile.path);
});
return newNotebookFile;
});
result.then(() => this.resourceTree.triggerRender());
return result;
}
public onCreateDirectory(parent: NotebookContentItem): Q.Promise<NotebookContentItem> {
public onCreateDirectory(parent: NotebookContentItem): void {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create notebook directory, but notebook is not enabled";
handleError(error, "Explorer/onCreateDirectory");
throw new Error(error);
}
const result = this.stringInputPane.openWithOptions<NotebookContentItem>({
errorMessage: "Could not create directory ",
inProgressMessage: "Creating directory ",
successMessage: "Created directory ",
inputLabel: "Enter new directory name",
paneTitle: "Create new directory",
submitButtonLabel: "Create",
defaultInput: "",
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input),
});
result.then(() => this.resourceTree.triggerRender());
return result;
this.openSidePanel(
"",
<StringInputPane
explorer={this}
closePanel={() => {
this.closeSidePanel();
this.resourceTree.triggerRender();
}}
errorMessage="Could not create directory "
inProgressMessage="Creating directory "
successMessage="Created directory "
inputLabel="Enter new directory name"
paneTitle="Create new directory"
submitButtonLabel="Create"
defaultInput=""
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input)
}
notebookFile={parent}
/>
);
}
public readFile(notebookFile: NotebookContentItem): Promise<string> {
@@ -1917,33 +1906,35 @@ export default class Explorer {
const title = "Gallery";
const hashLocation = "gallery";
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
const galleryTabOptions: GalleryTabOptions = {
account: userContext.databaseAccount,
container: this,
junoClient: this.notebookManager?.junoClient,
selectedTab: selectedTab || GalleryTabKind.PublicGallery,
notebookUrl,
galleryItem,
isFavorite,
tabKind: ViewModels.CollectionTabKind.Gallery,
title: title,
tabPath: title,
hashLocation: hashLocation,
onUpdateTabsButtons: this.onUpdateTabsButtons,
isTabsContentExpanded: ko.observable(true),
onLoadStartKey: null,
};
const galleryTab = this.tabsManager
.getTabs(ViewModels.CollectionTabKind.Gallery)
.find((tab) => tab.hashLocation() == hashLocation);
if (galleryTab instanceof GalleryTab) {
this.tabsManager.activateTab(galleryTab);
galleryTab.reset(galleryTabOptions);
} else {
this.tabsManager.activateNewTab(new GalleryTab(galleryTabOptions));
this.tabsManager.activateNewTab(
new GalleryTab(
{
tabKind: ViewModels.CollectionTabKind.Gallery,
title: title,
tabPath: title,
hashLocation: hashLocation,
onUpdateTabsButtons: this.onUpdateTabsButtons,
onLoadStartKey: null,
isTabsContentExpanded: ko.observable(true),
},
{
account: userContext.databaseAccount,
container: this,
junoClient: this.notebookManager?.junoClient,
selectedTab: selectedTab || GalleryTabKind.PublicGallery,
notebookUrl,
galleryItem,
isFavorite,
}
)
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
* Contains all notebook related stuff meant to be dynamically loaded by explorer
*/
import type { ImmutableNotebook } from "@nteract/commutable";
import { ImmutableNotebook } from "@nteract/commutable";
import type { IContentProvider } from "@nteract/core";
import ko from "knockout";
import React from "react";
@@ -22,7 +22,7 @@ import Explorer from "../Explorer";
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
import { GitHubReposPane } from "../Panes/GitHubReposPane";
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
import { NotebookContainerClient } from "./NotebookContainerClient";
@@ -53,7 +53,6 @@ export default class NotebookManager {
private gitHubClient: GitHubClient;
public gitHubReposPane: ContextualPaneBase;
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
public initialize(params: NotebookManagerOptions): void {
this.params = params;
@@ -91,8 +90,6 @@ export default class NotebookManager {
this.notebookContentProvider
);
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
this.gitHubClient.setToken(token?.access_token);
@@ -123,7 +120,20 @@ export default class NotebookManager {
content: NotebookPaneContent,
parentDomElement: HTMLElement
): Promise<void> {
await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement);
const explorer = this.params.container;
explorer.openSidePanel(
"New Collection",
<PublishNotebookPane
explorer={this.params.container}
junoClient={this.junoClient}
closePanel={this.params.container.closeSidePanel}
openNotificationConsole={this.params.container.expandConsole}
name={name}
author={getFullName()}
notebookContent={content}
parentDomElement={parentDomElement}
/>
);
}
public openCopyNotebookPane(name: string, content: string): void {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -175,9 +175,7 @@ exports[`Settings Pane should render Default properly 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -200,20 +198,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -368,9 +352,7 @@ exports[`Settings Pane should render Default properly 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -483,21 +465,9 @@ exports[`Settings Pane should render Default properly 1`] = `
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -523,20 +493,6 @@ exports[`Settings Pane should render Default properly 1`] = `
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],
@@ -857,9 +813,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -882,20 +836,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -1050,9 +990,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -1165,21 +1103,9 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -1205,20 +1131,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -175,9 +175,7 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -200,20 +198,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -368,9 +352,7 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -483,21 +465,9 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -523,20 +493,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],

View File

@@ -176,9 +176,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -201,20 +199,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"userTableQuery": [Function],
"visible": [Function],
},
StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function],
@@ -369,9 +353,7 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": HashMap {
"container": Object {},
},
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
@@ -488,21 +470,9 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"resourceTree": ResourceTreeAdapter {
"container": [Circular],
"copyNotebook": [Function],
"databaseCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsCollectionIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"koSubsDatabaseIdMap": ArrayHashMap {
"store": HashMap {
"container": Object {},
},
},
"databaseCollectionIdMap": Map {},
"koSubsCollectionIdMap": Map {},
"koSubsDatabaseIdMap": Map {},
"parameters": [Function],
},
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
@@ -528,20 +498,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"onResizeStop": [Function],
"splitterId": "h_splitter1",
},
"stringInputPane": StringInputPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "stringinputpane",
"inputLabel": [Function],
"isExecuting": [Function],
"isTemplateReady": [Function],
"stringInput": [Function],
"submitButtonLabel": [Function],
"title": [Function],
"visible": [Function],
},
"tabsManager": TabsManager {
"activeTab": [Function],
"openedTabs": [Function],

View File

@@ -1,13 +1,14 @@
import { DatabaseAccount } from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { IGalleryItem, JunoClient } from "../../Juno/JunoClient";
import { GalleryAndNotebookViewerComponentProps } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponent";
import { GalleryAndNotebookViewerComponentAdapter } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponentAdapter";
import { GalleryTab as GalleryViewerTab, SortBy } from "../Controls/NotebookGallery/GalleryViewerComponent";
import Explorer from "../Explorer";
import React from "react";
import type { DatabaseAccount } from "../../Contracts/DataModels";
import type { TabOptions } from "../../Contracts/ViewModels";
import type { IGalleryItem, JunoClient } from "../../Juno/JunoClient";
import { GalleryAndNotebookViewerComponent as GalleryViewer } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponent";
import type { GalleryTab as GalleryViewerTab } from "../Controls/NotebookGallery/GalleryViewerComponent";
import { SortBy } from "../Controls/NotebookGallery/GalleryViewerComponent";
import type Explorer from "../Explorer";
import TabsBase from "./TabsBase";
export interface GalleryTabOptions extends ViewModels.TabOptions {
interface Props {
account: DatabaseAccount;
container: Explorer;
junoClient: JunoClient;
@@ -17,51 +18,16 @@ export interface GalleryTabOptions extends ViewModels.TabOptions {
isFavorite?: boolean;
}
/**
* Notebook gallery tab
*/
export default class GalleryTab extends TabsBase {
public readonly html = '<div style="height: 100%" data-bind="react:galleryAndNotebookViewerComponentAdapter"></div>';
private container: Explorer;
private galleryAndNotebookViewerComponentProps: GalleryAndNotebookViewerComponentProps;
public galleryAndNotebookViewerComponentAdapter: GalleryAndNotebookViewerComponentAdapter;
constructor(options: GalleryTabOptions) {
constructor(options: TabOptions, private props: Props) {
super(options);
this.container = options.container;
this.galleryAndNotebookViewerComponentProps = {
container: options.container,
junoClient: options.junoClient,
notebookUrl: options.notebookUrl,
galleryItem: options.galleryItem,
isFavorite: options.isFavorite,
selectedTab: options.selectedTab,
sortBy: SortBy.MostRecent,
searchText: undefined,
};
this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter(
this.galleryAndNotebookViewerComponentProps
);
}
public reset(options: GalleryTabOptions) {
this.container = options.container;
this.galleryAndNotebookViewerComponentProps.container = options.container;
this.galleryAndNotebookViewerComponentProps.junoClient = options.junoClient;
this.galleryAndNotebookViewerComponentProps.notebookUrl = options.notebookUrl;
this.galleryAndNotebookViewerComponentProps.galleryItem = options.galleryItem;
this.galleryAndNotebookViewerComponentProps.isFavorite = options.isFavorite;
this.galleryAndNotebookViewerComponentProps.selectedTab = options.selectedTab;
this.galleryAndNotebookViewerComponentProps.sortBy = SortBy.MostViewed;
this.galleryAndNotebookViewerComponentProps.searchText = undefined;
this.galleryAndNotebookViewerComponentAdapter.reset();
this.galleryAndNotebookViewerComponentAdapter.triggerRender();
public render() {
return <GalleryViewer {...this.props} sortBy={SortBy.MostRecent} searchText={undefined} />;
}
public getContainer(): Explorer {
return this.container;
return this.props.container;
}
}

View File

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

View File

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

View File

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

View File

@@ -12,11 +12,15 @@ export const Tabs = ({ tabs, activeTab }: { tabs: readonly Tab[]; activeTab: Tab
<div id="content" className="flexContainer hideOverflows">
<div className="nav-tabs-margin">
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
{...tabs.map((tab) => <TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />)}
{tabs.map((tab) => (
<TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />
))}
</ul>
</div>
<div className="tabPanesContainer">
{...tabs.map((tab) => <TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />)}
{tabs.map((tab) => (
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
))}
</div>
</div>
</div>

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
import postRobot from "post-robot";
export interface IGitHubConnectorParams {
state: string;
code: string;
@@ -6,25 +8,26 @@ export interface IGitHubConnectorParams {
export const GitHubConnectorMsgType = "GitHubConnectorMsgType";
export class GitHubConnector {
public start(params: URLSearchParams, window: Window & typeof globalThis) {
window.postMessage(
public async start(params: URLSearchParams, window: Window & typeof globalThis): Promise<void> {
await postRobot.send(
window,
GitHubConnectorMsgType,
{
type: GitHubConnectorMsgType,
data: {
state: params.get("state"),
code: params.get("code"),
} as IGitHubConnectorParams,
},
window.location.origin
state: params.get("state"),
code: params.get("code"),
} as IGitHubConnectorParams,
{
domain: window.location.origin,
}
);
}
}
var connector = new GitHubConnector();
window.addEventListener("load", () => {
window.addEventListener("load", async () => {
const openerWindow = window.opener;
if (openerWindow) {
connector.start(new URLSearchParams(document.location.search), openerWindow);
await connector.start(new URLSearchParams(document.location.search), openerWindow);
window.close();
}
});

View File

@@ -1,12 +1,12 @@
import ko from "knockout";
import { HttpStatusCodes } from "../Common/Constants";
import * as DataModels from "../Contracts/DataModels";
import { JunoClient } from "../Juno/JunoClient";
import { GitHubConnector, IGitHubConnectorParams } from "./GitHubConnector";
import { GitHubOAuthService } from "./GitHubOAuthService";
import Explorer from "../Explorer/Explorer";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import NotebookManager from "../Explorer/Notebook/NotebookManager";
import Explorer from "../Explorer/Explorer";
import { JunoClient } from "../Juno/JunoClient";
import { IGitHubConnectorParams } from "./GitHubConnector";
import { GitHubOAuthService } from "./GitHubOAuthService";
const sampleDatabaseAccount: DataModels.DatabaseAccount = {
id: "id",
@@ -85,25 +85,6 @@ describe("GitHubOAuthService", () => {
expect(newParams.get("state")).not.toEqual(initialParams.get("state"));
});
it("finishOAuth is called whenever GitHubConnector is started", async () => {
const finishOAuthCallback = jest.fn().mockImplementation();
gitHubOAuthService.finishOAuth = finishOAuthCallback;
const params: IGitHubConnectorParams = {
state: "state",
code: "code",
};
const searchParams = new URLSearchParams({ ...params });
const gitHubConnector = new GitHubConnector();
gitHubConnector.start(searchParams, window);
// GitHubConnector uses Window.postMessage and there's no good way to know when the message has received
await new Promise((resolve) => setTimeout(resolve, 100));
expect(finishOAuthCallback).toBeCalledWith(params);
});
it("finishOAuth updates token", async () => {
const data = { key: "value" };
const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.OK, data });

View File

@@ -1,24 +1,25 @@
import ko from "knockout";
import postRobot from "post-robot";
import { HttpStatusCodes } from "../Common/Constants";
import { handleError } from "../Common/ErrorHandlingUtils";
import { configContext } from "../ConfigContext";
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
import { JunoClient } from "../Juno/JunoClient";
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
import { logConsoleInfo } from "../Utils/NotificationConsoleUtils";
import { GitHubConnectorMsgType, IGitHubConnectorParams } from "./GitHubConnector";
window.addEventListener("message", (event: MessageEvent) => {
if (isInvalidParentFrameOrigin(event)) {
return;
}
const msg = event.data;
if (msg.type === GitHubConnectorMsgType) {
const params = msg.data as IGitHubConnectorParams;
postRobot.on(
GitHubConnectorMsgType,
{
domain: window.location.origin,
},
(event) => {
// Typescript definition for event is wrong. So read params by casting to <any>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const params = (event as any).data as IGitHubConnectorParams;
window.dataExplorer.notebookManager?.gitHubOAuthService.finishOAuth(params);
}
});
);
export interface IGitHubOAuthToken {
// API properties

View File

@@ -232,13 +232,9 @@ const App: React.FunctionComponent = () => {
<div data-bind='component: { name: "add-collection-pane", params: { data: addCollectionPane} }' />
<div data-bind='component: { name: "graph-styling-pane", params: { data: graphStylingPane} }' />
<div data-bind='component: { name: "cassandra-add-collection-pane", params: { data: cassandraAddCollectionPane} }' />
<div data-bind='component: { name: "string-input-pane", params: { data: stringInputPane} }' />
<KOCommentIfStart if="isGitHubPaneEnabled" />
<div data-bind='component: { name: "github-repos-pane", params: { data: gitHubReposPane } }' />
<KOCommentEnd />
<KOCommentIfStart if="isPublishNotebookPaneEnabled" />
<div data-bind="react: publishNotebookPaneAdapter" />
<KOCommentEnd />
{showDialog && <Dialog {...dialogProps} />}
</div>
);

View File

@@ -2,11 +2,11 @@ import { IResourceProviderClient } from "../ResourceProvider/IResourceProviderCl
import { NotebookWorkspace } from "../Contracts/DataModels";
export class NotebookWorkspaceSettingsProviderClient implements IResourceProviderClient<string> {
public async deleteAsync(url: string, apiVersion: string): Promise<void> {
public async deleteAsync(_url: string, _apiVersion: string): Promise<void> {
throw new Error("Not yet implemented");
}
public async postAsync(url: string, body: any, apiVersion: string): Promise<any> {
public async postAsync(_url: string, _body: any, _apiVersion: string): Promise<any> {
return Promise.resolve({
notebookServerEndpoint: "http://localhost:8888",
username: "",
@@ -14,37 +14,37 @@ export class NotebookWorkspaceSettingsProviderClient implements IResourceProvide
});
}
public async getAsync(url: string, apiVersion: string): Promise<string> {
public async getAsync(): Promise<string> {
throw new Error("Not yet implemented");
}
public async putAsync(url: string, body: any, apiVersion: string): Promise<string> {
public async putAsync(): Promise<string> {
throw new Error("Not yet implemented");
}
public async patchAsync(url: string, apiVersion: string): Promise<string> {
public async patchAsync(): Promise<string> {
throw new Error("Not yet implemented");
}
}
export class NotebookWorkspaceResourceProviderClient implements IResourceProviderClient<NotebookWorkspace> {
public async deleteAsync(url: string, apiVersion: string): Promise<void> {
public async deleteAsync(): Promise<void> {
throw new Error("Not yet implemented");
}
public async postAsync(url: string, body: any, apiVersion: string): Promise<NotebookWorkspace> {
public async postAsync(): Promise<NotebookWorkspace> {
throw new Error("Not yet implemented");
}
public async getAsync(url: string, apiVersion: string): Promise<NotebookWorkspace | NotebookWorkspace[]> {
public async getAsync(): Promise<NotebookWorkspace | NotebookWorkspace[]> {
throw new Error("Not yet implemented");
}
public async putAsync(url: string, body: any, apiVersion: string): Promise<NotebookWorkspace> {
public async putAsync(): Promise<NotebookWorkspace> {
throw new Error("Not yet implemented");
}
public async patchAsync(url: string, body: any, apiVersion: string): Promise<NotebookWorkspace> {
public async patchAsync(): Promise<NotebookWorkspace> {
throw new Error("Not yet implemented");
}
}

View File

@@ -9,7 +9,7 @@ import { HttpHeaders, TerminalQueryParams } from "../Common/Constants";
const getUrlVars = (): { [key: string]: string } => {
const vars: { [key: string]: string } = {};
window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, (m, key, value): string => {
window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, (_m, key, value): string => {
vars[key] = decodeURIComponent(value);
return value;
});

View File

@@ -1,8 +1,8 @@
import { decryptJWTToken } from "./AuthorizationUtils";
import { userContext } from "../UserContext";
import { decryptJWTToken } from "./AuthorizationUtils";
export function getFullName(): string {
const authToken = userContext.authorizationToken;
const props = decryptJWTToken(authToken);
return props.name;
}
export const getFullName = (): string => {
const { authorizationToken } = userContext;
const { name } = decryptJWTToken(authorizationToken);
return name;
};

View File

@@ -4,7 +4,8 @@
"noEmit": true,
"strictNullChecks": true,
"strict": true,
"noUnusedLocals": true
"noUnusedLocals": true,
"noUnusedParameters": true
},
"files": [
"./src/AuthType.ts",
@@ -14,7 +15,6 @@
"./src/Common/DeleteFeedback.ts",
"./src/Common/DocumentUtility.ts",
"./src/Common/EnvironmentUtility.ts",
"./src/Common/HashMap.ts",
"./src/Common/HeadersUtility.test.ts",
"./src/Common/HeadersUtility.ts",
"./src/Common/Logger.ts",

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable no-undef */
require("dotenv/config");
const path = require("path");
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
@@ -83,7 +85,8 @@ const typescriptRule = {
exclude: /node_modules/,
};
module.exports = function (env = {}, argv = {}) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
module.exports = function (_env = {}, argv = {}) {
const mode = argv.mode || "development";
const rules = [fontRule, lessRule, imagesRule, cssRule, htmlRule, typescriptRule];
const envVars = {
@@ -211,6 +214,7 @@ module.exports = function (env = {}, argv = {}) {
util: true,
tls: "empty",
net: "empty",
fs: "empty",
},
output: {
chunkFilename: "[name].[chunkhash:6].js",
@@ -264,7 +268,7 @@ module.exports = function (env = {}, argv = {}) {
target: "https://main.documentdb.ext.azure.com",
changeOrigin: true,
logLevel: "debug",
bypass: function (req, res, proxyOptions) {
bypass: (req, res) => {
if (req.method === "OPTIONS") {
res.statusCode = 200;
res.send();
@@ -304,8 +308,5 @@ module.exports = function (env = {}, argv = {}) {
},
},
stats: "minimal",
node: {
fs: "empty",
},
};
};