marge master conflict

This commit is contained in:
hardiknai-techm
2021-04-28 19:58:09 +05:30
94 changed files with 40196 additions and 9692 deletions

View File

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

30401
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -178,7 +178,7 @@
"typescript": "4.2.4", "typescript": "4.2.4",
"url-loader": "1.1.1", "url-loader": "1.1.1",
"wait-on": "4.0.2", "wait-on": "4.0.2",
"webpack": "4.43.0", "webpack": "4.46.0",
"webpack-bundle-analyzer": "3.6.1", "webpack-bundle-analyzer": "3.6.1",
"webpack-cli": "3.3.10", "webpack-cli": "3.3.10",
"webpack-dev-server": "3.11.0" "webpack-dev-server": "3.11.0"

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ export interface TableEntityProps {
entityValuePlaceholder: string; entityValuePlaceholder: string;
entityValue: string | Date; entityValue: string | Date;
isEntityTypeDate: boolean; isEntityTypeDate: boolean;
isEntityValueDisable?: boolean;
entityTimeValue: string; entityTimeValue: string;
entityValueType: string; entityValueType: string;
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void; onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
@@ -22,6 +23,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
entityValueType, entityValueType,
onEntityValueChange, onEntityValueChange,
onSelectDate, onSelectDate,
isEntityValueDisable,
onEntityTimeValueChange, onEntityTimeValueChange,
}: TableEntityProps): JSX.Element => { }: TableEntityProps): JSX.Element => {
if (isEntityTypeDate) { if (isEntityTypeDate) {
@@ -33,6 +35,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
value={entityValue && new Date(entityValue)} value={entityValue && new Date(entityValue)}
ariaLabel={entityValuePlaceholder} ariaLabel={entityValuePlaceholder}
onSelectDate={onSelectDate} onSelectDate={onSelectDate}
disabled={isEntityValueDisable}
/> />
<TextField <TextField
label={entityValueLabel && entityValueLabel} label={entityValueLabel && entityValueLabel}
@@ -41,6 +44,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
type="time" type="time"
value={entityTimeValue} value={entityTimeValue}
onChange={onEntityTimeValueChange} onChange={onEntityTimeValueChange}
disabled={isEntityValueDisable}
/> />
</> </>
); );
@@ -52,6 +56,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
className="addEntityTextField" className="addEntityTextField"
id="entityValueId" id="entityValueId"
autoFocus autoFocus
disabled={isEntityValueDisable}
type={entityValueType} type={entityValueType}
placeholder={entityValuePlaceholder} placeholder={entityValuePlaceholder}
value={typeof entityValue === "string" && entityValue} value={typeof entityValue === "string" && entityValue}

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("b", 2);
cache.set("c", 3); cache.set("c", 3);
cache.set("d", 4); 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", () => { 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 Map<string, T> {
constructor(private limit: number) {
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) {
super(); super();
this.keyQueue = [];
this.maxNbElements = maxNbElements;
this.clear();
} }
public clear(): void { public get(key: string): T | undefined {
super.clear(); return this.touch(key);
this.keyQueue = [];
} }
public get(key: string): T { public set(key: string, value: T): this {
this.markKeyAsTouched(key); if (this.size === this.limit) {
return super.get(key); this.delete(this.keys().next().value);
} }
public set(key: string, value: T): void { return this.touch(key, value), this;
}
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); super.set(key, value);
this.markKeyAsTouched(key);
if (super.size() > this.maxNbElements && key !== this.keyQueue[0]) {
this.reduceCacheSize();
}
} }
/** return value;
* 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);
}
}
/**
* 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);
} }
} }

View File

@@ -5,16 +5,16 @@ import * as ViewModels from "../Contracts/ViewModels";
import Explorer from "../Explorer/Explorer"; import Explorer from "../Explorer/Explorer";
import DocumentsTab from "../Explorer/Tabs/DocumentsTab"; import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
import DocumentId from "../Explorer/Tree/DocumentId"; import DocumentId from "../Explorer/Tree/DocumentId";
import { userContext } from "../UserContext";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import * as QueryUtils from "../Utils/QueryUtils"; import * as QueryUtils from "../Utils/QueryUtils";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants"; import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import { userContext } from "../UserContext";
import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage";
import { createCollection } from "./dataAccess/createCollection"; import { createCollection } from "./dataAccess/createCollection";
import { handleError } from "./ErrorHandlingUtils";
import { createDocument } from "./dataAccess/createDocument"; import { createDocument } from "./dataAccess/createDocument";
import { deleteDocument } from "./dataAccess/deleteDocument"; import { deleteDocument } from "./dataAccess/deleteDocument";
import { queryDocuments } from "./dataAccess/queryDocuments"; import { queryDocuments } from "./dataAccess/queryDocuments";
import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage";
import { handleError } from "./ErrorHandlingUtils";
export class QueriesClient { export class QueriesClient {
private static readonly PartitionKey: DataModels.PartitionKey = { private static readonly PartitionKey: DataModels.PartitionKey = {
@@ -211,7 +211,7 @@ export class QueriesClient {
} }
private fetchQueriesQuery(): string { private fetchQueriesQuery(): string {
if (this.container.isPreferredApiMongoDB()) { if (userContext.apiType === "Mongo") {
return QueriesClient.FetchMongoQuery; return QueriesClient.FetchMongoQuery;
} }
return QueriesClient.FetchQuery; return QueriesClient.FetchQuery;

View File

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

View File

@@ -32,6 +32,7 @@ export interface TableEntityProps {
options: { key: string; text: string }[]; options: { key: string; text: string }[];
isPropertyTypeDisable: boolean; isPropertyTypeDisable: boolean;
entityTimeValue: string; entityTimeValue: string;
isEntityValueDisable?: boolean;
onDeleteEntity?: () => void; onDeleteEntity?: () => void;
onEditEntity?: () => void; onEditEntity?: () => void;
onEntityPropertyChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void; onEntityPropertyChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
@@ -55,6 +56,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
isPropertyTypeDisable, isPropertyTypeDisable,
isEntityTypeDate, isEntityTypeDate,
entityTimeValue, entityTimeValue,
isEntityValueDisable,
onEditEntity, onEditEntity,
onDeleteEntity, onDeleteEntity,
onEntityPropertyChange, onEntityPropertyChange,
@@ -113,6 +115,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
<EntityValue <EntityValue
entityValueLabel={entityValueLabel} entityValueLabel={entityValueLabel}
entityValueType={getEntityValueType()} entityValueType={getEntityValueType()}
isEntityValueDisable={isEntityValueDisable}
entityValuePlaceholder={entityValuePlaceholder} entityValuePlaceholder={entityValuePlaceholder}
entityValue={entityValue} entityValue={entityValue}
isEntityTypeDate={isEntityTypeDate} isEntityTypeDate={isEntityTypeDate}
@@ -121,10 +124,11 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
onSelectDate={onSelectDate} onSelectDate={onSelectDate}
onEntityTimeValueChange={onEntityTimeValueChange} onEntityTimeValueChange={onEntityTimeValueChange}
/> />
{!isEntityValueDisable && (
<TooltipHost content="Edit property" id="editTooltip"> <TooltipHost content="Edit property" id="editTooltip">
<Image {...imageProps} src={EditIcon} alt="editEntity" id="editEntity" onClick={onEditEntity} /> <Image {...imageProps} src={EditIcon} alt="editEntity" id="editEntity" onClick={onEditEntity} />
</TooltipHost> </TooltipHost>
)}
{isDeleteOptionVisible && userContext.apiType !== "Cassandra" && ( {isDeleteOptionVisible && userContext.apiType !== "Cassandra" && (
<TooltipHost content="Delete property" id="deleteTooltip"> <TooltipHost content="Delete property" id="deleteTooltip">
<Image {...imageProps} src={DeleteIcon} alt="delete entity" id="deleteEntity" onClick={onDeleteEntity} /> <Image {...imageProps} src={DeleteIcon} alt="delete entity" id="deleteEntity" onClick={onDeleteEntity} />

View File

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

View File

@@ -26,5 +26,4 @@ ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPane
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent()); ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent()); ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent());
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); 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()); ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());

View File

@@ -63,7 +63,7 @@ export class ResourceTreeContextMenuButtonFactory {
}); });
} }
if (container.isPreferredApiMongoDB()) { if (userContext.apiType === "Mongo") {
items.push({ items.push({
iconSrc: AddSqlQueryIcon, iconSrc: AddSqlQueryIcon,
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null), onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null),

View File

@@ -1,6 +1,6 @@
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { loadMonaco, monaco } from "../../LazyMonaco";
import template from "./diff-editor-component.html"; import template from "./diff-editor-component.html";
import * as monaco from "monaco-editor";
/** /**
* Helper class for ko component registration * Helper class for ko component registration
@@ -92,7 +92,7 @@ export class DiffEditorViewModel {
/** /**
* Create the monaco editor on diff mode and attach to DOM * Create the monaco editor on diff mode and attach to DOM
*/ */
protected createDiffEditor( protected async createDiffEditor(
originalContent: string, originalContent: string,
modifiedContent: string, modifiedContent: string,
createCallback: (e: monaco.editor.IStandaloneDiffEditor) => void createCallback: (e: monaco.editor.IStandaloneDiffEditor) => void
@@ -111,7 +111,7 @@ export class DiffEditorViewModel {
} }
const language = this.params.editorLanguage || "json"; const language = this.params.editorLanguage || "json";
const monaco = await loadMonaco();
const originalModel = monaco.editor.createModel(originalContent, language); const originalModel = monaco.editor.createModel(originalContent, language);
const modifiedModel = monaco.editor.createModel(modifiedContent, language); const modifiedModel = monaco.editor.createModel(modifiedContent, language);
const diffEditor: monaco.editor.IStandaloneDiffEditor = monaco.editor.createDiffEditor( 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 { JsonEditorParams, JsonEditorViewModel } from "../JsonEditor/JsonEditorComponent";
import template from "./editor-component.html"; 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 * Helper class for ko component registration
@@ -49,15 +48,17 @@ class EditorViewModel extends JsonEditorViewModel {
return this.params.contentType; return this.params.contentType;
} }
protected registerCompletionItemProvider() { protected async registerCompletionItemProvider() {
let sqlCompletionItemProvider = new SqlCompletionItemProvider();
if (EditorViewModel.providerRegistered.indexOf("sql") < 0) { 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"); 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); return ErrorMarkProvider.getErrorMark(input);
} }
} }

View File

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

View File

@@ -128,21 +128,21 @@ class InputTypeaheadViewModel {
}, },
}, },
callback: { callback: {
onClick: (node: any, a: any, item: OnClickItem, event: any) => { onClick: (_node: unknown, _a: unknown, item: OnClickItem) => {
cache.selection = item; cache.selection = item;
if (params.selection) { if (params.selection) {
params.selection(item); params.selection(item);
} }
}, },
onResult(node: any, query: any, result: any, resultCount: any, resultCountPerGroup: any) { onResult(_node: unknown, query: any) {
cache.inputValue = query; cache.inputValue = query;
if (params.inputValue) { if (params.inputValue) {
params.inputValue(query); params.inputValue(query);
} }
}, },
}, },
template: (query: string, item: any) => { template: (_query: string, item: any) => {
// Don't display id if caption *IS* the id // Don't display id if caption *IS* the id
return item.caption === item.value return item.caption === item.value
? "<span>{{caption}}</span>" ? "<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 * as ViewModels from "../../../Contracts/ViewModels";
import { loadMonaco, monaco } from "../../LazyMonaco";
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel"; import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
import template from "./json-editor-component.html"; import template from "./json-editor-component.html";
@@ -88,7 +87,7 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
/** /**
* Create the monaco editor and attach to DOM * 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.registerCompletionItemProvider();
this.editorContainer = document.getElementById(this.getEditorId()); this.editorContainer = document.getElementById(this.getEditorId());
const options: monaco.editor.IEditorConstructionOptions = { const options: monaco.editor.IEditorConstructionOptions = {
@@ -102,6 +101,7 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
}; };
this.editorContainer.innerHTML = ""; this.editorContainer.innerHTML = "";
const monaco = await loadMonaco();
createCallback(monaco.editor.create(this.editorContainer, options)); createCallback(monaco.editor.create(this.editorContainer, options));
} }
@@ -109,15 +109,16 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
protected registerCompletionItemProvider() {} protected registerCompletionItemProvider() {}
// Interface. Will be implemented in children editor view model such as EditorViewModel. // Interface. Will be implemented in children editor view model such as EditorViewModel.
protected getErrorMarkers(input: string): Q.Promise<monaco.editor.IMarkerData[]> { protected async getErrorMarkers(_: string): Promise<monaco.editor.IMarkerData[]> {
return Q.Promise(() => {}); return [];
} }
protected getEditorLanguage(): string { protected getEditorLanguage(): string {
return "json"; return "json";
} }
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) { protected async configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
const monaco = await loadMonaco();
this.editor = editor; this.editor = editor;
const queryEditorModel = this.editor.getModel(); const queryEditorModel = this.editor.getModel();
if (!this.params.isReadOnly && this.params.updatedContent) { if (!this.params.isReadOnly && this.params.updatedContent) {

View File

@@ -1,25 +1,25 @@
import { Card } from "@uifabric/react-cards"; import { Card } from "@uifabric/react-cards";
import { import {
BaseButton,
Button,
FontWeights, FontWeights,
Icon, Icon,
IconButton, IconButton,
Image, Image,
ImageFit, ImageFit,
Persona,
Text,
Link, Link,
BaseButton,
Button,
LinkBase, LinkBase,
Persona,
Separator, Separator,
TooltipHost,
Spinner, Spinner,
SpinnerSize, SpinnerSize,
Text,
TooltipHost,
} from "office-ui-fabric-react"; } 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 { IGalleryItem } from "../../../../Juno/JunoClient";
import * as FileSystemUtil from "../../../Notebook/FileSystemUtil"; import * as FileSystemUtil from "../../../Notebook/FileSystemUtil";
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
export interface GalleryCardComponentProps { export interface GalleryCardComponentProps {
data: IGalleryItem; data: IGalleryItem;
@@ -34,166 +34,48 @@ export interface GalleryCardComponentProps {
onDeleteClick: (beforeDelete: () => void, afterDelete: () => void) => void; onDeleteClick: (beforeDelete: () => void, afterDelete: () => void) => void;
} }
interface GalleryCardComponentState { export const GalleryCardComponent: FunctionComponent<GalleryCardComponentProps> = ({
isDeletingPublishedNotebook: boolean; 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> { const [isDeletingPublishedNotebook, setIsDeletingPublishedNotebook] = useState<boolean>(false);
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;
constructor(props: GalleryCardComponentProps) { const cardButtonsVisible = isFavorite !== undefined || showDownload || showDelete;
super(props);
this.state = {
isDeletingPublishedNotebook: false,
};
}
public render(): JSX.Element {
const cardButtonsVisible = this.props.isFavorite !== undefined || this.props.showDownload || this.props.showDelete;
const options: Intl.DateTimeFormatOptions = { const options: Intl.DateTimeFormatOptions = {
year: "numeric", year: "numeric",
month: "short", month: "short",
day: "numeric", day: "numeric",
}; };
const dateString = new Date(this.props.data.created).toLocaleString("default", options); const dateString = new Date(data.created).toLocaleString("default", options);
const cardTitle = FileSystemUtil.stripExtension(this.props.data.name, "ipynb"); const cardTitle = FileSystemUtil.stripExtension(data.name, "ipynb");
return ( const renderTruncatedDescription = (): string => {
<Card let truncatedDescription = data.description.substr(0, cardDescriptionMaxChars);
style={{ background: "white" }} if (data.description.length > cardDescriptionMaxChars) {
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) {
truncatedDescription = `${truncatedDescription} ...`; truncatedDescription = `${truncatedDescription} ...`;
} }
return truncatedDescription; return truncatedDescription;
}; };
private generateIconText = (iconName: string, text: string): JSX.Element => { const generateIconText = (iconName: string, text: string): JSX.Element => {
return ( 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} <Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} /> {text}
</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 * 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) * to do the following (from https://developer.microsoft.com/en-us/fluentui#/controls/web/button)
*/ */
private generateIconButtonWithTooltip = ( const generateIconButtonWithTooltip = (
iconName: string, iconName: string,
title: string, title: string,
horizontalAlign: "right" | "left", horizontalAlign: "right" | "left",
@@ -220,13 +102,13 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
iconProps={{ iconName }} iconProps={{ iconName }}
title={title} title={title}
ariaLabel={title} ariaLabel={title}
onClick={(event) => this.onClick(event, activate)} onClick={(event) => handlerOnClick(event, activate)}
/> />
</TooltipHost> </TooltipHost>
); );
}; };
private onClick = ( const handlerOnClick = (
event: event:
| React.MouseEvent<HTMLElement | HTMLAnchorElement | HTMLButtonElement | LinkBase, MouseEvent> | React.MouseEvent<HTMLElement | HTMLAnchorElement | HTMLButtonElement | LinkBase, MouseEvent>
| React.MouseEvent< | React.MouseEvent<
@@ -239,4 +121,112 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
event.preventDefault(); event.preventDefault();
activate(); 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 { shallow } from "enzyme";
import React from "react"; import React from "react";
import { CodeOfConductComponent, CodeOfConductComponentProps } from "./CodeOfConductComponent"; import { CodeOfConductComponent, CodeOfConductComponentProps } from ".";
import { JunoClient } from "../../../Juno/JunoClient"; import { HttpStatusCodes } from "../../../../Common/Constants";
import { HttpStatusCodes } from "../../../Common/Constants"; import { JunoClient } from "../../../../Juno/JunoClient";
describe("CodeOfConductComponent", () => { describe("CodeOfConductComponent", () => {
let codeOfConductProps: CodeOfConductComponentProps; 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 "./GalleryViewerComponent.less";
import { InfoComponent } from "./InfoComponent/InfoComponent"; import { InfoComponent } from "./InfoComponent/InfoComponent";
const CARD_WIDTH = 256;
export interface GalleryViewerComponentProps { export interface GalleryViewerComponentProps {
container?: Explorer; container?: Explorer;
junoClient: JunoClient; junoClient: JunoClient;
@@ -643,7 +644,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private getPageSpecification = (itemIndex?: number, visibleRect?: IRectangle): IPageSpecification => { private getPageSpecification = (itemIndex?: number, visibleRect?: IRectangle): IPageSpecification => {
if (itemIndex === 0) { 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; this.rowCount = GalleryViewerComponent.rowsPerPage;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ import React from "react";
import _ from "underscore"; import _ from "underscore";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import { ExplorerMetrics } from "../Common/Constants"; import { ExplorerMetrics } from "../Common/Constants";
import { readCollection } from "../Common/dataAccess/readCollection"; import { readCollection } from "../Common/dataAccess/readCollection";
@@ -65,16 +64,15 @@ import { LoadQueryPane } from "./Panes/LoadQueryPane/LoadQueryPane";
import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane"; import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane";
import { SettingsPane } from "./Panes/SettingsPane/SettingsPane"; import { SettingsPane } from "./Panes/SettingsPane/SettingsPane";
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel"; import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
import { StringInputPane } from "./Panes/StringInputPane"; import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel"; import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane"; import { EditTableEntityPanel } from "./Panes/Tables/EditTableEntityPanel";
import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel"; import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel";
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane"; import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane"; import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel"; import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel";
import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel"; import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import type { GalleryTabOptions } from "./Tabs/GalleryTab";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import QueryTablesTab from "./Tabs/QueryTablesTab"; import QueryTablesTab from "./Tabs/QueryTablesTab";
import { TabsManager } from "./Tabs/TabsManager"; import { TabsManager } from "./Tabs/TabsManager";
@@ -121,11 +119,6 @@ export default class Explorer {
* Use userContext.apiType instead * Use userContext.apiType instead
* */ * */
public defaultExperience: ko.Observable<string>; public defaultExperience: ko.Observable<string>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Mongo"
* */
public isPreferredApiMongoDB: ko.Computed<boolean>;
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>; public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
/** /**
* @deprecated * @deprecated
@@ -174,11 +167,8 @@ export default class Explorer {
public addDatabasePane: AddDatabasePane; public addDatabasePane: AddDatabasePane;
public addCollectionPane: AddCollectionPane; public addCollectionPane: AddCollectionPane;
public graphStylingPane: GraphStylingPane; public graphStylingPane: GraphStylingPane;
public editTableEntityPane: EditTableEntityPane;
public cassandraAddCollectionPane: CassandraAddCollectionPane; public cassandraAddCollectionPane: CassandraAddCollectionPane;
public stringInputPane: StringInputPane;
public gitHubReposPane: ContextualPaneBase; public gitHubReposPane: ContextualPaneBase;
public publishNotebookPaneAdapter: ReactAdapter;
// features // features
public isGitHubPaneEnabled: ko.Observable<boolean>; public isGitHubPaneEnabled: ko.Observable<boolean>;
@@ -415,27 +405,6 @@ export default class Explorer {
) !== undefined ) !== undefined
); );
this.isPreferredApiMongoDB = ko.computed(() => {
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.MongoDB.toLowerCase()) {
return true;
}
if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.ApiForMongoDB.toLowerCase()) {
return true;
}
if (
this.databaseAccount &&
this.databaseAccount() &&
this.databaseAccount().kind.toLowerCase() === Constants.AccountKind.MongoDB
) {
return true;
}
return false;
});
this.isEnableMongoCapabilityPresent = ko.computed(() => { this.isEnableMongoCapabilityPresent = ko.computed(() => {
const capabilities = this.databaseAccount && this.databaseAccount()?.properties?.capabilities; const capabilities = this.databaseAccount && this.databaseAccount()?.properties?.capabilities;
if (!capabilities) { if (!capabilities) {
@@ -501,13 +470,6 @@ export default class Explorer {
container: this, container: this,
}); });
this.editTableEntityPane = new EditTableEntityPane({
id: "edittableentitypane",
visible: ko.observable<boolean>(false),
container: this,
});
this.cassandraAddCollectionPane = new CassandraAddCollectionPane({ this.cassandraAddCollectionPane = new CassandraAddCollectionPane({
id: "cassandraaddcollectionpane", id: "cassandraaddcollectionpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -515,13 +477,6 @@ export default class Explorer {
container: this, container: this,
}); });
this.stringInputPane = new StringInputPane({
id: "stringinputpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.tabsManager = params?.tabsManager ?? new TabsManager(); this.tabsManager = params?.tabsManager ?? new TabsManager();
this.tabsManager.openedTabs.subscribe((tabs) => { this.tabsManager.openedTabs.subscribe((tabs) => {
if (tabs.length === 0) { if (tabs.length === 0) {
@@ -534,9 +489,7 @@ export default class Explorer {
this.addDatabasePane, this.addDatabasePane,
this.addCollectionPane, this.addCollectionPane,
this.graphStylingPane, this.graphStylingPane,
this.editTableEntityPane,
this.cassandraAddCollectionPane, this.cassandraAddCollectionPane,
this.stringInputPane,
]; ];
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText)); this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
this.isTabsContentExpanded = ko.observable(false); this.isTabsContentExpanded = ko.observable(false);
@@ -605,7 +558,6 @@ export default class Explorer {
this.addCollectionPane.collectionIdTitle("Table id"); this.addCollectionPane.collectionIdTitle("Table id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.refreshTreeTitle("Refresh tables"); this.refreshTreeTitle("Refresh tables");
this.editTableEntityPane.title("Edit Table Entity");
this.tableDataClient = new TablesAPIDataClient(); this.tableDataClient = new TablesAPIDataClient();
break; break;
case "Cassandra": case "Cassandra":
@@ -619,7 +571,6 @@ export default class Explorer {
this.addCollectionPane.collectionIdTitle("Table id"); this.addCollectionPane.collectionIdTitle("Table id");
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table"); this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
this.refreshTreeTitle("Refresh tables"); this.refreshTreeTitle("Refresh tables");
this.editTableEntityPane.title("Edit Table Row");
this.tableDataClient = new CassandraAPIDataClient(); this.tableDataClient = new CassandraAPIDataClient();
break; break;
} }
@@ -1432,7 +1383,6 @@ export default class Explorer {
): Promise<void> { ): Promise<void> {
if (this.notebookManager) { if (this.notebookManager) {
await this.notebookManager.openPublishNotebookPane(name, content, parentDomElement); await this.notebookManager.openPublishNotebookPane(name, content, parentDomElement);
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
this.isPublishNotebookPaneEnabled(true); this.isPublishNotebookPaneEnabled(true);
} }
} }
@@ -1545,7 +1495,7 @@ export default class Explorer {
return true; return true;
} }
public renameNotebook(notebookFile: NotebookContentItem): Q.Promise<NotebookContentItem> { public renameNotebook(notebookFile: NotebookContentItem): void {
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to rename notebook, but notebook is not enabled"; const error = "Attempt to rename notebook, but notebook is not enabled";
handleError(error, "Explorer/renameNotebook"); handleError(error, "Explorer/renameNotebook");
@@ -1561,57 +1511,59 @@ export default class Explorer {
); );
if (openedNotebookTabs.length > 0) { if (openedNotebookTabs.length > 0) {
this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again."); 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) { if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
const error = "Attempt to create notebook directory, but notebook is not enabled"; const error = "Attempt to create notebook directory, but notebook is not enabled";
handleError(error, "Explorer/onCreateDirectory"); handleError(error, "Explorer/onCreateDirectory");
throw new Error(error); throw new Error(error);
} }
const result = this.stringInputPane.openWithOptions<NotebookContentItem>({ this.openSidePanel(
errorMessage: "Could not create directory ", "",
inProgressMessage: "Creating directory ", <StringInputPane
successMessage: "Created directory ", explorer={this}
inputLabel: "Enter new directory name", closePanel={() => {
paneTitle: "Create new directory", this.closeSidePanel();
submitButtonLabel: "Create", this.resourceTree.triggerRender();
defaultInput: "", }}
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input), errorMessage="Could not create directory "
}); inProgressMessage="Creating directory "
result.then(() => this.resourceTree.triggerRender()); successMessage="Created directory "
return result; 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> { public readFile(notebookFile: NotebookContentItem): Promise<string> {
@@ -1935,8 +1887,25 @@ export default class Explorer {
const title = "Gallery"; const title = "Gallery";
const hashLocation = "gallery"; const hashLocation = "gallery";
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default; const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
const galleryTab = this.tabsManager
.getTabs(ViewModels.CollectionTabKind.Gallery)
.find((tab) => tab.hashLocation() == hashLocation);
const galleryTabOptions: GalleryTabOptions = { if (galleryTab instanceof GalleryTab) {
this.tabsManager.activateTab(galleryTab);
} else {
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, account: userContext.databaseAccount,
container: this, container: this,
junoClient: this.notebookManager?.junoClient, junoClient: this.notebookManager?.junoClient,
@@ -1944,24 +1913,9 @@ export default class Explorer {
notebookUrl, notebookUrl,
galleryItem, galleryItem,
isFavorite, 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));
} }
} }
@@ -2163,8 +2117,7 @@ export default class Explorer {
); );
} }
public openAddDatabasePane(): void { public openAddDatabasePane(): void {
let isEnable = true; if (userContext.features.enableKOPanel) {
if (isEnable) {
this.addDatabasePane.open(); this.addDatabasePane.open();
document.getElementById("linkAddDatabase").focus(); document.getElementById("linkAddDatabase").focus();
} else { } else {
@@ -2228,6 +2181,19 @@ export default class Explorer {
); );
} }
public openEditTableEntityPanel(queryTablesTab: QueryTablesTab, tableEntityListViewModel: TableListViewModal): void {
this.openSidePanel(
"Edit Table Entity",
<EditTableEntityPanel
explorer={this}
closePanel={this.closeSidePanel}
queryTablesTab={queryTablesTab}
tableEntityListViewModel={tableEntityListViewModel}
cassandraApiClient={new CassandraAPIDataClient()}
/>
);
}
public openTableSelectQueryPanel(queryViewModal: QueryViewModel): void { public openTableSelectQueryPanel(queryViewModal: QueryViewModel): void {
this.openSidePanel( this.openSidePanel(
"Select Column", "Select Column",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -220,7 +220,7 @@ export class NotebookContentClient {
return this.contentProvider return this.contentProvider
.remove(this.getServerConfig(), path) .remove(this.getServerConfig(), path)
.toPromise() .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 * 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 type { IContentProvider } from "@nteract/core";
import ko from "knockout"; import ko from "knockout";
import React from "react"; import React from "react";
@@ -22,7 +22,7 @@ import Explorer from "../Explorer";
import { ContextualPaneBase } from "../Panes/ContextualPaneBase"; import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
import { GitHubReposPane } from "../Panes/GitHubReposPane"; import { GitHubReposPane } from "../Panes/GitHubReposPane";
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter"; import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider"; import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
import { NotebookContainerClient } from "./NotebookContainerClient"; import { NotebookContainerClient } from "./NotebookContainerClient";
@@ -53,7 +53,6 @@ export default class NotebookManager {
private gitHubClient: GitHubClient; private gitHubClient: GitHubClient;
public gitHubReposPane: ContextualPaneBase; public gitHubReposPane: ContextualPaneBase;
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
public initialize(params: NotebookManagerOptions): void { public initialize(params: NotebookManagerOptions): void {
this.params = params; this.params = params;
@@ -91,8 +90,6 @@ export default class NotebookManager {
this.notebookContentProvider this.notebookContentProvider
); );
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
this.gitHubOAuthService.getTokenObservable().subscribe((token) => { this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
this.gitHubClient.setToken(token?.access_token); this.gitHubClient.setToken(token?.access_token);
@@ -123,7 +120,20 @@ export default class NotebookManager {
content: NotebookPaneContent, content: NotebookPaneContent,
parentDomElement: HTMLElement parentDomElement: HTMLElement
): Promise<void> { ): 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 { public openCopyNotebookPane(name: string, content: string): void {

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,14 +8,15 @@ describe("PublishNotebookPaneComponent", () => {
notebookName: "SampleNotebook.ipynb", notebookName: "SampleNotebook.ipynb",
notebookDescription: "sample description", notebookDescription: "sample description",
notebookTags: "tag1, tag2", notebookTags: "tag1, tag2",
imageSrc: "https://i.ytimg.com/vi/E_lByLdKeKY/maxresdefault.jpg",
notebookAuthor: "CosmosDB", notebookAuthor: "CosmosDB",
notebookCreatedDate: "2020-07-17T00:00:00Z", notebookCreatedDate: "2020-07-17T00:00:00Z",
notebookObject: undefined, notebookObject: undefined,
notebookParentDomElement: undefined, notebookParentDomElement: undefined,
onChangeName: undefined, setNotebookName: undefined,
onChangeDescription: undefined, setNotebookDescription: undefined,
onChangeTags: undefined, setNotebookTags: undefined,
onChangeImageSrc: undefined, setImageSrc: undefined,
onError: undefined, onError: undefined,
clearFormError: 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 { Object {
"author": "CosmosDB", "author": "CosmosDB",
"created": "2020-07-17T00:00:00Z", "created": "2020-07-17T00:00:00Z",
"description": "", "description": "sample description",
"downloads": undefined, "downloads": undefined,
"favorites": undefined, "favorites": undefined,
"gitSha": undefined, "gitSha": undefined,
@@ -99,12 +99,14 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
"pendingScanJobIds": undefined, "pendingScanJobIds": undefined,
"policyViolations": undefined, "policyViolations": undefined,
"tags": Array [ "tags": Array [
"", "tag1",
" tag2",
], ],
"thumbnailUrl": undefined, "thumbnailUrl": "https://i.ytimg.com/vi/E_lByLdKeKY/maxresdefault.jpg",
"views": undefined, "views": undefined,
} }
} }
onClick={[Function]}
showDelete={false} showDelete={false}
showDownload={false} showDownload={false}
/> />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,6 +21,7 @@ import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import { logConsoleError } from "../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
import * as QueryUtils from "../../Utils/QueryUtils"; import * as QueryUtils from "../../Utils/QueryUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
@@ -71,9 +72,7 @@ export default class DocumentsTab extends TabsBase {
constructor(options: ViewModels.DocumentsTabOptions) { constructor(options: ViewModels.DocumentsTabOptions) {
super(options); super(options);
this.isPreferredApiMongoDB = !!this.collection this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB;
? this.collection.container.isPreferredApiMongoDB()
: options.isPreferredApiMongoDB;
this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id"; this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id";

View File

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

View File

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

View File

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

View File

@@ -151,7 +151,7 @@ export default class QueryTablesTab extends TabsBase {
}; };
public onEditEntityClick = (): Q.Promise<any> => { public onEditEntityClick = (): Q.Promise<any> => {
this.tableCommands.editEntityCommand(this.tableEntityListViewModel()); this.container.openEditTableEntityPanel(this, this.tableEntityListViewModel());
return null; return null;
}; };

View File

@@ -1,5 +1,4 @@
import * as ko from "knockout"; import * as ko from "knockout";
import * as monaco from "monaco-editor";
import Q from "q"; import Q from "q";
import DiscardIcon from "../../../images/discard.svg"; import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.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 DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import { loadMonaco, monaco } from "../LazyMonaco";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
export default abstract class ScriptTabBase extends TabsBase implements ViewModels.WaitsForTemplate { 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; return !!value;
} }
private static _toSeverity(severity: string): monaco.MarkerSeverity { protected async _createBodyEditor() {
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() {
const id = this.editorId; const id = this.editorId;
const container = document.getElementById(id); const container = document.getElementById(id);
const options = { const options = {
@@ -341,7 +310,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
}; };
container.innerHTML = ""; container.innerHTML = "";
const monaco = await loadMonaco();
const editor = monaco.editor.create(container, options); const editor = monaco.editor.create(container, options);
this.editor(editor); this.editor(editor);
@@ -353,32 +322,4 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
const editorModel = this.editor().getModel(); const editorModel = this.editor().getModel();
this.editorContent(editorModel.getValue()); 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 id="content" className="flexContainer hideOverflows">
<div className="nav-tabs-margin"> <div className="nav-tabs-margin">
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist"> <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> </ul>
</div> </div>
<div className="tabPanesContainer"> <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> </div>
</div> </div>

View File

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

View File

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

View File

@@ -127,16 +127,12 @@ export default class Collection implements ViewModels.Collection {
this.partitionKey.paths[0]) || this.partitionKey.paths[0]) ||
null; null;
if (!!container.isPreferredApiMongoDB() && this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) { if (userContext.apiType === "Mongo" && this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) {
this.partitionKeyProperty = this.partitionKeyProperty.replace(/["]+/g, ""); this.partitionKeyProperty = this.partitionKeyProperty.replace(/["]+/g, "");
} }
// TODO #10738269 : Add this logic in a derived class for Mongo // TODO #10738269 : Add this logic in a derived class for Mongo
if ( if (userContext.apiType === "Mongo" && this.partitionKeyProperty && this.partitionKeyProperty.indexOf("$v") > -1) {
!!container.isPreferredApiMongoDB() &&
this.partitionKeyProperty &&
this.partitionKeyProperty.indexOf("$v") > -1
) {
// From $v.shard.$v.key.$v > shard.key // From $v.shard.$v.key.$v > shard.key
this.partitionKeyProperty = this.partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, ""); this.partitionKeyProperty = this.partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, "");
this.partitionKeyPropertyHeader = "/" + this.partitionKeyProperty; this.partitionKeyPropertyHeader = "/" + this.partitionKeyProperty;
@@ -1123,7 +1119,7 @@ export default class Collection implements ViewModels.Collection {
} else if (userContext.apiType === "Gremlin") { } else if (userContext.apiType === "Gremlin") {
this.onGraphDocumentsClick(); this.onGraphDocumentsClick();
return; return;
} else if (this.container.isPreferredApiMongoDB()) { } else if (userContext.apiType === "Mongo") {
this.onMongoDBDocumentsClick(); this.onMongoDBDocumentsClick();
return; return;
} }
@@ -1141,7 +1137,7 @@ export default class Collection implements ViewModels.Collection {
return "Rows"; return "Rows";
} else if (userContext.apiType === "Gremlin") { } else if (userContext.apiType === "Gremlin") {
return "Graph"; return "Graph";
} else if (this.container.isPreferredApiMongoDB()) { } else if (userContext.apiType === "Mongo") {
return "Documents"; return "Documents";
} }

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
import ko from "knockout"; import ko from "knockout";
import { HttpStatusCodes } from "../Common/Constants"; import { HttpStatusCodes } from "../Common/Constants";
import * as DataModels from "../Contracts/DataModels"; import * as DataModels from "../Contracts/DataModels";
import { JunoClient } from "../Juno/JunoClient"; import Explorer from "../Explorer/Explorer";
import { GitHubConnector, IGitHubConnectorParams } from "./GitHubConnector";
import { GitHubOAuthService } from "./GitHubOAuthService";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import NotebookManager from "../Explorer/Notebook/NotebookManager"; 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 = { const sampleDatabaseAccount: DataModels.DatabaseAccount = {
id: "id", id: "id",
@@ -85,25 +85,6 @@ describe("GitHubOAuthService", () => {
expect(newParams.get("state")).not.toEqual(initialParams.get("state")); 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 () => { it("finishOAuth updates token", async () => {
const data = { key: "value" }; const data = { key: "value" };
const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.OK, data }); const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.OK, data });

View File

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

View File

@@ -230,15 +230,10 @@ const App: React.FunctionComponent = () => {
<div data-bind='component: { name: "add-database-pane", params: {data: addDatabasePane} }' /> <div data-bind='component: { name: "add-database-pane", params: {data: addDatabasePane} }' />
<div data-bind='component: { name: "add-collection-pane", params: { data: addCollectionPane} }' /> <div data-bind='component: { name: "add-collection-pane", params: { data: addCollectionPane} }' />
<div data-bind='component: { name: "graph-styling-pane", params: { data: graphStylingPane} }' /> <div data-bind='component: { name: "graph-styling-pane", params: { data: graphStylingPane} }' />
<div data-bind='component: { name: "table-edit-entity-pane", params: { data: editTableEntityPane} }' />
<div data-bind='component: { name: "cassandra-add-collection-pane", params: { data: cassandraAddCollectionPane} }' /> <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" /> <KOCommentIfStart if="isGitHubPaneEnabled" />
<div data-bind='component: { name: "github-repos-pane", params: { data: gitHubReposPane } }' /> <div data-bind='component: { name: "github-repos-pane", params: { data: gitHubReposPane } }' />
<KOCommentEnd /> <KOCommentEnd />
<KOCommentIfStart if="isPublishNotebookPaneEnabled" />
<div data-bind="react: publishNotebookPaneAdapter" />
<KOCommentEnd />
{showDialog && <Dialog {...dialogProps} />} {showDialog && <Dialog {...dialogProps} />}
</div> </div>
); );

View File

@@ -2,11 +2,11 @@ import { IResourceProviderClient } from "../ResourceProvider/IResourceProviderCl
import { NotebookWorkspace } from "../Contracts/DataModels"; import { NotebookWorkspace } from "../Contracts/DataModels";
export class NotebookWorkspaceSettingsProviderClient implements IResourceProviderClient<string> { 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"); 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({ return Promise.resolve({
notebookServerEndpoint: "http://localhost:8888", notebookServerEndpoint: "http://localhost:8888",
username: "", 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"); 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"); 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"); throw new Error("Not yet implemented");
} }
} }
export class NotebookWorkspaceResourceProviderClient implements IResourceProviderClient<NotebookWorkspace> { 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"); 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"); 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"); 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"); 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"); throw new Error("Not yet implemented");
} }
} }

View File

@@ -175,10 +175,7 @@ export class TabRouteHandler {
databaseId, databaseId,
collectionId collectionId
); );
collection && userContext.apiType === "Mongo" && collection.onMongoDBDocumentsClick();
collection.container &&
collection.container.isPreferredApiMongoDB() &&
collection.onMongoDBDocumentsClick();
}); });
} }
@@ -188,10 +185,7 @@ export class TabRouteHandler {
databaseId, databaseId,
collectionId collectionId
); );
collection && collection && userContext.apiType === "Mongo" && collection.onSchemaAnalyzerClick();
collection.container &&
collection.container.isPreferredApiMongoDB() &&
collection.onSchemaAnalyzerClick();
}); });
} }
@@ -228,10 +222,7 @@ export class TabRouteHandler {
if (!!matchingTab) { if (!!matchingTab) {
matchingTab.onTabClick(); matchingTab.onTabClick();
} else { } else {
collection && userContext.apiType === "Mongo" && collection.onNewMongoQueryClick(collection, null);
collection.container &&
collection.container.isPreferredApiMongoDB() &&
collection.onNewMongoQueryClick(collection, null);
} }
}); });
} }
@@ -250,10 +241,7 @@ export class TabRouteHandler {
if (!!matchingTab) { if (!!matchingTab) {
matchingTab.onTabClick(); matchingTab.onTabClick();
} else { } else {
collection && userContext.apiType === "Mongo" && collection.onNewMongoShellClick();
collection.container &&
collection.container.isPreferredApiMongoDB() &&
collection.onNewMongoShellClick();
} }
}); });
} }

View File

@@ -9,7 +9,7 @@ import { HttpHeaders, TerminalQueryParams } from "../Common/Constants";
const getUrlVars = (): { [key: string]: string } => { const getUrlVars = (): { [key: string]: string } => {
const vars: { [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); vars[key] = decodeURIComponent(value);
return value; return value;
}); });

View File

@@ -21,7 +21,7 @@ interface UserContext {
readonly quotaId?: string; readonly quotaId?: string;
// API Type is not yet provided by ARM. You need to manually inspect all the capabilities+kind so we abstract that logic in userContext // API Type is not yet provided by ARM. You need to manually inspect all the capabilities+kind so we abstract that logic in userContext
// This is coming in a future Cosmos ARM API version as a prperty on databaseAccount // This is coming in a future Cosmos ARM API version as a prperty on databaseAccount
readonly apiType?: ApiType; apiType?: ApiType;
readonly isTryCosmosDBSubscription?: boolean; readonly isTryCosmosDBSubscription?: boolean;
readonly portalEnv?: PortalEnv; readonly portalEnv?: PortalEnv;
readonly features: Features; readonly features: Features;
@@ -47,8 +47,10 @@ const userContext: UserContext = {
}; };
function updateUserContext(newContext: Partial<UserContext>): void { function updateUserContext(newContext: Partial<UserContext>): void {
if (newContext.databaseAccount) {
newContext.apiType = apiType(newContext.databaseAccount);
}
Object.assign(userContext, newContext); Object.assign(userContext, newContext);
Object.assign(userContext, { apiType: apiType(userContext.databaseAccount) });
} }
function apiType(account: DatabaseAccount | undefined): ApiType { function apiType(account: DatabaseAccount | undefined): ApiType {

View File

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

View File

@@ -4,7 +4,8 @@
"noEmit": true, "noEmit": true,
"strictNullChecks": true, "strictNullChecks": true,
"strict": true, "strict": true,
"noUnusedLocals": true "noUnusedLocals": true,
"noUnusedParameters": true
}, },
"files": [ "files": [
"./src/AuthType.ts", "./src/AuthType.ts",
@@ -14,7 +15,6 @@
"./src/Common/DeleteFeedback.ts", "./src/Common/DeleteFeedback.ts",
"./src/Common/DocumentUtility.ts", "./src/Common/DocumentUtility.ts",
"./src/Common/EnvironmentUtility.ts", "./src/Common/EnvironmentUtility.ts",
"./src/Common/HashMap.ts",
"./src/Common/HeadersUtility.test.ts", "./src/Common/HeadersUtility.test.ts",
"./src/Common/HeadersUtility.ts", "./src/Common/HeadersUtility.ts",
"./src/Common/Logger.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"); require("dotenv/config");
const path = require("path"); const path = require("path");
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin"); const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
@@ -83,7 +85,8 @@ const typescriptRule = {
exclude: /node_modules/, 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 mode = argv.mode || "development";
const rules = [fontRule, lessRule, imagesRule, cssRule, htmlRule, typescriptRule]; const rules = [fontRule, lessRule, imagesRule, cssRule, htmlRule, typescriptRule];
const envVars = { const envVars = {
@@ -211,6 +214,7 @@ module.exports = function (env = {}, argv = {}) {
util: true, util: true,
tls: "empty", tls: "empty",
net: "empty", net: "empty",
fs: "empty",
}, },
output: { output: {
chunkFilename: "[name].[chunkhash:6].js", chunkFilename: "[name].[chunkhash:6].js",
@@ -264,7 +268,7 @@ module.exports = function (env = {}, argv = {}) {
target: "https://main.documentdb.ext.azure.com", target: "https://main.documentdb.ext.azure.com",
changeOrigin: true, changeOrigin: true,
logLevel: "debug", logLevel: "debug",
bypass: function (req, res, proxyOptions) { bypass: (req, res) => {
if (req.method === "OPTIONS") { if (req.method === "OPTIONS") {
res.statusCode = 200; res.statusCode = 200;
res.send(); res.send();
@@ -304,8 +308,5 @@ module.exports = function (env = {}, argv = {}) {
}, },
}, },
stats: "minimal", stats: "minimal",
node: {
fs: "empty",
},
}; };
}; };