mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-21 01:41:31 +00:00
marge master conflict
This commit is contained in:
@@ -5,7 +5,6 @@ src/Api/Apis.ts
|
||||
src/AuthType.ts
|
||||
src/Bindings/BindingHandlersRegisterer.ts
|
||||
src/Bindings/ReactBindingHandler.ts
|
||||
src/Common/ArrayHashMap.ts
|
||||
src/Common/Constants.ts
|
||||
src/Common/CosmosClient.test.ts
|
||||
src/Common/CosmosClient.ts
|
||||
@@ -13,15 +12,12 @@ src/Common/DataAccessUtilityBase.test.ts
|
||||
src/Common/DataAccessUtilityBase.ts
|
||||
src/Common/EditableUtility.ts
|
||||
src/Common/HashMap.test.ts
|
||||
src/Common/HashMap.ts
|
||||
src/Common/Logger.test.ts
|
||||
src/Common/MessageHandler.test.ts
|
||||
src/Common/MessageHandler.ts
|
||||
src/Common/MongoProxyClient.test.ts
|
||||
src/Common/MongoUtility.ts
|
||||
src/Common/NotificationsClientBase.ts
|
||||
src/Common/ObjectCache.test.ts
|
||||
src/Common/ObjectCache.ts
|
||||
src/Common/QueriesClient.ts
|
||||
src/Common/Splitter.ts
|
||||
src/Config.ts
|
||||
@@ -124,7 +120,7 @@ src/Explorer/Panes/GraphStylingPane.ts
|
||||
# src/Explorer/Panes/NewVertexPane.ts
|
||||
src/Explorer/Panes/PaneComponents.ts
|
||||
src/Explorer/Panes/RenewAdHocAccessPane.ts
|
||||
src/Explorer/Panes/StringInputPane.ts
|
||||
src/Explorer/Panes/SetupNotebooksPane.ts
|
||||
src/Explorer/Panes/SwitchDirectoryPane.ts
|
||||
src/Explorer/Panes/Tables/EditTableEntityPane.ts
|
||||
src/Explorer/Panes/Tables/EntityPropertyViewModel.ts
|
||||
|
||||
30401
package-lock.json
generated
30401
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -178,7 +178,7 @@
|
||||
"typescript": "4.2.4",
|
||||
"url-loader": "1.1.1",
|
||||
"wait-on": "4.0.2",
|
||||
"webpack": "4.43.0",
|
||||
"webpack": "4.46.0",
|
||||
"webpack-bundle-analyzer": "3.6.1",
|
||||
"webpack-cli": "3.3.10",
|
||||
"webpack-dev-server": "3.11.0"
|
||||
|
||||
@@ -22,13 +22,7 @@ export interface ReactAdapter {
|
||||
export class Registerer {
|
||||
public static register(): void {
|
||||
ko.bindingHandlers.react = {
|
||||
init: (
|
||||
element: any,
|
||||
wrappedValueAccessor: () => any,
|
||||
allBindings?: ko.AllBindings,
|
||||
viewModel?: any,
|
||||
bindingContext?: ko.BindingContext
|
||||
) => {
|
||||
init: (element: any, wrappedValueAccessor: () => any) => {
|
||||
const adapter: ReactAdapter = wrappedValueAccessor();
|
||||
|
||||
if (adapter.setElement) {
|
||||
|
||||
@@ -1,49 +1,9 @@
|
||||
import { HashMap } from "./HashMap";
|
||||
|
||||
/**
|
||||
* Hash map of arrays which allows to:
|
||||
* - push an item by key: add to array and create array if needed
|
||||
* - remove item by key: remove from array and delete array if needed
|
||||
*/
|
||||
|
||||
export class ArrayHashMap<T> {
|
||||
private store: HashMap<T[]>;
|
||||
|
||||
constructor() {
|
||||
this.store = new HashMap();
|
||||
}
|
||||
|
||||
public has(key: string): boolean {
|
||||
return this.store.has(key);
|
||||
}
|
||||
|
||||
public get(key: string): T[] {
|
||||
return this.store.get(key);
|
||||
}
|
||||
|
||||
public size(): number {
|
||||
return this.store.size();
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.store.clear();
|
||||
}
|
||||
|
||||
public keys(): string[] {
|
||||
return this.store.keys();
|
||||
}
|
||||
|
||||
public delete(key: string): boolean {
|
||||
return this.store.delete(key);
|
||||
}
|
||||
|
||||
public forEach(key: string, iteratorFct: (value: T) => void) {
|
||||
const values = this.store.get(key);
|
||||
if (values) {
|
||||
values.forEach((value) => iteratorFct(value));
|
||||
}
|
||||
}
|
||||
|
||||
export class ArrayHashMap<T> extends Map<string, T[]> {
|
||||
/**
|
||||
* Insert item into array.
|
||||
* If no array, create one.
|
||||
@@ -52,16 +12,8 @@ export class ArrayHashMap<T> {
|
||||
* @param item
|
||||
*/
|
||||
public push(key: string, item: T): void {
|
||||
let itemsArray: T[] = this.store.get(key);
|
||||
if (!itemsArray) {
|
||||
itemsArray = [item];
|
||||
this.store.set(key, itemsArray);
|
||||
return;
|
||||
}
|
||||
|
||||
if (itemsArray.indexOf(item) === -1) {
|
||||
itemsArray.push(item);
|
||||
}
|
||||
const array = this.get(key);
|
||||
array ? array.includes(item) || array.push(item) : this.set(key, [item]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,18 +22,11 @@ export class ArrayHashMap<T> {
|
||||
* @param key
|
||||
* @param itemToRemove
|
||||
*/
|
||||
public remove(key: string, itemToRemove: T) {
|
||||
if (!this.store.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemsArray = this.store.get(key);
|
||||
const index = itemsArray.indexOf(itemToRemove);
|
||||
if (index >= 0) {
|
||||
itemsArray.splice(index, 1);
|
||||
if (itemsArray.length === 0) {
|
||||
this.store.delete(key);
|
||||
}
|
||||
public remove(key: string, itemToRemove: T): void {
|
||||
const array = this.get(key);
|
||||
if (array) {
|
||||
const remaining = array.filter((item) => item !== itemToRemove);
|
||||
remaining.length ? this.set(key, remaining) : this.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface TableEntityProps {
|
||||
entityValuePlaceholder: string;
|
||||
entityValue: string | Date;
|
||||
isEntityTypeDate: boolean;
|
||||
isEntityValueDisable?: boolean;
|
||||
entityTimeValue: string;
|
||||
entityValueType: string;
|
||||
onEntityValueChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
||||
@@ -22,6 +23,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
||||
entityValueType,
|
||||
onEntityValueChange,
|
||||
onSelectDate,
|
||||
isEntityValueDisable,
|
||||
onEntityTimeValueChange,
|
||||
}: TableEntityProps): JSX.Element => {
|
||||
if (isEntityTypeDate) {
|
||||
@@ -33,6 +35,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
||||
value={entityValue && new Date(entityValue)}
|
||||
ariaLabel={entityValuePlaceholder}
|
||||
onSelectDate={onSelectDate}
|
||||
disabled={isEntityValueDisable}
|
||||
/>
|
||||
<TextField
|
||||
label={entityValueLabel && entityValueLabel}
|
||||
@@ -41,6 +44,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
||||
type="time"
|
||||
value={entityTimeValue}
|
||||
onChange={onEntityTimeValueChange}
|
||||
disabled={isEntityValueDisable}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -52,6 +56,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
||||
className="addEntityTextField"
|
||||
id="entityValueId"
|
||||
autoFocus
|
||||
disabled={isEntityValueDisable}
|
||||
type={entityValueType}
|
||||
placeholder={entityValuePlaceholder}
|
||||
value={typeof entityValue === "string" && entityValue}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ describe("Object cache", () => {
|
||||
cache.set("b", 2);
|
||||
cache.set("c", 3);
|
||||
cache.set("d", 4);
|
||||
expect(cache.size()).toBe(2);
|
||||
expect(cache.size).toBe(2);
|
||||
});
|
||||
|
||||
it("should remove first added element to keep size at limit", () => {
|
||||
|
||||
@@ -1,56 +1,27 @@
|
||||
import { HashMap } from "./HashMap";
|
||||
|
||||
export class ObjectCache<T> extends HashMap<T> {
|
||||
private keyQueue: string[]; // Last touched key FIFO to purge cache if too big.
|
||||
private maxNbElements: number;
|
||||
|
||||
public constructor(maxNbElements: number) {
|
||||
export class ObjectCache<T> extends Map<string, T> {
|
||||
constructor(private limit: number) {
|
||||
super();
|
||||
this.keyQueue = [];
|
||||
this.maxNbElements = maxNbElements;
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
super.clear();
|
||||
this.keyQueue = [];
|
||||
public get(key: string): T | undefined {
|
||||
return this.touch(key);
|
||||
}
|
||||
|
||||
public get(key: string): T {
|
||||
this.markKeyAsTouched(key);
|
||||
return super.get(key);
|
||||
}
|
||||
|
||||
public set(key: string, value: T): void {
|
||||
super.set(key, value);
|
||||
|
||||
this.markKeyAsTouched(key);
|
||||
|
||||
if (super.size() > this.maxNbElements && key !== this.keyQueue[0]) {
|
||||
this.reduceCacheSize();
|
||||
public set(key: string, value: T): this {
|
||||
if (this.size === this.limit) {
|
||||
this.delete(this.keys().next().value);
|
||||
}
|
||||
|
||||
return this.touch(key, value), this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate elements to keep the total number below the limit
|
||||
*/
|
||||
private reduceCacheSize(): void {
|
||||
// remove a key
|
||||
const oldKey = this.keyQueue.shift();
|
||||
if (oldKey) {
|
||||
super.delete(oldKey);
|
||||
private touch(key: string, value = super.get(key)) {
|
||||
// Map keeps (re) insertion order according to ES6 spec
|
||||
if (value) {
|
||||
this.delete(key);
|
||||
super.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bubble up this key as new.
|
||||
* @param key
|
||||
*/
|
||||
private markKeyAsTouched(key: string) {
|
||||
const n = this.keyQueue.indexOf(key);
|
||||
if (n > -1) {
|
||||
this.keyQueue.splice(n, 1);
|
||||
}
|
||||
this.keyQueue.push(key);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,16 @@ import * as ViewModels from "../Contracts/ViewModels";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { userContext } from "../UserContext";
|
||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||
import * as QueryUtils from "../Utils/QueryUtils";
|
||||
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
|
||||
import { userContext } from "../UserContext";
|
||||
import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage";
|
||||
import { createCollection } from "./dataAccess/createCollection";
|
||||
import { handleError } from "./ErrorHandlingUtils";
|
||||
import { createDocument } from "./dataAccess/createDocument";
|
||||
import { deleteDocument } from "./dataAccess/deleteDocument";
|
||||
import { queryDocuments } from "./dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage";
|
||||
import { handleError } from "./ErrorHandlingUtils";
|
||||
|
||||
export class QueriesClient {
|
||||
private static readonly PartitionKey: DataModels.PartitionKey = {
|
||||
@@ -211,7 +211,7 @@ export class QueriesClient {
|
||||
}
|
||||
|
||||
private fetchQueriesQuery(): string {
|
||||
if (this.container.isPreferredApiMongoDB()) {
|
||||
if (userContext.apiType === "Mongo") {
|
||||
return QueriesClient.FetchMongoQuery;
|
||||
}
|
||||
return QueriesClient.FetchQuery;
|
||||
|
||||
@@ -73,7 +73,7 @@ export class Splitter {
|
||||
$(this.leftSide).resizable(splitterOptions);
|
||||
}
|
||||
|
||||
private onResizeStart: JQueryUI.ResizableEvent = (e: Event, ui: JQueryUI.ResizableUIParams) => {
|
||||
private onResizeStart: JQueryUI.ResizableEvent = () => {
|
||||
if (this.direction === SplitterDirection.Vertical) {
|
||||
$(".ui-resizable-helper").height("100%");
|
||||
} else {
|
||||
@@ -82,9 +82,7 @@ export class Splitter {
|
||||
$("iframe").css("pointer-events", "none");
|
||||
};
|
||||
|
||||
private onResizeStop: JQueryUI.ResizableEvent = (e: Event, ui: JQueryUI.ResizableUIParams) => {
|
||||
$("iframe").css("pointer-events", "auto");
|
||||
};
|
||||
private onResizeStop: JQueryUI.ResizableEvent = () => $("iframe").css("pointer-events", "auto");
|
||||
|
||||
public collapseLeft() {
|
||||
this.lastX = $(this.splitter).position().left;
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface TableEntityProps {
|
||||
options: { key: string; text: string }[];
|
||||
isPropertyTypeDisable: boolean;
|
||||
entityTimeValue: string;
|
||||
isEntityValueDisable?: boolean;
|
||||
onDeleteEntity?: () => void;
|
||||
onEditEntity?: () => void;
|
||||
onEntityPropertyChange: (event: React.FormEvent<HTMLElement>, newInput?: string) => void;
|
||||
@@ -55,6 +56,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
||||
isPropertyTypeDisable,
|
||||
isEntityTypeDate,
|
||||
entityTimeValue,
|
||||
isEntityValueDisable,
|
||||
onEditEntity,
|
||||
onDeleteEntity,
|
||||
onEntityPropertyChange,
|
||||
@@ -113,6 +115,7 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
||||
<EntityValue
|
||||
entityValueLabel={entityValueLabel}
|
||||
entityValueType={getEntityValueType()}
|
||||
isEntityValueDisable={isEntityValueDisable}
|
||||
entityValuePlaceholder={entityValuePlaceholder}
|
||||
entityValue={entityValue}
|
||||
isEntityTypeDate={isEntityTypeDate}
|
||||
@@ -121,10 +124,11 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
||||
onSelectDate={onSelectDate}
|
||||
onEntityTimeValueChange={onEntityTimeValueChange}
|
||||
/>
|
||||
<TooltipHost content="Edit property" id="editTooltip">
|
||||
<Image {...imageProps} src={EditIcon} alt="editEntity" id="editEntity" onClick={onEditEntity} />
|
||||
</TooltipHost>
|
||||
|
||||
{!isEntityValueDisable && (
|
||||
<TooltipHost content="Edit property" id="editTooltip">
|
||||
<Image {...imageProps} src={EditIcon} alt="editEntity" id="editEntity" onClick={onEditEntity} />
|
||||
</TooltipHost>
|
||||
)}
|
||||
{isDeleteOptionVisible && userContext.apiType !== "Cassandra" && (
|
||||
<TooltipHost content="Delete property" id="deleteTooltip">
|
||||
<Image {...imageProps} src={DeleteIcon} alt="delete entity" id="deleteEntity" onClick={onDeleteEntity} />
|
||||
|
||||
@@ -28,10 +28,6 @@ describe("Component Registerer", () => {
|
||||
expect(ko.components.isRegistered("graph-styling-pane")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register string-input-pane component", () => {
|
||||
expect(ko.components.isRegistered("string-input-pane")).toBe(true);
|
||||
});
|
||||
|
||||
it("should register dynamic-list component", () => {
|
||||
expect(ko.components.isRegistered("dynamic-list")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -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-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent());
|
||||
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
|
||||
ko.components.register("string-input-pane", new PaneComponents.StringInputPaneComponent());
|
||||
ko.components.register("github-repos-pane", new PaneComponents.GitHubReposPaneComponent());
|
||||
|
||||
@@ -63,7 +63,7 @@ export class ResourceTreeContextMenuButtonFactory {
|
||||
});
|
||||
}
|
||||
|
||||
if (container.isPreferredApiMongoDB()) {
|
||||
if (userContext.apiType === "Mongo") {
|
||||
items.push({
|
||||
iconSrc: AddSqlQueryIcon,
|
||||
onClick: () => selectedCollection && selectedCollection.onNewMongoQueryClick(selectedCollection, null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { loadMonaco, monaco } from "../../LazyMonaco";
|
||||
import template from "./diff-editor-component.html";
|
||||
import * as monaco from "monaco-editor";
|
||||
|
||||
/**
|
||||
* Helper class for ko component registration
|
||||
@@ -92,7 +92,7 @@ export class DiffEditorViewModel {
|
||||
/**
|
||||
* Create the monaco editor on diff mode and attach to DOM
|
||||
*/
|
||||
protected createDiffEditor(
|
||||
protected async createDiffEditor(
|
||||
originalContent: string,
|
||||
modifiedContent: string,
|
||||
createCallback: (e: monaco.editor.IStandaloneDiffEditor) => void
|
||||
@@ -111,7 +111,7 @@ export class DiffEditorViewModel {
|
||||
}
|
||||
|
||||
const language = this.params.editorLanguage || "json";
|
||||
|
||||
const monaco = await loadMonaco();
|
||||
const originalModel = monaco.editor.createModel(originalContent, language);
|
||||
const modifiedModel = monaco.editor.createModel(modifiedContent, language);
|
||||
const diffEditor: monaco.editor.IStandaloneDiffEditor = monaco.editor.createDiffEditor(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { loadMonaco, monaco } from "../../LazyMonaco";
|
||||
import { JsonEditorParams, JsonEditorViewModel } from "../JsonEditor/JsonEditorComponent";
|
||||
import template from "./editor-component.html";
|
||||
import * as monaco from "monaco-editor";
|
||||
import { SqlCompletionItemProvider, ErrorMarkProvider } from "@azure/cosmos-language-service";
|
||||
|
||||
/**
|
||||
* Helper class for ko component registration
|
||||
@@ -49,15 +48,17 @@ class EditorViewModel extends JsonEditorViewModel {
|
||||
return this.params.contentType;
|
||||
}
|
||||
|
||||
protected registerCompletionItemProvider() {
|
||||
let sqlCompletionItemProvider = new SqlCompletionItemProvider();
|
||||
protected async registerCompletionItemProvider() {
|
||||
if (EditorViewModel.providerRegistered.indexOf("sql") < 0) {
|
||||
monaco.languages.registerCompletionItemProvider("sql", sqlCompletionItemProvider);
|
||||
const { SqlCompletionItemProvider } = await import("@azure/cosmos-language-service");
|
||||
const monaco = await loadMonaco();
|
||||
monaco.languages.registerCompletionItemProvider("sql", new SqlCompletionItemProvider());
|
||||
EditorViewModel.providerRegistered.push("sql");
|
||||
}
|
||||
}
|
||||
|
||||
protected getErrorMarkers(input: string): Q.Promise<monaco.editor.IMarkerData[]> {
|
||||
protected async getErrorMarkers(input: string): Promise<monaco.editor.IMarkerData[]> {
|
||||
const { ErrorMarkProvider } = await import("@azure/cosmos-language-service");
|
||||
return ErrorMarkProvider.getErrorMark(input);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import * as monaco from "monaco-editor";
|
||||
import { loadMonaco, monaco } from "../../LazyMonaco";
|
||||
|
||||
export interface EditorReactProps {
|
||||
language: string;
|
||||
@@ -61,7 +61,7 @@ export class EditorReact extends React.Component<EditorReactProps> {
|
||||
/**
|
||||
* Create the monaco editor and attach to DOM
|
||||
*/
|
||||
private createEditor(createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
|
||||
private async createEditor(createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
|
||||
const options: monaco.editor.IEditorConstructionOptions = {
|
||||
value: this.props.content,
|
||||
language: this.props.language,
|
||||
@@ -74,6 +74,7 @@ export class EditorReact extends React.Component<EditorReactProps> {
|
||||
};
|
||||
|
||||
this.rootNode.innerHTML = "";
|
||||
const monaco = await loadMonaco();
|
||||
createCallback(monaco.editor.create(this.rootNode, options));
|
||||
}
|
||||
|
||||
|
||||
@@ -128,21 +128,21 @@ class InputTypeaheadViewModel {
|
||||
},
|
||||
},
|
||||
callback: {
|
||||
onClick: (node: any, a: any, item: OnClickItem, event: any) => {
|
||||
onClick: (_node: unknown, _a: unknown, item: OnClickItem) => {
|
||||
cache.selection = item;
|
||||
|
||||
if (params.selection) {
|
||||
params.selection(item);
|
||||
}
|
||||
},
|
||||
onResult(node: any, query: any, result: any, resultCount: any, resultCountPerGroup: any) {
|
||||
onResult(_node: unknown, query: any) {
|
||||
cache.inputValue = query;
|
||||
if (params.inputValue) {
|
||||
params.inputValue(query);
|
||||
}
|
||||
},
|
||||
},
|
||||
template: (query: string, item: any) => {
|
||||
template: (_query: string, item: any) => {
|
||||
// Don't display id if caption *IS* the id
|
||||
return item.caption === item.value
|
||||
? "<span>{{caption}}</span>"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Q from "q";
|
||||
import * as monaco from "monaco-editor";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { loadMonaco, monaco } from "../../LazyMonaco";
|
||||
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
|
||||
import template from "./json-editor-component.html";
|
||||
|
||||
@@ -88,7 +87,7 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
|
||||
/**
|
||||
* Create the monaco editor and attach to DOM
|
||||
*/
|
||||
protected createEditor(content: string, createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
|
||||
protected async createEditor(content: string, createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
|
||||
this.registerCompletionItemProvider();
|
||||
this.editorContainer = document.getElementById(this.getEditorId());
|
||||
const options: monaco.editor.IEditorConstructionOptions = {
|
||||
@@ -102,6 +101,7 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
|
||||
};
|
||||
|
||||
this.editorContainer.innerHTML = "";
|
||||
const monaco = await loadMonaco();
|
||||
createCallback(monaco.editor.create(this.editorContainer, options));
|
||||
}
|
||||
|
||||
@@ -109,15 +109,16 @@ export class JsonEditorViewModel extends WaitsForTemplateViewModel {
|
||||
protected registerCompletionItemProvider() {}
|
||||
|
||||
// Interface. Will be implemented in children editor view model such as EditorViewModel.
|
||||
protected getErrorMarkers(input: string): Q.Promise<monaco.editor.IMarkerData[]> {
|
||||
return Q.Promise(() => {});
|
||||
protected async getErrorMarkers(_: string): Promise<monaco.editor.IMarkerData[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
protected getEditorLanguage(): string {
|
||||
return "json";
|
||||
}
|
||||
|
||||
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
|
||||
protected async configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
|
||||
const monaco = await loadMonaco();
|
||||
this.editor = editor;
|
||||
const queryEditorModel = this.editor.getModel();
|
||||
if (!this.params.isReadOnly && this.params.updatedContent) {
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { Card } from "@uifabric/react-cards";
|
||||
import {
|
||||
BaseButton,
|
||||
Button,
|
||||
FontWeights,
|
||||
Icon,
|
||||
IconButton,
|
||||
Image,
|
||||
ImageFit,
|
||||
Persona,
|
||||
Text,
|
||||
Link,
|
||||
BaseButton,
|
||||
Button,
|
||||
LinkBase,
|
||||
Persona,
|
||||
Separator,
|
||||
TooltipHost,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
Text,
|
||||
TooltipHost,
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import React, { FunctionComponent, useState } from "react";
|
||||
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
|
||||
import { IGalleryItem } from "../../../../Juno/JunoClient";
|
||||
import * as FileSystemUtil from "../../../Notebook/FileSystemUtil";
|
||||
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
|
||||
|
||||
export interface GalleryCardComponentProps {
|
||||
data: IGalleryItem;
|
||||
@@ -34,166 +34,48 @@ export interface GalleryCardComponentProps {
|
||||
onDeleteClick: (beforeDelete: () => void, afterDelete: () => void) => void;
|
||||
}
|
||||
|
||||
interface GalleryCardComponentState {
|
||||
isDeletingPublishedNotebook: boolean;
|
||||
}
|
||||
export const GalleryCardComponent: FunctionComponent<GalleryCardComponentProps> = ({
|
||||
data,
|
||||
isFavorite,
|
||||
showDownload,
|
||||
showDelete,
|
||||
onClick,
|
||||
onTagClick,
|
||||
onFavoriteClick,
|
||||
onUnfavoriteClick,
|
||||
onDownloadClick,
|
||||
onDeleteClick,
|
||||
}: GalleryCardComponentProps) => {
|
||||
const CARD_WIDTH = 256;
|
||||
const cardImageHeight = 144;
|
||||
const cardDescriptionMaxChars = 80;
|
||||
const cardItemGapBig = 10;
|
||||
const cardItemGapSmall = 8;
|
||||
const cardDeleteSpinnerHeight = 360;
|
||||
const smallTextLineHeight = 18;
|
||||
|
||||
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps, GalleryCardComponentState> {
|
||||
public static readonly CARD_WIDTH = 256;
|
||||
private static readonly cardImageHeight = 144;
|
||||
public static readonly cardHeightToWidthRatio =
|
||||
GalleryCardComponent.cardImageHeight / GalleryCardComponent.CARD_WIDTH;
|
||||
private static readonly cardDescriptionMaxChars = 80;
|
||||
private static readonly cardItemGapBig = 10;
|
||||
private static readonly cardItemGapSmall = 8;
|
||||
private static readonly cardDeleteSpinnerHeight = 360;
|
||||
private static readonly smallTextLineHeight = 18;
|
||||
const [isDeletingPublishedNotebook, setIsDeletingPublishedNotebook] = useState<boolean>(false);
|
||||
|
||||
constructor(props: GalleryCardComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isDeletingPublishedNotebook: false,
|
||||
};
|
||||
}
|
||||
const cardButtonsVisible = isFavorite !== undefined || showDownload || showDelete;
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
};
|
||||
const dateString = new Date(data.created).toLocaleString("default", options);
|
||||
const cardTitle = FileSystemUtil.stripExtension(data.name, "ipynb");
|
||||
|
||||
public render(): JSX.Element {
|
||||
const cardButtonsVisible = this.props.isFavorite !== undefined || this.props.showDownload || this.props.showDelete;
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
};
|
||||
const dateString = new Date(this.props.data.created).toLocaleString("default", options);
|
||||
const cardTitle = FileSystemUtil.stripExtension(this.props.data.name, "ipynb");
|
||||
|
||||
return (
|
||||
<Card
|
||||
style={{ background: "white" }}
|
||||
aria-label={cardTitle}
|
||||
data-is-focusable="true"
|
||||
tokens={{ width: GalleryCardComponent.CARD_WIDTH, childrenGap: 0 }}
|
||||
onClick={(event) => this.onClick(event, this.props.onClick)}
|
||||
>
|
||||
{this.state.isDeletingPublishedNotebook && (
|
||||
<Card.Item tokens={{ padding: GalleryCardComponent.cardItemGapBig }}>
|
||||
<Spinner
|
||||
size={SpinnerSize.large}
|
||||
label={`Deleting '${cardTitle}'`}
|
||||
styles={{ root: { height: GalleryCardComponent.cardDeleteSpinnerHeight } }}
|
||||
/>
|
||||
</Card.Item>
|
||||
)}
|
||||
{!this.state.isDeletingPublishedNotebook && (
|
||||
<>
|
||||
<Card.Item tokens={{ padding: GalleryCardComponent.cardItemGapBig }}>
|
||||
<Persona
|
||||
imageUrl={this.props.data.isSample && CosmosDBLogo}
|
||||
text={this.props.data.author}
|
||||
secondaryText={dateString}
|
||||
/>
|
||||
</Card.Item>
|
||||
|
||||
<Card.Item>
|
||||
<Image
|
||||
src={this.props.data.thumbnailUrl}
|
||||
width={GalleryCardComponent.CARD_WIDTH}
|
||||
height={GalleryCardComponent.cardImageHeight}
|
||||
imageFit={ImageFit.cover}
|
||||
alt={`${cardTitle} cover image`}
|
||||
/>
|
||||
</Card.Item>
|
||||
|
||||
<Card.Section styles={{ root: { padding: GalleryCardComponent.cardItemGapBig } }}>
|
||||
<Text variant="small" nowrap styles={{ root: { height: GalleryCardComponent.smallTextLineHeight } }}>
|
||||
{this.props.data.tags ? (
|
||||
this.props.data.tags.map((tag, index, array) => (
|
||||
<span key={tag}>
|
||||
<Link onClick={(event) => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link>
|
||||
{index === array.length - 1 ? <></> : ", "}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<br />
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
styles={{
|
||||
root: {
|
||||
fontWeight: FontWeights.semibold,
|
||||
paddingTop: GalleryCardComponent.cardItemGapSmall,
|
||||
paddingBottom: GalleryCardComponent.cardItemGapSmall,
|
||||
},
|
||||
}}
|
||||
nowrap
|
||||
>
|
||||
{cardTitle}
|
||||
</Text>
|
||||
|
||||
<Text variant="small" styles={{ root: { height: GalleryCardComponent.smallTextLineHeight * 2 } }}>
|
||||
{this.renderTruncatedDescription()}
|
||||
</Text>
|
||||
|
||||
<span>
|
||||
{this.props.data.views !== undefined &&
|
||||
this.generateIconText("RedEye", this.props.data.views.toString())}
|
||||
{this.props.data.downloads !== undefined &&
|
||||
this.generateIconText("Download", this.props.data.downloads.toString())}
|
||||
{this.props.data.favorites !== undefined &&
|
||||
this.generateIconText("Heart", this.props.data.favorites.toString())}
|
||||
</span>
|
||||
</Card.Section>
|
||||
|
||||
{cardButtonsVisible && (
|
||||
<Card.Section
|
||||
styles={{
|
||||
root: {
|
||||
marginLeft: GalleryCardComponent.cardItemGapBig,
|
||||
marginRight: GalleryCardComponent.cardItemGapBig,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Separator styles={{ root: { padding: 0, height: 1 } }} />
|
||||
|
||||
<span>
|
||||
{this.props.isFavorite !== undefined &&
|
||||
this.generateIconButtonWithTooltip(
|
||||
this.props.isFavorite ? "HeartFill" : "Heart",
|
||||
this.props.isFavorite ? "Unfavorite" : "Favorite",
|
||||
"left",
|
||||
this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick
|
||||
)}
|
||||
|
||||
{this.props.showDownload &&
|
||||
this.generateIconButtonWithTooltip("Download", "Download", "left", this.props.onDownloadClick)}
|
||||
|
||||
{this.props.showDelete &&
|
||||
this.generateIconButtonWithTooltip("Delete", "Remove", "right", () =>
|
||||
this.props.onDeleteClick(
|
||||
() => this.setState({ isDeletingPublishedNotebook: true }),
|
||||
() => this.setState({ isDeletingPublishedNotebook: false })
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</Card.Section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
private renderTruncatedDescription = (): string => {
|
||||
let truncatedDescription = this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars);
|
||||
if (this.props.data.description.length > GalleryCardComponent.cardDescriptionMaxChars) {
|
||||
const renderTruncatedDescription = (): string => {
|
||||
let truncatedDescription = data.description.substr(0, cardDescriptionMaxChars);
|
||||
if (data.description.length > cardDescriptionMaxChars) {
|
||||
truncatedDescription = `${truncatedDescription} ...`;
|
||||
}
|
||||
return truncatedDescription;
|
||||
};
|
||||
|
||||
private generateIconText = (iconName: string, text: string): JSX.Element => {
|
||||
const generateIconText = (iconName: string, text: string): JSX.Element => {
|
||||
return (
|
||||
<Text variant="tiny" styles={{ root: { color: "#605E5C", paddingRight: GalleryCardComponent.cardItemGapSmall } }}>
|
||||
<Text variant="tiny" styles={{ root: { color: "#605E5C", paddingRight: cardItemGapSmall } }}>
|
||||
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} /> {text}
|
||||
</Text>
|
||||
);
|
||||
@@ -203,7 +85,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
||||
* Fluent UI doesn't support tooltips on IconButtons out of the box. In the meantime the recommendation is
|
||||
* to do the following (from https://developer.microsoft.com/en-us/fluentui#/controls/web/button)
|
||||
*/
|
||||
private generateIconButtonWithTooltip = (
|
||||
const generateIconButtonWithTooltip = (
|
||||
iconName: string,
|
||||
title: string,
|
||||
horizontalAlign: "right" | "left",
|
||||
@@ -220,13 +102,13 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
||||
iconProps={{ iconName }}
|
||||
title={title}
|
||||
ariaLabel={title}
|
||||
onClick={(event) => this.onClick(event, activate)}
|
||||
onClick={(event) => handlerOnClick(event, activate)}
|
||||
/>
|
||||
</TooltipHost>
|
||||
);
|
||||
};
|
||||
|
||||
private onClick = (
|
||||
const handlerOnClick = (
|
||||
event:
|
||||
| React.MouseEvent<HTMLElement | HTMLAnchorElement | HTMLButtonElement | LinkBase, MouseEvent>
|
||||
| React.MouseEvent<
|
||||
@@ -239,4 +121,112 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
||||
event.preventDefault();
|
||||
activate();
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
style={{ background: "white" }}
|
||||
aria-label={cardTitle}
|
||||
data-is-focusable="true"
|
||||
tokens={{ width: CARD_WIDTH, childrenGap: 0 }}
|
||||
onClick={(event) => handlerOnClick(event, onClick)}
|
||||
>
|
||||
{isDeletingPublishedNotebook && (
|
||||
<Card.Item tokens={{ padding: cardItemGapBig }}>
|
||||
<Spinner
|
||||
size={SpinnerSize.large}
|
||||
label={`Deleting '${cardTitle}'`}
|
||||
styles={{ root: { height: cardDeleteSpinnerHeight } }}
|
||||
/>
|
||||
</Card.Item>
|
||||
)}
|
||||
{!isDeletingPublishedNotebook && (
|
||||
<>
|
||||
<Card.Item tokens={{ padding: cardItemGapBig }}>
|
||||
<Persona imageUrl={data.isSample && CosmosDBLogo} text={data.author} secondaryText={dateString} />
|
||||
</Card.Item>
|
||||
|
||||
<Card.Item>
|
||||
<Image
|
||||
src={data.thumbnailUrl}
|
||||
width={CARD_WIDTH}
|
||||
height={cardImageHeight}
|
||||
imageFit={ImageFit.cover}
|
||||
alt={`${cardTitle} cover image`}
|
||||
/>
|
||||
</Card.Item>
|
||||
|
||||
<Card.Section styles={{ root: { padding: cardItemGapBig } }}>
|
||||
<Text variant="small" nowrap styles={{ root: { height: smallTextLineHeight } }}>
|
||||
{data.tags ? (
|
||||
data.tags.map((tag, index, array) => (
|
||||
<span key={tag}>
|
||||
<Link onClick={(event) => handlerOnClick(event, () => onTagClick(tag))}>{tag}</Link>
|
||||
{index === array.length - 1 ? <></> : ", "}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<br />
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
styles={{
|
||||
root: {
|
||||
fontWeight: FontWeights.semibold,
|
||||
paddingTop: cardItemGapSmall,
|
||||
paddingBottom: cardItemGapSmall,
|
||||
},
|
||||
}}
|
||||
nowrap
|
||||
>
|
||||
{cardTitle}
|
||||
</Text>
|
||||
|
||||
<Text variant="small" styles={{ root: { height: smallTextLineHeight * 2 } }}>
|
||||
{renderTruncatedDescription()}
|
||||
</Text>
|
||||
|
||||
<span>
|
||||
{data.views !== undefined && generateIconText("RedEye", data.views.toString())}
|
||||
{data.downloads !== undefined && generateIconText("Download", data.downloads.toString())}
|
||||
{data.favorites !== undefined && generateIconText("Heart", data.favorites.toString())}
|
||||
</span>
|
||||
</Card.Section>
|
||||
|
||||
{cardButtonsVisible && (
|
||||
<Card.Section
|
||||
styles={{
|
||||
root: {
|
||||
marginLeft: cardItemGapBig,
|
||||
marginRight: cardItemGapBig,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Separator styles={{ root: { padding: 0, height: 1 } }} />
|
||||
|
||||
<span>
|
||||
{isFavorite !== undefined &&
|
||||
generateIconButtonWithTooltip(
|
||||
isFavorite ? "HeartFill" : "Heart",
|
||||
isFavorite ? "Unfavorite" : "Favorite",
|
||||
"left",
|
||||
isFavorite ? onUnfavoriteClick : onFavoriteClick
|
||||
)}
|
||||
|
||||
{showDownload && generateIconButtonWithTooltip("Download", "Download", "left", onDownloadClick)}
|
||||
|
||||
{showDelete &&
|
||||
generateIconButtonWithTooltip("Delete", "Remove", "right", () =>
|
||||
onDeleteClick(
|
||||
() => setIsDeletingPublishedNotebook(true),
|
||||
() => setIsDeletingPublishedNotebook(false)
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</Card.Section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
jest.mock("../../../Juno/JunoClient");
|
||||
jest.mock("../../../../Juno/JunoClient");
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { CodeOfConductComponent, CodeOfConductComponentProps } from "./CodeOfConductComponent";
|
||||
import { JunoClient } from "../../../Juno/JunoClient";
|
||||
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||
import { CodeOfConductComponent, CodeOfConductComponentProps } from ".";
|
||||
import { HttpStatusCodes } from "../../../../Common/Constants";
|
||||
import { JunoClient } from "../../../../Juno/JunoClient";
|
||||
|
||||
describe("CodeOfConductComponent", () => {
|
||||
let codeOfConductProps: CodeOfConductComponentProps;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import { CodeOfConductComponent } from "./CodeOfConductComponent";
|
||||
import "./GalleryViewerComponent.less";
|
||||
import { InfoComponent } from "./InfoComponent/InfoComponent";
|
||||
|
||||
const CARD_WIDTH = 256;
|
||||
export interface GalleryViewerComponentProps {
|
||||
container?: Explorer;
|
||||
junoClient: JunoClient;
|
||||
@@ -643,7 +644,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
|
||||
private getPageSpecification = (itemIndex?: number, visibleRect?: IRectangle): IPageSpecification => {
|
||||
if (itemIndex === 0) {
|
||||
this.columnCount = Math.floor(visibleRect.width / GalleryCardComponent.CARD_WIDTH) || this.columnCount;
|
||||
this.columnCount = Math.floor(visibleRect.width / CARD_WIDTH) || this.columnCount;
|
||||
this.rowCount = GalleryViewerComponent.rowsPerPage;
|
||||
}
|
||||
|
||||
|
||||
@@ -136,15 +136,13 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
this.container = this.collection?.container;
|
||||
this.offer = this.collection?.offer();
|
||||
this.isAnalyticalStorageEnabled = !!this.collection?.analyticalStorageTtl();
|
||||
this.shouldShowIndexingPolicyEditor =
|
||||
this.container && userContext.apiType !== "Cassandra" && !this.container.isPreferredApiMongoDB();
|
||||
this.shouldShowIndexingPolicyEditor = userContext.apiType !== "Cassandra" && userContext.apiType !== "Mongo";
|
||||
|
||||
this.changeFeedPolicyVisible = userContext.features.enableChangeFeedPolicy;
|
||||
|
||||
// Mongo container with system partition key still treat as "Fixed"
|
||||
this.isFixedContainer =
|
||||
this.container.isPreferredApiMongoDB() &&
|
||||
(!this.collection?.partitionKey || this.collection?.partitionKey.systemKey);
|
||||
userContext.apiType === "Mongo" && (!this.collection?.partitionKey || this.collection?.partitionKey.systemKey);
|
||||
} else {
|
||||
this.database = this.props.settingsTab.database;
|
||||
this.container = this.database?.container;
|
||||
@@ -236,7 +234,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
public loadMongoIndexes = async (): Promise<void> => {
|
||||
if (
|
||||
this.container.isPreferredApiMongoDB() &&
|
||||
userContext.apiType === "Mongo" &&
|
||||
this.container.isEnableMongoCapabilityPresent() &&
|
||||
this.container.databaseAccount()
|
||||
) {
|
||||
@@ -1002,7 +1000,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />,
|
||||
});
|
||||
} else if (this.container.isPreferredApiMongoDB()) {
|
||||
} else if (userContext.apiType === "Mongo") {
|
||||
const mongoIndexTabContext = this.getMongoIndexTabContent(mongoIndexingPolicyComponentProps);
|
||||
if (mongoIndexTabContext) {
|
||||
tabs.push({
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { MessageBar, MessageBarType, Stack } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import * as monaco from "monaco-editor";
|
||||
import { isDirty, isIndexTransforming } from "../SettingsUtils";
|
||||
import { MessageBar, MessageBarType, Stack } from "office-ui-fabric-react";
|
||||
import { loadMonaco, monaco } from "../../../LazyMonaco";
|
||||
import { indexingPolicynUnsavedWarningMessage, titleAndInputStackProps } from "../SettingsRenderUtils";
|
||||
import { isDirty, isIndexTransforming } from "../SettingsUtils";
|
||||
import { IndexingPolicyRefreshComponent } from "./IndexingPolicyRefresh/IndexingPolicyRefreshComponent";
|
||||
|
||||
export interface IndexingPolicyComponentProps {
|
||||
@@ -84,9 +84,9 @@ export class IndexingPolicyComponent extends React.Component<
|
||||
return false;
|
||||
};
|
||||
|
||||
private createIndexingPolicyEditor = (): void => {
|
||||
private async createIndexingPolicyEditor(): Promise<void> {
|
||||
const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4);
|
||||
|
||||
const monaco = await loadMonaco();
|
||||
this.indexingPolicyEditor = monaco.editor.create(this.indexingPolicyDiv.current, {
|
||||
value: value,
|
||||
language: "json",
|
||||
@@ -98,7 +98,7 @@ export class IndexingPolicyComponent extends React.Component<
|
||||
indexingPolicyEditorModel.onDidChangeContent(this.onEditorContentChange.bind(this));
|
||||
this.props.logIndexingPolicySuccessMessage();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private onEditorContentChange = (): void => {
|
||||
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
||||
|
||||
@@ -323,7 +323,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
userContext.apiType === "Cassandra" ||
|
||||
userContext.apiType === "Tables" ||
|
||||
!this.props.collection.partitionKeyProperty ||
|
||||
(this.props.container.isPreferredApiMongoDB() && this.props.collection.partitionKey.systemKey)
|
||||
(userContext.apiType === "Mongo" && this.props.collection.partitionKey.systemKey)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -177,40 +177,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
EditTableEntityPane {
|
||||
"addButtonLabel": "Add Property",
|
||||
"attributeNameLabel": "Property Name",
|
||||
"attributeValueLabel": "Value",
|
||||
"canAdd": [Function],
|
||||
"canApply": [Function],
|
||||
"container": [Circular],
|
||||
"dataTypeLabel": "Type",
|
||||
"displayedAttributes": [Function],
|
||||
"editAttribute": [Function],
|
||||
"editButtonLabel": "Edit",
|
||||
"editingProperty": [Function],
|
||||
"edmTypes": [Function],
|
||||
"finishEditingAttribute": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "edittableentitypane",
|
||||
"insertAttribute": [Function],
|
||||
"isEditing": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"onAddPropertyKeyDown": [Function],
|
||||
"onBackButtonKeyDown": [Function],
|
||||
"onDeletePropertyKeyDown": [Function],
|
||||
"onEditPropertyKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"removeAttribute": [Function],
|
||||
"removeButtonLabel": "Remove",
|
||||
"scrollId": [Function],
|
||||
"submitButtonText": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
CassandraAddCollectionPane {
|
||||
"autoPilotUsageCost": [Function],
|
||||
"canConfigureThroughput": [Function],
|
||||
@@ -233,9 +199,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"keyspaceHasSharedOffer": [Function],
|
||||
"keyspaceId": [Function],
|
||||
"keyspaceIds": [Function],
|
||||
"keyspaceOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"keyspaceOffers": Map {},
|
||||
"keyspaceThroughput": [Function],
|
||||
"maxThroughputRU": [Function],
|
||||
"minThroughputRU": [Function],
|
||||
@@ -258,20 +222,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"userTableQuery": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
StringInputPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "stringinputpane",
|
||||
"inputLabel": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"stringInput": [Function],
|
||||
"submitButtonLabel": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
],
|
||||
"_refreshSparkEnabledStateForAccount": [Function],
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
@@ -426,9 +376,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"keyspaceHasSharedOffer": [Function],
|
||||
"keyspaceId": [Function],
|
||||
"keyspaceIds": [Function],
|
||||
"keyspaceOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"keyspaceOffers": Map {},
|
||||
"keyspaceThroughput": [Function],
|
||||
"maxThroughputRU": [Function],
|
||||
"minThroughputRU": [Function],
|
||||
@@ -479,40 +427,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"defaultExperience": [Function],
|
||||
"deleteCollectionText": [Function],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
"addButtonLabel": "Add Property",
|
||||
"attributeNameLabel": "Property Name",
|
||||
"attributeValueLabel": "Value",
|
||||
"canAdd": [Function],
|
||||
"canApply": [Function],
|
||||
"container": [Circular],
|
||||
"dataTypeLabel": "Type",
|
||||
"displayedAttributes": [Function],
|
||||
"editAttribute": [Function],
|
||||
"editButtonLabel": "Edit",
|
||||
"editingProperty": [Function],
|
||||
"edmTypes": [Function],
|
||||
"finishEditingAttribute": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "edittableentitypane",
|
||||
"insertAttribute": [Function],
|
||||
"isEditing": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"onAddPropertyKeyDown": [Function],
|
||||
"onBackButtonKeyDown": [Function],
|
||||
"onDeletePropertyKeyDown": [Function],
|
||||
"onEditPropertyKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"removeAttribute": [Function],
|
||||
"removeButtonLabel": "Remove",
|
||||
"scrollId": [Function],
|
||||
"submitButtonText": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"graphStylingPane": GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -544,7 +458,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isPreferredApiMongoDB": [Function],
|
||||
"isPublishNotebookPaneEnabled": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isRightPanelV2Enabled": [Function],
|
||||
@@ -576,21 +489,9 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"databaseCollectionIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"koSubsCollectionIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"koSubsDatabaseIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"databaseCollectionIdMap": Map {},
|
||||
"koSubsCollectionIdMap": Map {},
|
||||
"koSubsDatabaseIdMap": Map {},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
@@ -616,20 +517,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onResizeStop": [Function],
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"stringInputPane": StringInputPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "stringinputpane",
|
||||
"inputLabel": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"stringInput": [Function],
|
||||
"submitButtonLabel": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
@@ -805,40 +692,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
EditTableEntityPane {
|
||||
"addButtonLabel": "Add Property",
|
||||
"attributeNameLabel": "Property Name",
|
||||
"attributeValueLabel": "Value",
|
||||
"canAdd": [Function],
|
||||
"canApply": [Function],
|
||||
"container": [Circular],
|
||||
"dataTypeLabel": "Type",
|
||||
"displayedAttributes": [Function],
|
||||
"editAttribute": [Function],
|
||||
"editButtonLabel": "Edit",
|
||||
"editingProperty": [Function],
|
||||
"edmTypes": [Function],
|
||||
"finishEditingAttribute": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "edittableentitypane",
|
||||
"insertAttribute": [Function],
|
||||
"isEditing": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"onAddPropertyKeyDown": [Function],
|
||||
"onBackButtonKeyDown": [Function],
|
||||
"onDeletePropertyKeyDown": [Function],
|
||||
"onEditPropertyKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"removeAttribute": [Function],
|
||||
"removeButtonLabel": "Remove",
|
||||
"scrollId": [Function],
|
||||
"submitButtonText": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
CassandraAddCollectionPane {
|
||||
"autoPilotUsageCost": [Function],
|
||||
"canConfigureThroughput": [Function],
|
||||
@@ -861,9 +714,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"keyspaceHasSharedOffer": [Function],
|
||||
"keyspaceId": [Function],
|
||||
"keyspaceIds": [Function],
|
||||
"keyspaceOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"keyspaceOffers": Map {},
|
||||
"keyspaceThroughput": [Function],
|
||||
"maxThroughputRU": [Function],
|
||||
"minThroughputRU": [Function],
|
||||
@@ -886,20 +737,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"userTableQuery": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
StringInputPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "stringinputpane",
|
||||
"inputLabel": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"stringInput": [Function],
|
||||
"submitButtonLabel": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
],
|
||||
"_refreshSparkEnabledStateForAccount": [Function],
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
@@ -1054,9 +891,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"keyspaceHasSharedOffer": [Function],
|
||||
"keyspaceId": [Function],
|
||||
"keyspaceIds": [Function],
|
||||
"keyspaceOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"keyspaceOffers": Map {},
|
||||
"keyspaceThroughput": [Function],
|
||||
"maxThroughputRU": [Function],
|
||||
"minThroughputRU": [Function],
|
||||
@@ -1107,40 +942,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"defaultExperience": [Function],
|
||||
"deleteCollectionText": [Function],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
"addButtonLabel": "Add Property",
|
||||
"attributeNameLabel": "Property Name",
|
||||
"attributeValueLabel": "Value",
|
||||
"canAdd": [Function],
|
||||
"canApply": [Function],
|
||||
"container": [Circular],
|
||||
"dataTypeLabel": "Type",
|
||||
"displayedAttributes": [Function],
|
||||
"editAttribute": [Function],
|
||||
"editButtonLabel": "Edit",
|
||||
"editingProperty": [Function],
|
||||
"edmTypes": [Function],
|
||||
"finishEditingAttribute": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "edittableentitypane",
|
||||
"insertAttribute": [Function],
|
||||
"isEditing": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"onAddPropertyKeyDown": [Function],
|
||||
"onBackButtonKeyDown": [Function],
|
||||
"onDeletePropertyKeyDown": [Function],
|
||||
"onEditPropertyKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"removeAttribute": [Function],
|
||||
"removeButtonLabel": "Remove",
|
||||
"scrollId": [Function],
|
||||
"submitButtonText": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"graphStylingPane": GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -1172,7 +973,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isPreferredApiMongoDB": [Function],
|
||||
"isPublishNotebookPaneEnabled": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isRightPanelV2Enabled": [Function],
|
||||
@@ -1204,21 +1004,9 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"databaseCollectionIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"koSubsCollectionIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"koSubsDatabaseIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"databaseCollectionIdMap": Map {},
|
||||
"koSubsCollectionIdMap": Map {},
|
||||
"koSubsDatabaseIdMap": Map {},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
@@ -1244,20 +1032,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onResizeStop": [Function],
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"stringInputPane": StringInputPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "stringinputpane",
|
||||
"inputLabel": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"stringInput": [Function],
|
||||
"submitButtonLabel": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
@@ -1446,40 +1220,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
EditTableEntityPane {
|
||||
"addButtonLabel": "Add Property",
|
||||
"attributeNameLabel": "Property Name",
|
||||
"attributeValueLabel": "Value",
|
||||
"canAdd": [Function],
|
||||
"canApply": [Function],
|
||||
"container": [Circular],
|
||||
"dataTypeLabel": "Type",
|
||||
"displayedAttributes": [Function],
|
||||
"editAttribute": [Function],
|
||||
"editButtonLabel": "Edit",
|
||||
"editingProperty": [Function],
|
||||
"edmTypes": [Function],
|
||||
"finishEditingAttribute": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "edittableentitypane",
|
||||
"insertAttribute": [Function],
|
||||
"isEditing": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"onAddPropertyKeyDown": [Function],
|
||||
"onBackButtonKeyDown": [Function],
|
||||
"onDeletePropertyKeyDown": [Function],
|
||||
"onEditPropertyKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"removeAttribute": [Function],
|
||||
"removeButtonLabel": "Remove",
|
||||
"scrollId": [Function],
|
||||
"submitButtonText": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
CassandraAddCollectionPane {
|
||||
"autoPilotUsageCost": [Function],
|
||||
"canConfigureThroughput": [Function],
|
||||
@@ -1502,9 +1242,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"keyspaceHasSharedOffer": [Function],
|
||||
"keyspaceId": [Function],
|
||||
"keyspaceIds": [Function],
|
||||
"keyspaceOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"keyspaceOffers": Map {},
|
||||
"keyspaceThroughput": [Function],
|
||||
"maxThroughputRU": [Function],
|
||||
"minThroughputRU": [Function],
|
||||
@@ -1527,20 +1265,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"userTableQuery": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
StringInputPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "stringinputpane",
|
||||
"inputLabel": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"stringInput": [Function],
|
||||
"submitButtonLabel": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
],
|
||||
"_refreshSparkEnabledStateForAccount": [Function],
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
@@ -1695,9 +1419,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"keyspaceHasSharedOffer": [Function],
|
||||
"keyspaceId": [Function],
|
||||
"keyspaceIds": [Function],
|
||||
"keyspaceOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"keyspaceOffers": Map {},
|
||||
"keyspaceThroughput": [Function],
|
||||
"maxThroughputRU": [Function],
|
||||
"minThroughputRU": [Function],
|
||||
@@ -1748,40 +1470,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"defaultExperience": [Function],
|
||||
"deleteCollectionText": [Function],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
"addButtonLabel": "Add Property",
|
||||
"attributeNameLabel": "Property Name",
|
||||
"attributeValueLabel": "Value",
|
||||
"canAdd": [Function],
|
||||
"canApply": [Function],
|
||||
"container": [Circular],
|
||||
"dataTypeLabel": "Type",
|
||||
"displayedAttributes": [Function],
|
||||
"editAttribute": [Function],
|
||||
"editButtonLabel": "Edit",
|
||||
"editingProperty": [Function],
|
||||
"edmTypes": [Function],
|
||||
"finishEditingAttribute": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "edittableentitypane",
|
||||
"insertAttribute": [Function],
|
||||
"isEditing": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"onAddPropertyKeyDown": [Function],
|
||||
"onBackButtonKeyDown": [Function],
|
||||
"onDeletePropertyKeyDown": [Function],
|
||||
"onEditPropertyKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"removeAttribute": [Function],
|
||||
"removeButtonLabel": "Remove",
|
||||
"scrollId": [Function],
|
||||
"submitButtonText": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"graphStylingPane": GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -1813,7 +1501,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isPreferredApiMongoDB": [Function],
|
||||
"isPublishNotebookPaneEnabled": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isRightPanelV2Enabled": [Function],
|
||||
@@ -1845,21 +1532,9 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"databaseCollectionIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"koSubsCollectionIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"koSubsDatabaseIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"databaseCollectionIdMap": Map {},
|
||||
"koSubsCollectionIdMap": Map {},
|
||||
"koSubsDatabaseIdMap": Map {},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
@@ -1885,20 +1560,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onResizeStop": [Function],
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"stringInputPane": StringInputPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "stringinputpane",
|
||||
"inputLabel": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"stringInput": [Function],
|
||||
"submitButtonLabel": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
@@ -2074,40 +1735,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
EditTableEntityPane {
|
||||
"addButtonLabel": "Add Property",
|
||||
"attributeNameLabel": "Property Name",
|
||||
"attributeValueLabel": "Value",
|
||||
"canAdd": [Function],
|
||||
"canApply": [Function],
|
||||
"container": [Circular],
|
||||
"dataTypeLabel": "Type",
|
||||
"displayedAttributes": [Function],
|
||||
"editAttribute": [Function],
|
||||
"editButtonLabel": "Edit",
|
||||
"editingProperty": [Function],
|
||||
"edmTypes": [Function],
|
||||
"finishEditingAttribute": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "edittableentitypane",
|
||||
"insertAttribute": [Function],
|
||||
"isEditing": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"onAddPropertyKeyDown": [Function],
|
||||
"onBackButtonKeyDown": [Function],
|
||||
"onDeletePropertyKeyDown": [Function],
|
||||
"onEditPropertyKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"removeAttribute": [Function],
|
||||
"removeButtonLabel": "Remove",
|
||||
"scrollId": [Function],
|
||||
"submitButtonText": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
CassandraAddCollectionPane {
|
||||
"autoPilotUsageCost": [Function],
|
||||
"canConfigureThroughput": [Function],
|
||||
@@ -2130,9 +1757,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"keyspaceHasSharedOffer": [Function],
|
||||
"keyspaceId": [Function],
|
||||
"keyspaceIds": [Function],
|
||||
"keyspaceOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"keyspaceOffers": Map {},
|
||||
"keyspaceThroughput": [Function],
|
||||
"maxThroughputRU": [Function],
|
||||
"minThroughputRU": [Function],
|
||||
@@ -2155,20 +1780,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"userTableQuery": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
StringInputPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "stringinputpane",
|
||||
"inputLabel": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"stringInput": [Function],
|
||||
"submitButtonLabel": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
],
|
||||
"_refreshSparkEnabledStateForAccount": [Function],
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
@@ -2323,9 +1934,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"keyspaceHasSharedOffer": [Function],
|
||||
"keyspaceId": [Function],
|
||||
"keyspaceIds": [Function],
|
||||
"keyspaceOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"keyspaceOffers": Map {},
|
||||
"keyspaceThroughput": [Function],
|
||||
"maxThroughputRU": [Function],
|
||||
"minThroughputRU": [Function],
|
||||
@@ -2376,40 +1985,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"defaultExperience": [Function],
|
||||
"deleteCollectionText": [Function],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
"addButtonLabel": "Add Property",
|
||||
"attributeNameLabel": "Property Name",
|
||||
"attributeValueLabel": "Value",
|
||||
"canAdd": [Function],
|
||||
"canApply": [Function],
|
||||
"container": [Circular],
|
||||
"dataTypeLabel": "Type",
|
||||
"displayedAttributes": [Function],
|
||||
"editAttribute": [Function],
|
||||
"editButtonLabel": "Edit",
|
||||
"editingProperty": [Function],
|
||||
"edmTypes": [Function],
|
||||
"finishEditingAttribute": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "edittableentitypane",
|
||||
"insertAttribute": [Function],
|
||||
"isEditing": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"onAddPropertyKeyDown": [Function],
|
||||
"onBackButtonKeyDown": [Function],
|
||||
"onDeletePropertyKeyDown": [Function],
|
||||
"onEditPropertyKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"removeAttribute": [Function],
|
||||
"removeButtonLabel": "Remove",
|
||||
"scrollId": [Function],
|
||||
"submitButtonText": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"graphStylingPane": GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -2441,7 +2016,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isPreferredApiMongoDB": [Function],
|
||||
"isPublishNotebookPaneEnabled": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isRightPanelV2Enabled": [Function],
|
||||
@@ -2473,21 +2047,9 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"databaseCollectionIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"koSubsCollectionIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"koSubsDatabaseIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"databaseCollectionIdMap": Map {},
|
||||
"koSubsCollectionIdMap": Map {},
|
||||
"koSubsDatabaseIdMap": Map {},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
@@ -2513,20 +2075,6 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onResizeStop": [Function],
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"stringInputPane": StringInputPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "stringinputpane",
|
||||
"inputLabel": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"stringInput": [Function],
|
||||
"submitButtonLabel": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
|
||||
@@ -14,7 +14,6 @@ describe("ContainerSampleGenerator", () => {
|
||||
const createExplorerStub = (database: ViewModels.Database): Explorer => {
|
||||
const explorerStub = {} as Explorer;
|
||||
explorerStub.databases = ko.observableArray<ViewModels.Database>([database]);
|
||||
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
explorerStub.canExceedMaximumValue = ko.computed<boolean>(() => false);
|
||||
explorerStub.findDatabaseWithId = () => database;
|
||||
explorerStub.refreshAllDatabases = () => Q.resolve();
|
||||
|
||||
@@ -6,7 +6,6 @@ import React from "react";
|
||||
import _ from "underscore";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import { ExplorerMetrics } from "../Common/Constants";
|
||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||
@@ -65,16 +64,15 @@ import { LoadQueryPane } from "./Panes/LoadQueryPane/LoadQueryPane";
|
||||
import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane";
|
||||
import { SettingsPane } from "./Panes/SettingsPane/SettingsPane";
|
||||
import { SetupNoteBooksPanel } from "./Panes/SetupNotebooksPanel/SetupNotebooksPanel";
|
||||
import { StringInputPane } from "./Panes/StringInputPane";
|
||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||
import { AddTableEntityPanel } from "./Panes/Tables/AddTableEntityPanel";
|
||||
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
||||
import { EditTableEntityPanel } from "./Panes/Tables/EditTableEntityPanel";
|
||||
import { TableQuerySelectPanel } from "./Panes/Tables/TableQuerySelectPanel";
|
||||
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
|
||||
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
||||
import TableListViewModal from "./Tables/DataTable/TableEntityListViewModel";
|
||||
import QueryViewModel from "./Tables/QueryBuilder/QueryViewModel";
|
||||
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
||||
import type { GalleryTabOptions } from "./Tabs/GalleryTab";
|
||||
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
|
||||
import QueryTablesTab from "./Tabs/QueryTablesTab";
|
||||
import { TabsManager } from "./Tabs/TabsManager";
|
||||
@@ -121,11 +119,6 @@ export default class Explorer {
|
||||
* Use userContext.apiType instead
|
||||
* */
|
||||
public defaultExperience: ko.Observable<string>;
|
||||
/**
|
||||
* @deprecated
|
||||
* Compare a string with userContext.apiType instead: userContext.apiType === "Mongo"
|
||||
* */
|
||||
public isPreferredApiMongoDB: ko.Computed<boolean>;
|
||||
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
|
||||
/**
|
||||
* @deprecated
|
||||
@@ -174,11 +167,8 @@ export default class Explorer {
|
||||
public addDatabasePane: AddDatabasePane;
|
||||
public addCollectionPane: AddCollectionPane;
|
||||
public graphStylingPane: GraphStylingPane;
|
||||
public editTableEntityPane: EditTableEntityPane;
|
||||
public cassandraAddCollectionPane: CassandraAddCollectionPane;
|
||||
public stringInputPane: StringInputPane;
|
||||
public gitHubReposPane: ContextualPaneBase;
|
||||
public publishNotebookPaneAdapter: ReactAdapter;
|
||||
|
||||
// features
|
||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||
@@ -415,27 +405,6 @@ export default class Explorer {
|
||||
) !== undefined
|
||||
);
|
||||
|
||||
this.isPreferredApiMongoDB = ko.computed(() => {
|
||||
const defaultExperience = (this.defaultExperience && this.defaultExperience()) || "";
|
||||
if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.MongoDB.toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.ApiForMongoDB.toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
this.databaseAccount &&
|
||||
this.databaseAccount() &&
|
||||
this.databaseAccount().kind.toLowerCase() === Constants.AccountKind.MongoDB
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
this.isEnableMongoCapabilityPresent = ko.computed(() => {
|
||||
const capabilities = this.databaseAccount && this.databaseAccount()?.properties?.capabilities;
|
||||
if (!capabilities) {
|
||||
@@ -501,13 +470,6 @@ export default class Explorer {
|
||||
container: this,
|
||||
});
|
||||
|
||||
this.editTableEntityPane = new EditTableEntityPane({
|
||||
id: "edittableentitypane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
container: this,
|
||||
});
|
||||
|
||||
this.cassandraAddCollectionPane = new CassandraAddCollectionPane({
|
||||
id: "cassandraaddcollectionpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
@@ -515,13 +477,6 @@ export default class Explorer {
|
||||
container: this,
|
||||
});
|
||||
|
||||
this.stringInputPane = new StringInputPane({
|
||||
id: "stringinputpane",
|
||||
visible: ko.observable<boolean>(false),
|
||||
|
||||
container: this,
|
||||
});
|
||||
|
||||
this.tabsManager = params?.tabsManager ?? new TabsManager();
|
||||
this.tabsManager.openedTabs.subscribe((tabs) => {
|
||||
if (tabs.length === 0) {
|
||||
@@ -534,9 +489,7 @@ export default class Explorer {
|
||||
this.addDatabasePane,
|
||||
this.addCollectionPane,
|
||||
this.graphStylingPane,
|
||||
this.editTableEntityPane,
|
||||
this.cassandraAddCollectionPane,
|
||||
this.stringInputPane,
|
||||
];
|
||||
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
|
||||
this.isTabsContentExpanded = ko.observable(false);
|
||||
@@ -605,7 +558,6 @@ export default class Explorer {
|
||||
this.addCollectionPane.collectionIdTitle("Table id");
|
||||
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
|
||||
this.refreshTreeTitle("Refresh tables");
|
||||
this.editTableEntityPane.title("Edit Table Entity");
|
||||
this.tableDataClient = new TablesAPIDataClient();
|
||||
break;
|
||||
case "Cassandra":
|
||||
@@ -619,7 +571,6 @@ export default class Explorer {
|
||||
this.addCollectionPane.collectionIdTitle("Table id");
|
||||
this.addCollectionPane.collectionWithThroughputInSharedTitle("Provision dedicated throughput for this table");
|
||||
this.refreshTreeTitle("Refresh tables");
|
||||
this.editTableEntityPane.title("Edit Table Row");
|
||||
this.tableDataClient = new CassandraAPIDataClient();
|
||||
break;
|
||||
}
|
||||
@@ -1432,7 +1383,6 @@ export default class Explorer {
|
||||
): Promise<void> {
|
||||
if (this.notebookManager) {
|
||||
await this.notebookManager.openPublishNotebookPane(name, content, parentDomElement);
|
||||
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
||||
this.isPublishNotebookPaneEnabled(true);
|
||||
}
|
||||
}
|
||||
@@ -1545,7 +1495,7 @@ export default class Explorer {
|
||||
return true;
|
||||
}
|
||||
|
||||
public renameNotebook(notebookFile: NotebookContentItem): Q.Promise<NotebookContentItem> {
|
||||
public renameNotebook(notebookFile: NotebookContentItem): void {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to rename notebook, but notebook is not enabled";
|
||||
handleError(error, "Explorer/renameNotebook");
|
||||
@@ -1561,57 +1511,59 @@ export default class Explorer {
|
||||
);
|
||||
if (openedNotebookTabs.length > 0) {
|
||||
this.showOkModalDialog("Unable to rename file", "This file is being edited. Please close the tab and try again.");
|
||||
return Q.reject();
|
||||
} else {
|
||||
this.openSidePanel(
|
||||
"",
|
||||
<StringInputPane
|
||||
explorer={this}
|
||||
closePanel={() => {
|
||||
this.closeSidePanel();
|
||||
this.resourceTree.triggerRender();
|
||||
}}
|
||||
inputLabel="Enter new notebook name"
|
||||
submitButtonLabel="Rename"
|
||||
errorMessage="Could not rename notebook"
|
||||
inProgressMessage="Renaming notebook to"
|
||||
successMessage="Renamed notebook to"
|
||||
paneTitle="Rename Notebook"
|
||||
defaultInput={FileSystemUtil.stripExtension(notebookFile.name, "ipynb")}
|
||||
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
|
||||
this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input)
|
||||
}
|
||||
notebookFile={notebookFile}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const originalPath = notebookFile.path;
|
||||
const result = this.stringInputPane
|
||||
.openWithOptions<NotebookContentItem>({
|
||||
errorMessage: "Could not rename notebook",
|
||||
inProgressMessage: "Renaming notebook to",
|
||||
successMessage: "Renamed notebook to",
|
||||
inputLabel: "Enter new notebook name",
|
||||
paneTitle: "Rename Notebook",
|
||||
submitButtonLabel: "Rename",
|
||||
defaultInput: FileSystemUtil.stripExtension(notebookFile.name, "ipynb"),
|
||||
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.renameNotebook(notebookFile, input),
|
||||
})
|
||||
.then((newNotebookFile) => {
|
||||
const notebookTabs = this.tabsManager.getTabs(
|
||||
ViewModels.CollectionTabKind.NotebookV2,
|
||||
(tab: NotebookV2Tab) => tab.notebookPath && FileSystemUtil.isPathEqual(tab.notebookPath(), originalPath)
|
||||
);
|
||||
notebookTabs.forEach((tab) => {
|
||||
tab.tabTitle(newNotebookFile.name);
|
||||
tab.tabPath(newNotebookFile.path);
|
||||
(tab as NotebookV2Tab).notebookPath(newNotebookFile.path);
|
||||
});
|
||||
|
||||
return newNotebookFile;
|
||||
});
|
||||
result.then(() => this.resourceTree.triggerRender());
|
||||
return result;
|
||||
}
|
||||
|
||||
public onCreateDirectory(parent: NotebookContentItem): Q.Promise<NotebookContentItem> {
|
||||
public onCreateDirectory(parent: NotebookContentItem): void {
|
||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||
const error = "Attempt to create notebook directory, but notebook is not enabled";
|
||||
handleError(error, "Explorer/onCreateDirectory");
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const result = this.stringInputPane.openWithOptions<NotebookContentItem>({
|
||||
errorMessage: "Could not create directory ",
|
||||
inProgressMessage: "Creating directory ",
|
||||
successMessage: "Created directory ",
|
||||
inputLabel: "Enter new directory name",
|
||||
paneTitle: "Create new directory",
|
||||
submitButtonLabel: "Create",
|
||||
defaultInput: "",
|
||||
onSubmit: (input: string) => this.notebookManager?.notebookContentClient.createDirectory(parent, input),
|
||||
});
|
||||
result.then(() => this.resourceTree.triggerRender());
|
||||
return result;
|
||||
this.openSidePanel(
|
||||
"",
|
||||
<StringInputPane
|
||||
explorer={this}
|
||||
closePanel={() => {
|
||||
this.closeSidePanel();
|
||||
this.resourceTree.triggerRender();
|
||||
}}
|
||||
errorMessage="Could not create directory "
|
||||
inProgressMessage="Creating directory "
|
||||
successMessage="Created directory "
|
||||
inputLabel="Enter new directory name"
|
||||
paneTitle="Create new directory"
|
||||
submitButtonLabel="Create"
|
||||
defaultInput=""
|
||||
onSubmit={(notebookFile: NotebookContentItem, input: string): Promise<NotebookContentItem> =>
|
||||
this.notebookManager?.notebookContentClient.createDirectory(notebookFile, input)
|
||||
}
|
||||
notebookFile={parent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public readFile(notebookFile: NotebookContentItem): Promise<string> {
|
||||
@@ -1935,33 +1887,35 @@ export default class Explorer {
|
||||
const title = "Gallery";
|
||||
const hashLocation = "gallery";
|
||||
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
|
||||
|
||||
const galleryTabOptions: GalleryTabOptions = {
|
||||
account: userContext.databaseAccount,
|
||||
container: this,
|
||||
junoClient: this.notebookManager?.junoClient,
|
||||
selectedTab: selectedTab || GalleryTabKind.PublicGallery,
|
||||
notebookUrl,
|
||||
galleryItem,
|
||||
isFavorite,
|
||||
tabKind: ViewModels.CollectionTabKind.Gallery,
|
||||
title: title,
|
||||
tabPath: title,
|
||||
hashLocation: hashLocation,
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
onLoadStartKey: null,
|
||||
};
|
||||
|
||||
const galleryTab = this.tabsManager
|
||||
.getTabs(ViewModels.CollectionTabKind.Gallery)
|
||||
.find((tab) => tab.hashLocation() == hashLocation);
|
||||
|
||||
if (galleryTab instanceof GalleryTab) {
|
||||
this.tabsManager.activateTab(galleryTab);
|
||||
galleryTab.reset(galleryTabOptions);
|
||||
} else {
|
||||
this.tabsManager.activateNewTab(new GalleryTab(galleryTabOptions));
|
||||
this.tabsManager.activateNewTab(
|
||||
new GalleryTab(
|
||||
{
|
||||
tabKind: ViewModels.CollectionTabKind.Gallery,
|
||||
title: title,
|
||||
tabPath: title,
|
||||
hashLocation: hashLocation,
|
||||
onUpdateTabsButtons: this.onUpdateTabsButtons,
|
||||
onLoadStartKey: null,
|
||||
isTabsContentExpanded: ko.observable(true),
|
||||
},
|
||||
{
|
||||
account: userContext.databaseAccount,
|
||||
container: this,
|
||||
junoClient: this.notebookManager?.junoClient,
|
||||
selectedTab: selectedTab || GalleryTabKind.PublicGallery,
|
||||
notebookUrl,
|
||||
galleryItem,
|
||||
isFavorite,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2163,8 +2117,7 @@ export default class Explorer {
|
||||
);
|
||||
}
|
||||
public openAddDatabasePane(): void {
|
||||
let isEnable = true;
|
||||
if (isEnable) {
|
||||
if (userContext.features.enableKOPanel) {
|
||||
this.addDatabasePane.open();
|
||||
document.getElementById("linkAddDatabase").focus();
|
||||
} 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 {
|
||||
this.openSidePanel(
|
||||
"Select Column",
|
||||
|
||||
@@ -11,7 +11,6 @@ import * as ko from "knockout";
|
||||
import Q from "q";
|
||||
import _ from "underscore";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { HashMap } from "../../../Common/HashMap";
|
||||
import { NeighborType } from "../../../Contracts/ViewModels";
|
||||
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { GraphConfig } from "../../Tabs/GraphTab";
|
||||
@@ -195,8 +194,8 @@ export class D3ForceGraph implements GraphRenderer {
|
||||
* Count edges and store in a hashmap: vertex id <--> number of links
|
||||
* @param linkSelection
|
||||
*/
|
||||
public static countEdges(links: D3Link[]): HashMap<number> {
|
||||
const countMap = new HashMap<number>();
|
||||
public static countEdges(links: D3Link[]): Map<string, number> {
|
||||
const countMap = new Map<string, number>();
|
||||
links.forEach((l: D3Link) => {
|
||||
let val = countMap.get(l.inV) || 0;
|
||||
val += 1;
|
||||
@@ -407,7 +406,7 @@ export class D3ForceGraph implements GraphRenderer {
|
||||
const rootId = graph.findRootNodeId();
|
||||
|
||||
// Remember nodes current position
|
||||
const posMap = new HashMap<Point2D>();
|
||||
const posMap = new Map<string, Point2D>();
|
||||
this.simulation.nodes().forEach((d: D3Node) => {
|
||||
if (d.x == undefined || d.y == undefined) {
|
||||
return;
|
||||
@@ -501,8 +500,8 @@ export class D3ForceGraph implements GraphRenderer {
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
const nodeFinalPositionMap = new HashMap<Point2D>();
|
||||
|
||||
const nodeFinalPositionMap = new Map<string, Point2D>();
|
||||
const viewCenter = this.viewCenter;
|
||||
const nonFixedNodes = _.filter(nodes, (node: D3Node) => {
|
||||
return !node._isFixedPosition && node.x === viewCenter.x && node.y === viewCenter.y;
|
||||
@@ -559,7 +558,7 @@ export class D3ForceGraph implements GraphRenderer {
|
||||
newNodes.selectAll(".loadmore").attr("visibility", "hidden").transition().delay(600).attr("visibility", "visible");
|
||||
}
|
||||
|
||||
private restartSimulation(graph: GraphData<D3Node, D3Link>, posMap: HashMap<Point2D>) {
|
||||
private restartSimulation(graph: GraphData<D3Node, D3Link>, posMap: Map<string, Point2D>) {
|
||||
if (!graph) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ObjectCache } from "../../../Common/ObjectCache";
|
||||
import { GremlinVertex, GraphData } from "./GraphData";
|
||||
import { GraphData, GremlinVertex } from "./GraphData";
|
||||
|
||||
/**
|
||||
* Remember vertex edge information
|
||||
@@ -10,9 +10,8 @@ export class EdgeInfoCache extends ObjectCache<GremlinVertex> {
|
||||
* @param vertex
|
||||
*/
|
||||
public addVertex(vertex: GremlinVertex): void {
|
||||
let v: GremlinVertex;
|
||||
if (super.has(vertex.id)) {
|
||||
v = super.get(vertex.id);
|
||||
let v = super.get(vertex.id);
|
||||
if (super.has(vertex.id) && v) {
|
||||
GraphData.addEdgeInfoToVertex(v, vertex);
|
||||
v._outEdgeIds = vertex._outEdgeIds;
|
||||
v._inEdgeIds = vertex._inEdgeIds;
|
||||
@@ -29,8 +28,8 @@ export class EdgeInfoCache extends ObjectCache<GremlinVertex> {
|
||||
* @param id
|
||||
*/
|
||||
public mergeEdgeInfo(target: GremlinVertex): void {
|
||||
if (super.has(target.id)) {
|
||||
const cachedVertex = super.get(target.id);
|
||||
const cachedVertex = super.get(target.id);
|
||||
if (super.has(target.id) && cachedVertex) {
|
||||
GraphData.addEdgeInfoToVertex(target, cachedVertex);
|
||||
target._outEdgeIds = cachedVertex._outEdgeIds;
|
||||
target._inEdgeIds = cachedVertex._inEdgeIds;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as sinon from "sinon";
|
||||
import { GremlinClient, GremlinClientParameters } from "./GremlinClient";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import { GremlinClient, GremlinClientParameters } from "./GremlinClient";
|
||||
|
||||
describe("Gremlin Client", () => {
|
||||
const emptyParams: GremlinClientParameters = {
|
||||
@@ -70,7 +70,7 @@ describe("Gremlin Client", () => {
|
||||
gremlinClient.execute("fake query");
|
||||
gremlinClient.execute("fake query");
|
||||
gremlinClient.execute("fake query");
|
||||
expect(gremlinClient.pendingResults.size()).toBe(3);
|
||||
expect(gremlinClient.pendingResults.size).toBe(3);
|
||||
});
|
||||
|
||||
it("should clean up pending request ids after success", async () => {
|
||||
@@ -89,7 +89,7 @@ describe("Gremlin Client", () => {
|
||||
return requestId;
|
||||
});
|
||||
await gremlinClient.execute("fake query");
|
||||
expect(gremlinClient.pendingResults.size()).toBe(0);
|
||||
expect(gremlinClient.pendingResults.size).toBe(0);
|
||||
});
|
||||
|
||||
it("should log and display error out on unknown requestId", () => {
|
||||
@@ -247,7 +247,7 @@ describe("Gremlin Client", () => {
|
||||
sinon.stub(gremlinClient.client, "executeGremlinQuery").callsFake((query: string): string => requestId);
|
||||
gremlinClient.execute("fake query").finally(() => {
|
||||
try {
|
||||
expect(gremlinClient.pendingResults.size()).toBe(0);
|
||||
expect(gremlinClient.pendingResults.size).toBe(0);
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
import * as Q from "q";
|
||||
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
|
||||
import { HashMap } from "../../../Common/HashMap";
|
||||
import { logConsoleInfo } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { GremlinSimpleClient, Result } from "./GremlinSimpleClient";
|
||||
|
||||
@@ -30,7 +29,7 @@ interface PendingResultData {
|
||||
|
||||
export class GremlinClient {
|
||||
public client: GremlinSimpleClient;
|
||||
public pendingResults: HashMap<PendingResultData>; // public for testing purposes
|
||||
public pendingResults: Map<string, PendingResultData>; // public for testing purposes
|
||||
private maxResultSize: number;
|
||||
private static readonly PENDING_REQUEST_TIMEOUT_MS = 6 /* minutes */ * 60 /* seconds */ * 1000 /* ms */;
|
||||
private static readonly TIMEOUT_ERROR_MSG = `Pending request timed out (${GremlinClient.PENDING_REQUEST_TIMEOUT_MS} ms)`;
|
||||
@@ -38,7 +37,7 @@ export class GremlinClient {
|
||||
|
||||
public initialize(params: GremlinClientParameters) {
|
||||
this.destroy();
|
||||
this.pendingResults = new HashMap();
|
||||
this.pendingResults = new Map();
|
||||
this.maxResultSize = params.maxResultSize;
|
||||
|
||||
this.client = new GremlinSimpleClient({
|
||||
@@ -68,9 +67,9 @@ export class GremlinClient {
|
||||
|
||||
// Fail all pending requests if no request id (fatal)
|
||||
if (!requestId) {
|
||||
this.pendingResults.keys().forEach((reqId: string) => {
|
||||
for (const reqId of this.pendingResults.keys()) {
|
||||
this.abortPendingRequest(reqId, errorMessage, null);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.abortPendingRequest(requestId, errorMessage, result.requestCharge);
|
||||
|
||||
5
src/Explorer/LazyMonaco.ts
Normal file
5
src/Explorer/LazyMonaco.ts
Normal 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");
|
||||
@@ -29,7 +29,6 @@ export class CommandBarComponentAdapter implements ReactAdapter {
|
||||
|
||||
// These are the parameters watched by the react binding that will trigger a renderComponent() if one of the ko mutates
|
||||
const toWatch = [
|
||||
container.isPreferredApiMongoDB,
|
||||
container.deleteCollectionText,
|
||||
container.deleteDatabaseText,
|
||||
container.addCollectionText,
|
||||
|
||||
@@ -23,7 +23,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
|
||||
@@ -67,7 +66,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
@@ -128,6 +126,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
|
||||
beforeAll(() => {
|
||||
mockExplorer = {} as Explorer;
|
||||
mockExplorer.addDatabaseText = ko.observable("mockText");
|
||||
mockExplorer.addCollectionText = ko.observable("mockText");
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
@@ -143,16 +142,25 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
mockExplorer.isServerlessEnabled = ko.computed<boolean>(() => false);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => true);
|
||||
updateUserContext({
|
||||
apiType: "Mongo",
|
||||
});
|
||||
mockExplorer.isNotebookEnabled = ko.observable(false);
|
||||
mockExplorer.isNotebooksEnabledForAccount = ko.observable(false);
|
||||
mockExplorer.isRunningOnNationalCloud = ko.observable(false);
|
||||
});
|
||||
|
||||
it("Mongo Api not available - button should be hidden", () => {
|
||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
|
||||
updateUserContext({
|
||||
apiType: "SQL",
|
||||
});
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer);
|
||||
const openMongoShellBtn = buttons.find((button) => button.commandButtonLabel === openMongoShellBtnLabel);
|
||||
expect(openMongoShellBtn).toBeUndefined();
|
||||
@@ -222,7 +230,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
|
||||
@@ -321,7 +328,6 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
mockExplorer.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
|
||||
|
||||
mockExplorer.isSynapseLinkUpdating = ko.observable(false);
|
||||
mockExplorer.isSparkEnabled = ko.observable(true);
|
||||
|
||||
@@ -70,7 +70,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
|
||||
buttons.push(createEnableNotebooksButton(container));
|
||||
}
|
||||
|
||||
if (container.isPreferredApiMongoDB()) {
|
||||
if (userContext.apiType === "Mongo") {
|
||||
buttons.push(createOpenMongoTerminalButton(container));
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
|
||||
}
|
||||
|
||||
const isSupportedOpenQueryApi =
|
||||
userContext.apiType === "SQL" || container.isPreferredApiMongoDB() || userContext.apiType === "Gremlin";
|
||||
userContext.apiType === "SQL" || userContext.apiType === "Mongo" || userContext.apiType === "Gremlin";
|
||||
const isSupportedOpenQueryFromDiskApi = userContext.apiType === "SQL" || userContext.apiType === "Gremlin";
|
||||
if (isSupportedOpenQueryApi && container.selectedNode() && container.findSelectedCollection()) {
|
||||
const openQueryBtn = createOpenQueryButton(container);
|
||||
@@ -133,7 +133,7 @@ export function createStaticCommandBarButtons(container: Explorer): CommandButto
|
||||
export function createContextCommandBarButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
|
||||
if (!container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB()) {
|
||||
if (!container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo") {
|
||||
const label = "New Shell";
|
||||
const newMongoShellBtn: CommandButtonComponentProps = {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
@@ -145,7 +145,7 @@ export function createContextCommandBarButtons(container: Explorer): CommandButt
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
disabled: container.isDatabaseNodeOrNoneSelected() && container.isPreferredApiMongoDB(),
|
||||
disabled: container.isDatabaseNodeOrNoneSelected() && userContext.apiType === "Mongo",
|
||||
};
|
||||
buttons.push(newMongoShellBtn);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ let fakeAjaxResponse: AjaxResponse = {
|
||||
responseType: "json",
|
||||
};
|
||||
export const sessions = {
|
||||
create: (serverConfig: unknown, body: object): Observable<AjaxResponse> => of(fakeAjaxResponse),
|
||||
create: (): Observable<AjaxResponse> => of(fakeAjaxResponse),
|
||||
__setResponse: (response: AjaxResponse) => {
|
||||
fakeAjaxResponse = response;
|
||||
},
|
||||
|
||||
@@ -220,7 +220,7 @@ export class NotebookContentClient {
|
||||
return this.contentProvider
|
||||
.remove(this.getServerConfig(), path)
|
||||
.toPromise()
|
||||
.then((xhr: AjaxResponse) => path);
|
||||
.then(() => path);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Contains all notebook related stuff meant to be dynamically loaded by explorer
|
||||
*/
|
||||
|
||||
import type { ImmutableNotebook } from "@nteract/commutable";
|
||||
import { ImmutableNotebook } from "@nteract/commutable";
|
||||
import type { IContentProvider } from "@nteract/core";
|
||||
import ko from "knockout";
|
||||
import React from "react";
|
||||
@@ -22,7 +22,7 @@ import Explorer from "../Explorer";
|
||||
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
|
||||
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
|
||||
import { GitHubReposPane } from "../Panes/GitHubReposPane";
|
||||
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
|
||||
import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
|
||||
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
|
||||
import { NotebookContainerClient } from "./NotebookContainerClient";
|
||||
@@ -53,7 +53,6 @@ export default class NotebookManager {
|
||||
private gitHubClient: GitHubClient;
|
||||
|
||||
public gitHubReposPane: ContextualPaneBase;
|
||||
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
|
||||
|
||||
public initialize(params: NotebookManagerOptions): void {
|
||||
this.params = params;
|
||||
@@ -91,8 +90,6 @@ export default class NotebookManager {
|
||||
this.notebookContentProvider
|
||||
);
|
||||
|
||||
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
|
||||
|
||||
this.gitHubOAuthService.getTokenObservable().subscribe((token) => {
|
||||
this.gitHubClient.setToken(token?.access_token);
|
||||
|
||||
@@ -123,7 +120,20 @@ export default class NotebookManager {
|
||||
content: NotebookPaneContent,
|
||||
parentDomElement: HTMLElement
|
||||
): Promise<void> {
|
||||
await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement);
|
||||
const explorer = this.params.container;
|
||||
explorer.openSidePanel(
|
||||
"New Collection",
|
||||
<PublishNotebookPane
|
||||
explorer={this.params.container}
|
||||
junoClient={this.junoClient}
|
||||
closePanel={this.params.container.closeSidePanel}
|
||||
openNotificationConsole={this.params.container.expandConsole}
|
||||
name={name}
|
||||
author={getFullName()}
|
||||
notebookContent={content}
|
||||
parentDomElement={parentDomElement}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
public openCopyNotebookPane(name: string, content: string): void {
|
||||
|
||||
@@ -251,10 +251,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Indexing For Shared Throughput - start -->
|
||||
<div
|
||||
class="seconddivpadding"
|
||||
data-bind="visible: showIndexingOptionsForSharedThroughput() && !container.isPreferredApiMongoDB()"
|
||||
>
|
||||
<div class="seconddivpadding" data-bind="visible: showIndexingOptionsForSharedThroughput() && !isMongo()">
|
||||
<div
|
||||
class="useIndexingForSharedThroughput createNewDatabaseOrUseExisting"
|
||||
aria-label="Indexing For Shared Throughput"
|
||||
@@ -297,7 +294,7 @@
|
||||
|
||||
<p
|
||||
class="seconddivpadding"
|
||||
data-bind="visible: container.isPreferredApiMongoDB() && !databaseHasSharedOffer() || container.isFixedCollectionWithSharedThroughputSupported"
|
||||
data-bind="visible: isMongo() && !databaseHasSharedOffer() || container.isFixedCollectionWithSharedThroughputSupported"
|
||||
>
|
||||
<span class="mandatoryStar">*</span>
|
||||
<span class="addCollectionLabel">Storage capacity</span>
|
||||
@@ -312,7 +309,7 @@
|
||||
<div class="tabs">
|
||||
<div
|
||||
tabindex="0"
|
||||
data-bind="event: { keydown: onStorageOptionsKeyDown }, visible: container.isPreferredApiMongoDB() && !databaseHasSharedOffer() || container.isFixedCollectionWithSharedThroughputSupported"
|
||||
data-bind="event: { keydown: onStorageOptionsKeyDown }, visible: isMongo() && !databaseHasSharedOffer() || container.isFixedCollectionWithSharedThroughputSupported"
|
||||
aria-label="Storage capacity"
|
||||
>
|
||||
<!-- Fixed option button - Start -->
|
||||
|
||||
@@ -119,7 +119,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
this.isPreferredApiTable = options.isPreferredApiTable;
|
||||
this.partitionKey = ko.observable<string>();
|
||||
this.partitionKey.subscribe((newPartitionKey: string) => {
|
||||
if (this.container.isPreferredApiMongoDB() || !newPartitionKey || newPartitionKey[0] === "/") {
|
||||
if (userContext.apiType === "Mongo" || !newPartitionKey || newPartitionKey[0] === "/") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
|
||||
// TODO: Create derived classes for Tables and Mongo to replace the If statements below
|
||||
this.partitionKeyName = ko.computed<string>(() => {
|
||||
if (this.container && !!this.container.isPreferredApiMongoDB()) {
|
||||
if (userContext.apiType === "Mongo") {
|
||||
return "Shard key";
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
this.lowerCasePartitionKeyName = ko.computed<string>(() => this.partitionKeyName().toLowerCase());
|
||||
|
||||
this.partitionKeyPlaceholder = ko.computed<string>(() => {
|
||||
if (this.container && !!this.container.isPreferredApiMongoDB()) {
|
||||
if (userContext.apiType === "Mongo") {
|
||||
return "e.g., address.zipCode";
|
||||
}
|
||||
|
||||
@@ -376,7 +376,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
});
|
||||
|
||||
this.uniqueKeysPlaceholder = ko.pureComputed<string>(() => {
|
||||
if (this.container && !!this.container.isPreferredApiMongoDB()) {
|
||||
if (userContext.apiType === "Mongo") {
|
||||
return "Comma separated paths e.g. firstName,address.zipCode";
|
||||
}
|
||||
|
||||
@@ -396,11 +396,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.container.isPreferredApiMongoDB() &&
|
||||
!this.isUnlimitedStorageSelected() &&
|
||||
this.databaseHasSharedOffer()
|
||||
) {
|
||||
if (userContext.apiType === "Mongo" && !this.isUnlimitedStorageSelected() && this.databaseHasSharedOffer()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -589,7 +585,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.container.isPreferredApiMongoDB()) {
|
||||
if (userContext.apiType === "Mongo") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -728,6 +724,10 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
}
|
||||
}
|
||||
|
||||
private isMongo(): boolean {
|
||||
return userContext.apiType === "Mongo";
|
||||
}
|
||||
|
||||
private _onDatabasesChange(newDatabaseIds: ViewModels.Database[]) {
|
||||
this.databaseIds(newDatabaseIds?.map((database: ViewModels.Database) => database.id()));
|
||||
}
|
||||
@@ -810,7 +810,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
let indexingPolicy: DataModels.IndexingPolicy;
|
||||
let createMongoWildcardIndex: boolean;
|
||||
// todo - remove mongo indexing policy ticket # 616274
|
||||
if (this.container.isPreferredApiMongoDB() && this.container.isEnableMongoCapabilityPresent()) {
|
||||
if (userContext.apiType === "Mongo" && this.container.isEnableMongoCapabilityPresent()) {
|
||||
createMongoWildcardIndex = this.shouldCreateMongoWildcardIndex();
|
||||
} else if (this.showIndexingOptionsForSharedThroughput()) {
|
||||
if (this.useIndexingForSharedThroughput()) {
|
||||
@@ -1145,7 +1145,7 @@ export default class AddCollectionPane extends ContextualPaneBase {
|
||||
let transform = (value: string) => {
|
||||
return value;
|
||||
};
|
||||
if (this.container.isPreferredApiMongoDB()) {
|
||||
if (userContext.apiType === "Mongo") {
|
||||
transform = (value: string) => {
|
||||
return this._convertShardKeyToPartitionKey(value);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as ko from "knockout";
|
||||
import * as _ from "underscore";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { HashMap } from "../../Common/HashMap";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@@ -51,7 +50,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
public ruToolTipText: ko.Computed<string>;
|
||||
public canConfigureThroughput: ko.PureComputed<boolean>;
|
||||
|
||||
private keyspaceOffers: HashMap<DataModels.Offer>;
|
||||
private keyspaceOffers: Map<string, DataModels.Offer>;
|
||||
|
||||
constructor(options: ViewModels.PaneOptions) {
|
||||
super(options);
|
||||
@@ -60,7 +59,7 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
|
||||
this.keyspaceCreateNew = ko.observable<boolean>(true);
|
||||
this.ruToolTipText = ko.pureComputed(() => PricingUtils.getRuToolTipText());
|
||||
this.canConfigureThroughput = ko.pureComputed(() => !this.container.isServerlessEnabled());
|
||||
this.keyspaceOffers = new HashMap<DataModels.Offer>();
|
||||
this.keyspaceOffers = new Map();
|
||||
this.keyspaceIds = ko.observableArray<string>();
|
||||
this.keyspaceHasSharedOffer = ko.observable<boolean>(false);
|
||||
this.keyspaceThroughput = ko.observable<number>();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@ import AddDatabasePaneTemplate from "./AddDatabasePane.html";
|
||||
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
|
||||
import GitHubReposPaneTemplate from "./GitHubReposPane.html";
|
||||
import GraphStylingPaneTemplate from "./GraphStylingPane.html";
|
||||
import StringInputPaneTemplate from "./StringInputPane.html";
|
||||
import TableAddEntityPaneTemplate from "./Tables/TableAddEntityPane.html";
|
||||
import TableEditEntityPaneTemplate from "./Tables/TableEditEntityPane.html";
|
||||
|
||||
@@ -65,15 +64,6 @@ export class CassandraAddCollectionPaneComponent {
|
||||
}
|
||||
}
|
||||
|
||||
export class StringInputPaneComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: PaneComponent,
|
||||
template: StringInputPaneTemplate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class GitHubReposPaneComponent {
|
||||
constructor() {
|
||||
return {
|
||||
|
||||
@@ -8,14 +8,15 @@ describe("PublishNotebookPaneComponent", () => {
|
||||
notebookName: "SampleNotebook.ipynb",
|
||||
notebookDescription: "sample description",
|
||||
notebookTags: "tag1, tag2",
|
||||
imageSrc: "https://i.ytimg.com/vi/E_lByLdKeKY/maxresdefault.jpg",
|
||||
notebookAuthor: "CosmosDB",
|
||||
notebookCreatedDate: "2020-07-17T00:00:00Z",
|
||||
notebookObject: undefined,
|
||||
notebookParentDomElement: undefined,
|
||||
onChangeName: undefined,
|
||||
onChangeDescription: undefined,
|
||||
onChangeTags: undefined,
|
||||
onChangeImageSrc: undefined,
|
||||
setNotebookName: undefined,
|
||||
setNotebookDescription: undefined,
|
||||
setNotebookTags: undefined,
|
||||
setImageSrc: undefined,
|
||||
onError: undefined,
|
||||
clearFormError: undefined,
|
||||
};
|
||||
205
src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.tsx
Normal file
205
src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -88,7 +88,7 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
||||
Object {
|
||||
"author": "CosmosDB",
|
||||
"created": "2020-07-17T00:00:00Z",
|
||||
"description": "",
|
||||
"description": "sample description",
|
||||
"downloads": undefined,
|
||||
"favorites": undefined,
|
||||
"gitSha": undefined,
|
||||
@@ -99,12 +99,14 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
||||
"pendingScanJobIds": undefined,
|
||||
"policyViolations": undefined,
|
||||
"tags": Array [
|
||||
"",
|
||||
"tag1",
|
||||
" tag2",
|
||||
],
|
||||
"thumbnailUrl": undefined,
|
||||
"thumbnailUrl": "https://i.ytimg.com/vi/E_lByLdKeKY/maxresdefault.jpg",
|
||||
"views": undefined,
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
showDelete={false}
|
||||
showDownload={false}
|
||||
/>
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -153,40 +153,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
EditTableEntityPane {
|
||||
"addButtonLabel": "Add Property",
|
||||
"attributeNameLabel": "Property Name",
|
||||
"attributeValueLabel": "Value",
|
||||
"canAdd": [Function],
|
||||
"canApply": [Function],
|
||||
"container": [Circular],
|
||||
"dataTypeLabel": "Type",
|
||||
"displayedAttributes": [Function],
|
||||
"editAttribute": [Function],
|
||||
"editButtonLabel": "Edit",
|
||||
"editingProperty": [Function],
|
||||
"edmTypes": [Function],
|
||||
"finishEditingAttribute": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "edittableentitypane",
|
||||
"insertAttribute": [Function],
|
||||
"isEditing": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"onAddPropertyKeyDown": [Function],
|
||||
"onBackButtonKeyDown": [Function],
|
||||
"onDeletePropertyKeyDown": [Function],
|
||||
"onEditPropertyKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"removeAttribute": [Function],
|
||||
"removeButtonLabel": "Remove",
|
||||
"scrollId": [Function],
|
||||
"submitButtonText": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
CassandraAddCollectionPane {
|
||||
"autoPilotUsageCost": [Function],
|
||||
"canConfigureThroughput": [Function],
|
||||
@@ -209,9 +175,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
"keyspaceHasSharedOffer": [Function],
|
||||
"keyspaceId": [Function],
|
||||
"keyspaceIds": [Function],
|
||||
"keyspaceOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"keyspaceOffers": Map {},
|
||||
"keyspaceThroughput": [Function],
|
||||
"maxThroughputRU": [Function],
|
||||
"minThroughputRU": [Function],
|
||||
@@ -234,20 +198,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
"userTableQuery": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
StringInputPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "stringinputpane",
|
||||
"inputLabel": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"stringInput": [Function],
|
||||
"submitButtonLabel": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
],
|
||||
"_refreshSparkEnabledStateForAccount": [Function],
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
@@ -402,9 +352,7 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
"keyspaceHasSharedOffer": [Function],
|
||||
"keyspaceId": [Function],
|
||||
"keyspaceIds": [Function],
|
||||
"keyspaceOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"keyspaceOffers": Map {},
|
||||
"keyspaceThroughput": [Function],
|
||||
"maxThroughputRU": [Function],
|
||||
"minThroughputRU": [Function],
|
||||
@@ -455,40 +403,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
"defaultExperience": [Function],
|
||||
"deleteCollectionText": [Function],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
"addButtonLabel": "Add Property",
|
||||
"attributeNameLabel": "Property Name",
|
||||
"attributeValueLabel": "Value",
|
||||
"canAdd": [Function],
|
||||
"canApply": [Function],
|
||||
"container": [Circular],
|
||||
"dataTypeLabel": "Type",
|
||||
"displayedAttributes": [Function],
|
||||
"editAttribute": [Function],
|
||||
"editButtonLabel": "Edit",
|
||||
"editingProperty": [Function],
|
||||
"edmTypes": [Function],
|
||||
"finishEditingAttribute": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "edittableentitypane",
|
||||
"insertAttribute": [Function],
|
||||
"isEditing": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"onAddPropertyKeyDown": [Function],
|
||||
"onBackButtonKeyDown": [Function],
|
||||
"onDeletePropertyKeyDown": [Function],
|
||||
"onEditPropertyKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"removeAttribute": [Function],
|
||||
"removeButtonLabel": "Remove",
|
||||
"scrollId": [Function],
|
||||
"submitButtonText": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"graphStylingPane": GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -520,7 +434,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isPreferredApiMongoDB": [Function],
|
||||
"isPublishNotebookPaneEnabled": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isRightPanelV2Enabled": [Function],
|
||||
@@ -552,21 +465,9 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"databaseCollectionIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"koSubsCollectionIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"koSubsDatabaseIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"databaseCollectionIdMap": Map {},
|
||||
"koSubsCollectionIdMap": Map {},
|
||||
"koSubsDatabaseIdMap": Map {},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
@@ -592,20 +493,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
"onResizeStop": [Function],
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"stringInputPane": StringInputPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "stringinputpane",
|
||||
"inputLabel": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"stringInput": [Function],
|
||||
"submitButtonLabel": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
@@ -904,40 +791,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
EditTableEntityPane {
|
||||
"addButtonLabel": "Add Property",
|
||||
"attributeNameLabel": "Property Name",
|
||||
"attributeValueLabel": "Value",
|
||||
"canAdd": [Function],
|
||||
"canApply": [Function],
|
||||
"container": [Circular],
|
||||
"dataTypeLabel": "Type",
|
||||
"displayedAttributes": [Function],
|
||||
"editAttribute": [Function],
|
||||
"editButtonLabel": "Edit",
|
||||
"editingProperty": [Function],
|
||||
"edmTypes": [Function],
|
||||
"finishEditingAttribute": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "edittableentitypane",
|
||||
"insertAttribute": [Function],
|
||||
"isEditing": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"onAddPropertyKeyDown": [Function],
|
||||
"onBackButtonKeyDown": [Function],
|
||||
"onDeletePropertyKeyDown": [Function],
|
||||
"onEditPropertyKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"removeAttribute": [Function],
|
||||
"removeButtonLabel": "Remove",
|
||||
"scrollId": [Function],
|
||||
"submitButtonText": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
CassandraAddCollectionPane {
|
||||
"autoPilotUsageCost": [Function],
|
||||
"canConfigureThroughput": [Function],
|
||||
@@ -960,9 +813,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
"keyspaceHasSharedOffer": [Function],
|
||||
"keyspaceId": [Function],
|
||||
"keyspaceIds": [Function],
|
||||
"keyspaceOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"keyspaceOffers": Map {},
|
||||
"keyspaceThroughput": [Function],
|
||||
"maxThroughputRU": [Function],
|
||||
"minThroughputRU": [Function],
|
||||
@@ -985,20 +836,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
"userTableQuery": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
StringInputPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "stringinputpane",
|
||||
"inputLabel": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"stringInput": [Function],
|
||||
"submitButtonLabel": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
],
|
||||
"_refreshSparkEnabledStateForAccount": [Function],
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
@@ -1153,9 +990,7 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
"keyspaceHasSharedOffer": [Function],
|
||||
"keyspaceId": [Function],
|
||||
"keyspaceIds": [Function],
|
||||
"keyspaceOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"keyspaceOffers": Map {},
|
||||
"keyspaceThroughput": [Function],
|
||||
"maxThroughputRU": [Function],
|
||||
"minThroughputRU": [Function],
|
||||
@@ -1206,40 +1041,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
"defaultExperience": [Function],
|
||||
"deleteCollectionText": [Function],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
"addButtonLabel": "Add Property",
|
||||
"attributeNameLabel": "Property Name",
|
||||
"attributeValueLabel": "Value",
|
||||
"canAdd": [Function],
|
||||
"canApply": [Function],
|
||||
"container": [Circular],
|
||||
"dataTypeLabel": "Type",
|
||||
"displayedAttributes": [Function],
|
||||
"editAttribute": [Function],
|
||||
"editButtonLabel": "Edit",
|
||||
"editingProperty": [Function],
|
||||
"edmTypes": [Function],
|
||||
"finishEditingAttribute": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "edittableentitypane",
|
||||
"insertAttribute": [Function],
|
||||
"isEditing": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"onAddPropertyKeyDown": [Function],
|
||||
"onBackButtonKeyDown": [Function],
|
||||
"onDeletePropertyKeyDown": [Function],
|
||||
"onEditPropertyKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"removeAttribute": [Function],
|
||||
"removeButtonLabel": "Remove",
|
||||
"scrollId": [Function],
|
||||
"submitButtonText": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"graphStylingPane": GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -1271,7 +1072,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isPreferredApiMongoDB": [Function],
|
||||
"isPublishNotebookPaneEnabled": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isRightPanelV2Enabled": [Function],
|
||||
@@ -1303,21 +1103,9 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"databaseCollectionIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"koSubsCollectionIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"koSubsDatabaseIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"databaseCollectionIdMap": Map {},
|
||||
"koSubsCollectionIdMap": Map {},
|
||||
"koSubsDatabaseIdMap": Map {},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
@@ -1343,20 +1131,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
"onResizeStop": [Function],
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"stringInputPane": StringInputPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "stringinputpane",
|
||||
"inputLabel": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"stringInput": [Function],
|
||||
"submitButtonLabel": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
|
||||
@@ -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>
|
||||
@@ -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("");
|
||||
}
|
||||
}
|
||||
28
src/Explorer/Panes/StringInputPane/StringInputPane.test.tsx
Normal file
28
src/Explorer/Panes/StringInputPane/StringInputPane.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
122
src/Explorer/Panes/StringInputPane/StringInputPane.tsx
Normal file
122
src/Explorer/Panes/StringInputPane/StringInputPane.tsx
Normal 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
51
src/Explorer/Panes/Tables/EditTableEntityPanel.test.tsx
Normal file
51
src/Explorer/Panes/Tables/EditTableEntityPanel.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
419
src/Explorer/Panes/Tables/EditTableEntityPanel.tsx
Normal file
419
src/Explorer/Panes/Tables/EditTableEntityPanel.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -5,28 +5,6 @@ import * as Entities from "../../../Tables/Entities";
|
||||
import * as Utilities from "../../../Tables/Utilities";
|
||||
|
||||
export const defaultStringPlaceHolder = "Enter identifier value.";
|
||||
export const defaultEntities = [
|
||||
{
|
||||
property: "PartitionKey",
|
||||
type: "String",
|
||||
value: "",
|
||||
isPropertyTypeDisable: true,
|
||||
isDeleteOptionVisible: false,
|
||||
id: 1,
|
||||
entityValuePlaceholder: defaultStringPlaceHolder,
|
||||
isEntityTypeDate: false,
|
||||
},
|
||||
{
|
||||
property: "RowKey",
|
||||
type: "String",
|
||||
value: "",
|
||||
isPropertyTypeDisable: true,
|
||||
isDeleteOptionVisible: false,
|
||||
id: 2,
|
||||
entityValuePlaceholder: defaultStringPlaceHolder,
|
||||
isEntityTypeDate: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Dropdown options
|
||||
const { String, Boolean, Binary, DateTime, Double, Guid, Int32, Int64 } = TableConstants.TableType;
|
||||
@@ -133,20 +111,21 @@ export const entityFromAttributes = (entities: EntityRowType[]): Entities.ITable
|
||||
|
||||
// GetPlaceholder according to entity type
|
||||
export const getEntityValuePlaceholder = (entityType: string | number): string => {
|
||||
const { String, Boolean, DateTime, Double, Guid, Int32, Int64 } = TableConstants.TableType;
|
||||
switch (entityType) {
|
||||
case "String":
|
||||
case String:
|
||||
return stringPlaceholder;
|
||||
case "Boolean":
|
||||
case Boolean:
|
||||
return booleanPlaceHolder;
|
||||
case "DateTime":
|
||||
case DateTime:
|
||||
return datePlaceholder;
|
||||
case "Double":
|
||||
case Double:
|
||||
return doublePlaceholder;
|
||||
case "Guid":
|
||||
case Guid:
|
||||
return guidPlaceholder;
|
||||
case "Int32":
|
||||
case Int32:
|
||||
return intPlaceholder;
|
||||
case "Int64":
|
||||
case Int64:
|
||||
return int64Placeholder;
|
||||
default:
|
||||
return "";
|
||||
@@ -164,13 +143,13 @@ export const isValidEntities = (entities: EntityRowType[]): boolean => {
|
||||
};
|
||||
|
||||
const isEntityPropertyTypeDisable = (header: string): boolean => {
|
||||
if (header === "PartitionKey" || header === "RowKey") {
|
||||
if (header === TableConstants.EntityKeyNames.PartitionKey || header === TableConstants.EntityKeyNames.RowKey) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getDefaultEntities = (headers: string[], entityTypes: { [key: string]: string }): EntityRowType[] => {
|
||||
export const getDefaultEntities = (headers: string[], entityTypes: EntityType): EntityRowType[] => {
|
||||
const defaultEntities: EntityRowType[] = [];
|
||||
headers.forEach((header: string) => {
|
||||
if (header !== "Timestamp") {
|
||||
@@ -183,7 +162,7 @@ export const getDefaultEntities = (headers: string[], entityTypes: { [key: strin
|
||||
isDeleteOptionVisible: !isEntityPropertyTypeDisable(header),
|
||||
id: 1,
|
||||
entityValuePlaceholder: defaultStringPlaceHolder,
|
||||
isEntityTypeDate: entityType === "DateTime",
|
||||
isEntityTypeDate: entityType === TableConstants.TableType.DateTime,
|
||||
};
|
||||
defaultEntities.push(entityRow);
|
||||
}
|
||||
@@ -212,6 +191,13 @@ export const getButtonLabel = (apiType: string): string => {
|
||||
return "Add Entity";
|
||||
};
|
||||
|
||||
export const getFormattedTime = (displayValue: string): string => {
|
||||
const date = new Date(displayValue);
|
||||
const minutes = date.getMinutes() < 10 ? `0${date.getMinutes()}` : date.getMinutes();
|
||||
const hours = date.getHours() < 10 ? `0${date.getHours()}` : date.getHours();
|
||||
return `${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
export const getCassandraDefaultEntities = (
|
||||
headers: string[],
|
||||
entityTypes: { [key: string]: string }
|
||||
@@ -244,4 +230,9 @@ export interface EntityRowType {
|
||||
entityValuePlaceholder: string;
|
||||
isEntityTypeDate: boolean;
|
||||
entityTimeValue?: string;
|
||||
isEntityValueDisable?: boolean;
|
||||
}
|
||||
|
||||
export interface EntityType {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -153,40 +153,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
EditTableEntityPane {
|
||||
"addButtonLabel": "Add Property",
|
||||
"attributeNameLabel": "Property Name",
|
||||
"attributeValueLabel": "Value",
|
||||
"canAdd": [Function],
|
||||
"canApply": [Function],
|
||||
"container": [Circular],
|
||||
"dataTypeLabel": "Type",
|
||||
"displayedAttributes": [Function],
|
||||
"editAttribute": [Function],
|
||||
"editButtonLabel": "Edit",
|
||||
"editingProperty": [Function],
|
||||
"edmTypes": [Function],
|
||||
"finishEditingAttribute": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "edittableentitypane",
|
||||
"insertAttribute": [Function],
|
||||
"isEditing": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"onAddPropertyKeyDown": [Function],
|
||||
"onBackButtonKeyDown": [Function],
|
||||
"onDeletePropertyKeyDown": [Function],
|
||||
"onEditPropertyKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"removeAttribute": [Function],
|
||||
"removeButtonLabel": "Remove",
|
||||
"scrollId": [Function],
|
||||
"submitButtonText": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
CassandraAddCollectionPane {
|
||||
"autoPilotUsageCost": [Function],
|
||||
"canConfigureThroughput": [Function],
|
||||
@@ -209,9 +175,7 @@ exports[`Upload Items Pane should render Default properly 1`] = `
|
||||
"keyspaceHasSharedOffer": [Function],
|
||||
"keyspaceId": [Function],
|
||||
"keyspaceIds": [Function],
|
||||
"keyspaceOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"keyspaceOffers": Map {},
|
||||
"keyspaceThroughput": [Function],
|
||||
"maxThroughputRU": [Function],
|
||||
"minThroughputRU": [Function],
|
||||
@@ -234,20 +198,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
|
||||
"userTableQuery": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
StringInputPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "stringinputpane",
|
||||
"inputLabel": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"stringInput": [Function],
|
||||
"submitButtonLabel": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
],
|
||||
"_refreshSparkEnabledStateForAccount": [Function],
|
||||
"_resetNotebookWorkspace": [Function],
|
||||
@@ -402,9 +352,7 @@ exports[`Upload Items Pane should render Default properly 1`] = `
|
||||
"keyspaceHasSharedOffer": [Function],
|
||||
"keyspaceId": [Function],
|
||||
"keyspaceIds": [Function],
|
||||
"keyspaceOffers": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
"keyspaceOffers": Map {},
|
||||
"keyspaceThroughput": [Function],
|
||||
"maxThroughputRU": [Function],
|
||||
"minThroughputRU": [Function],
|
||||
@@ -455,40 +403,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
|
||||
"defaultExperience": [Function],
|
||||
"deleteCollectionText": [Function],
|
||||
"deleteDatabaseText": [Function],
|
||||
"editTableEntityPane": EditTableEntityPane {
|
||||
"addButtonLabel": "Add Property",
|
||||
"attributeNameLabel": "Property Name",
|
||||
"attributeValueLabel": "Value",
|
||||
"canAdd": [Function],
|
||||
"canApply": [Function],
|
||||
"container": [Circular],
|
||||
"dataTypeLabel": "Type",
|
||||
"displayedAttributes": [Function],
|
||||
"editAttribute": [Function],
|
||||
"editButtonLabel": "Edit",
|
||||
"editingProperty": [Function],
|
||||
"edmTypes": [Function],
|
||||
"finishEditingAttribute": [Function],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "edittableentitypane",
|
||||
"insertAttribute": [Function],
|
||||
"isEditing": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"onAddPropertyKeyDown": [Function],
|
||||
"onBackButtonKeyDown": [Function],
|
||||
"onDeletePropertyKeyDown": [Function],
|
||||
"onEditPropertyKeyDown": [Function],
|
||||
"onKeyUp": [Function],
|
||||
"removeAttribute": [Function],
|
||||
"removeButtonLabel": "Remove",
|
||||
"scrollId": [Function],
|
||||
"submitButtonText": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"graphStylingPane": GraphStylingPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
@@ -520,7 +434,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
|
||||
"isMongoIndexingEnabled": [Function],
|
||||
"isNotebookEnabled": [Function],
|
||||
"isNotebooksEnabledForAccount": [Function],
|
||||
"isPreferredApiMongoDB": [Function],
|
||||
"isPublishNotebookPaneEnabled": [Function],
|
||||
"isResourceTokenCollectionNodeSelected": [Function],
|
||||
"isRightPanelV2Enabled": [Function],
|
||||
@@ -552,21 +465,9 @@ exports[`Upload Items Pane should render Default properly 1`] = `
|
||||
"resourceTree": ResourceTreeAdapter {
|
||||
"container": [Circular],
|
||||
"copyNotebook": [Function],
|
||||
"databaseCollectionIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"koSubsCollectionIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"koSubsDatabaseIdMap": ArrayHashMap {
|
||||
"store": HashMap {
|
||||
"container": Object {},
|
||||
},
|
||||
},
|
||||
"databaseCollectionIdMap": Map {},
|
||||
"koSubsCollectionIdMap": Map {},
|
||||
"koSubsDatabaseIdMap": Map {},
|
||||
"parameters": [Function],
|
||||
},
|
||||
"resourceTreeForResourceToken": ResourceTreeAdapterForResourceToken {
|
||||
@@ -592,20 +493,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
|
||||
"onResizeStop": [Function],
|
||||
"splitterId": "h_splitter1",
|
||||
},
|
||||
"stringInputPane": StringInputPane {
|
||||
"container": [Circular],
|
||||
"firstFieldHasFocus": [Function],
|
||||
"formErrors": [Function],
|
||||
"formErrorsDetails": [Function],
|
||||
"id": "stringinputpane",
|
||||
"inputLabel": [Function],
|
||||
"isExecuting": [Function],
|
||||
"isTemplateReady": [Function],
|
||||
"stringInput": [Function],
|
||||
"submitButtonLabel": [Function],
|
||||
"title": [Function],
|
||||
"visible": [Function],
|
||||
},
|
||||
"tabsManager": TabsManager {
|
||||
"activeTab": [Function],
|
||||
"openedTabs": [Function],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -237,7 +237,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
title: "New SQL Query",
|
||||
description: null,
|
||||
});
|
||||
} else if (this.container.isPreferredApiMongoDB()) {
|
||||
} else if (userContext.apiType === "Mongo") {
|
||||
items.push({
|
||||
iconSrc: NewQueryIcon,
|
||||
onClick: () => {
|
||||
|
||||
@@ -58,10 +58,6 @@ export default class TableCommands {
|
||||
var entityToUpdate: Entities.ITableEntity = viewModel.selected()[0];
|
||||
var originalNumberOfProperties = entityToUpdate ? 0 : Object.keys(entityToUpdate).length - 1; // .metadata is always a property for etag
|
||||
|
||||
this._container.editTableEntityPane.originEntity = entityToUpdate;
|
||||
this._container.editTableEntityPane.tableViewModel = viewModel;
|
||||
this._container.editTableEntityPane.originalNumberOfProperties = originalNumberOfProperties;
|
||||
this._container.editTableEntityPane.open();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import DocumentsTab from "./DocumentsTab";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { updateUserContext } from "../../UserContext";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../Explorer";
|
||||
import DocumentId from "../Tree/DocumentId";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import DocumentsTab from "./DocumentsTab";
|
||||
|
||||
describe("Documents tab", () => {
|
||||
describe("buildQuery", () => {
|
||||
@@ -25,7 +26,6 @@ describe("Documents tab", () => {
|
||||
|
||||
describe("showPartitionKey", () => {
|
||||
const explorer = new Explorer();
|
||||
|
||||
const mongoExplorer = new Explorer();
|
||||
mongoExplorer.defaultExperience(Constants.DefaultAccountExperience.MongoDB);
|
||||
|
||||
@@ -124,6 +124,9 @@ describe("Documents tab", () => {
|
||||
});
|
||||
|
||||
it("should be false for Mongo accounts with system partitionKey", () => {
|
||||
updateUserContext({
|
||||
apiType: "Mongo",
|
||||
});
|
||||
const documentsTab = new DocumentsTab({
|
||||
collection: mongoCollectionWithSystemPartitionKey,
|
||||
partitionKey: null,
|
||||
|
||||
@@ -21,6 +21,7 @@ import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||
import * as QueryUtils from "../../Utils/QueryUtils";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
@@ -71,9 +72,7 @@ export default class DocumentsTab extends TabsBase {
|
||||
|
||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||
super(options);
|
||||
this.isPreferredApiMongoDB = !!this.collection
|
||||
? this.collection.container.isPreferredApiMongoDB()
|
||||
: options.isPreferredApiMongoDB;
|
||||
this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB;
|
||||
|
||||
this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id";
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { IGalleryItem, JunoClient } from "../../Juno/JunoClient";
|
||||
import { GalleryAndNotebookViewerComponentProps } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponent";
|
||||
import { GalleryAndNotebookViewerComponentAdapter } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponentAdapter";
|
||||
import { GalleryTab as GalleryViewerTab, SortBy } from "../Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import Explorer from "../Explorer";
|
||||
import React from "react";
|
||||
import type { DatabaseAccount } from "../../Contracts/DataModels";
|
||||
import type { TabOptions } from "../../Contracts/ViewModels";
|
||||
import type { IGalleryItem, JunoClient } from "../../Juno/JunoClient";
|
||||
import { GalleryAndNotebookViewerComponent as GalleryViewer } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponent";
|
||||
import type { GalleryTab as GalleryViewerTab } from "../Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import { SortBy } from "../Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import type Explorer from "../Explorer";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
export interface GalleryTabOptions extends ViewModels.TabOptions {
|
||||
interface Props {
|
||||
account: DatabaseAccount;
|
||||
container: Explorer;
|
||||
junoClient: JunoClient;
|
||||
@@ -17,51 +18,16 @@ export interface GalleryTabOptions extends ViewModels.TabOptions {
|
||||
isFavorite?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notebook gallery tab
|
||||
*/
|
||||
export default class GalleryTab extends TabsBase {
|
||||
public readonly html = '<div style="height: 100%" data-bind="react:galleryAndNotebookViewerComponentAdapter"></div>';
|
||||
private container: Explorer;
|
||||
private galleryAndNotebookViewerComponentProps: GalleryAndNotebookViewerComponentProps;
|
||||
public galleryAndNotebookViewerComponentAdapter: GalleryAndNotebookViewerComponentAdapter;
|
||||
|
||||
constructor(options: GalleryTabOptions) {
|
||||
constructor(options: TabOptions, private props: Props) {
|
||||
super(options);
|
||||
this.container = options.container;
|
||||
|
||||
this.galleryAndNotebookViewerComponentProps = {
|
||||
container: options.container,
|
||||
junoClient: options.junoClient,
|
||||
notebookUrl: options.notebookUrl,
|
||||
galleryItem: options.galleryItem,
|
||||
isFavorite: options.isFavorite,
|
||||
selectedTab: options.selectedTab,
|
||||
sortBy: SortBy.MostRecent,
|
||||
searchText: undefined,
|
||||
};
|
||||
this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter(
|
||||
this.galleryAndNotebookViewerComponentProps
|
||||
);
|
||||
}
|
||||
|
||||
public reset(options: GalleryTabOptions) {
|
||||
this.container = options.container;
|
||||
|
||||
this.galleryAndNotebookViewerComponentProps.container = options.container;
|
||||
this.galleryAndNotebookViewerComponentProps.junoClient = options.junoClient;
|
||||
this.galleryAndNotebookViewerComponentProps.notebookUrl = options.notebookUrl;
|
||||
this.galleryAndNotebookViewerComponentProps.galleryItem = options.galleryItem;
|
||||
this.galleryAndNotebookViewerComponentProps.isFavorite = options.isFavorite;
|
||||
this.galleryAndNotebookViewerComponentProps.selectedTab = options.selectedTab;
|
||||
this.galleryAndNotebookViewerComponentProps.sortBy = SortBy.MostViewed;
|
||||
this.galleryAndNotebookViewerComponentProps.searchText = undefined;
|
||||
|
||||
this.galleryAndNotebookViewerComponentAdapter.reset();
|
||||
this.galleryAndNotebookViewerComponentAdapter.triggerRender();
|
||||
public render() {
|
||||
return <GalleryViewer {...this.props} sortBy={SortBy.MostRecent} searchText={undefined} />;
|
||||
}
|
||||
|
||||
public getContainer(): Explorer {
|
||||
return this.container;
|
||||
return this.props.container;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as ko from "knockout";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { HashMap } from "../../Common/HashMap";
|
||||
import { configContext, Platform } from "../../ConfigContext";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
@@ -17,11 +16,11 @@ export default class MongoShellTab extends TabsBase {
|
||||
public url: ko.Computed<string>;
|
||||
private _container: Explorer;
|
||||
private _runtimeEndpoint: string;
|
||||
private _logTraces: HashMap<number>;
|
||||
private _logTraces: Map<string, number>;
|
||||
|
||||
constructor(options: ViewModels.TabOptions) {
|
||||
super(options);
|
||||
this._logTraces = new HashMap<number>();
|
||||
this._logTraces = new Map();
|
||||
this._container = options.collection.container;
|
||||
this.url = ko.computed<string>(() => {
|
||||
const account = userContext.databaseAccount;
|
||||
|
||||
@@ -5,7 +5,6 @@ import * as Constants from "../../Common/Constants";
|
||||
import { queryDocuments } from "../../Common/dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "../../Common/dataAccess/queryDocumentsPage";
|
||||
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
|
||||
import { HashMap } from "../../Common/HashMap";
|
||||
import * as HeadersUtility from "../../Common/HeadersUtility";
|
||||
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
|
||||
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
|
||||
@@ -47,7 +46,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
public splitter: Splitter;
|
||||
public isPreferredApiMongoDB: boolean;
|
||||
|
||||
public queryMetrics: ko.Observable<HashMap<DataModels.QueryMetrics>>;
|
||||
public queryMetrics: ko.Observable<Map<string, DataModels.QueryMetrics>>;
|
||||
public aggregatedQueryMetrics: ko.Observable<DataModels.QueryMetrics>;
|
||||
public activityId: ko.Observable<string>;
|
||||
public roundTrips: ko.Observable<number>;
|
||||
@@ -91,10 +90,8 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
this.isPreferredApiMongoDB = false;
|
||||
this.aggregatedQueryMetrics = ko.observable<DataModels.QueryMetrics>();
|
||||
this._resetAggregateQueryMetrics();
|
||||
this.queryMetrics = ko.observable<HashMap<DataModels.QueryMetrics>>(new HashMap<DataModels.QueryMetrics>());
|
||||
this.queryMetrics.subscribe((metrics: HashMap<DataModels.QueryMetrics>) =>
|
||||
this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics))
|
||||
);
|
||||
this.queryMetrics = ko.observable<Map<string, DataModels.QueryMetrics>>(new Map());
|
||||
this.queryMetrics.subscribe((metrics) => this.aggregatedQueryMetrics(this._aggregateQueryMetrics(metrics)));
|
||||
this.isQueryMetricsEnabled = ko.computed<boolean>(() => {
|
||||
return userContext.apiType === "SQL" || false;
|
||||
});
|
||||
@@ -364,13 +361,13 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
this.queryMetrics.valueHasMutated();
|
||||
}
|
||||
|
||||
private _aggregateQueryMetrics(metricsMap: HashMap<DataModels.QueryMetrics>): DataModels.QueryMetrics {
|
||||
private _aggregateQueryMetrics(metricsMap: Map<string, DataModels.QueryMetrics>): DataModels.QueryMetrics {
|
||||
if (!metricsMap) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const aggregatedMetrics: DataModels.QueryMetrics = this.aggregatedQueryMetrics();
|
||||
metricsMap.forEach((partitionKeyRangeId: string, queryMetrics: DataModels.QueryMetrics) => {
|
||||
metricsMap.forEach((queryMetrics) => {
|
||||
if (queryMetrics) {
|
||||
aggregatedMetrics.documentLoadTime =
|
||||
queryMetrics.documentLoadTime &&
|
||||
@@ -510,7 +507,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
return null;
|
||||
}
|
||||
|
||||
const queryMetrics: HashMap<DataModels.QueryMetrics> = this.queryMetrics();
|
||||
const queryMetrics = this.queryMetrics();
|
||||
let csvData: string = "";
|
||||
const columnHeaders: string =
|
||||
[
|
||||
@@ -528,7 +525,7 @@ export default class QueryTab extends TabsBase implements ViewModels.WaitsForTem
|
||||
"Document write time (ms)",
|
||||
].join(",") + "\n";
|
||||
csvData = csvData + columnHeaders;
|
||||
queryMetrics.forEach((partitionKeyRangeId: string, queryMetric: DataModels.QueryMetrics) => {
|
||||
queryMetrics.forEach((queryMetric, partitionKeyRangeId) => {
|
||||
const partitionKeyRangeData: string =
|
||||
[
|
||||
partitionKeyRangeId,
|
||||
|
||||
@@ -151,7 +151,7 @@ export default class QueryTablesTab extends TabsBase {
|
||||
};
|
||||
|
||||
public onEditEntityClick = (): Q.Promise<any> => {
|
||||
this.tableCommands.editEntityCommand(this.tableEntityListViewModel());
|
||||
this.container.openEditTableEntityPanel(this, this.tableEntityListViewModel());
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as ko from "knockout";
|
||||
import * as monaco from "monaco-editor";
|
||||
import Q from "q";
|
||||
import DiscardIcon from "../../../images/discard.svg";
|
||||
import SaveIcon from "../../../images/save-cosmos.svg";
|
||||
@@ -8,6 +7,7 @@ import editable from "../../Common/EditableUtility";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import { loadMonaco, monaco } from "../LazyMonaco";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
export default abstract class ScriptTabBase extends TabsBase implements ViewModels.WaitsForTemplate {
|
||||
@@ -299,38 +299,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
|
||||
return !!value;
|
||||
}
|
||||
|
||||
private static _toSeverity(severity: string): monaco.MarkerSeverity {
|
||||
switch (severity.toLowerCase()) {
|
||||
case "error":
|
||||
return monaco.MarkerSeverity.Error;
|
||||
case "warning":
|
||||
return monaco.MarkerSeverity.Warning;
|
||||
case "info":
|
||||
return monaco.MarkerSeverity.Info;
|
||||
case "ignore":
|
||||
default:
|
||||
return monaco.MarkerSeverity.Hint;
|
||||
}
|
||||
}
|
||||
|
||||
private static _toEditorPosition(target: number, lines: string[]): ViewModels.EditorPosition {
|
||||
let cursor: number = 0;
|
||||
let previousCursor: number = 0;
|
||||
let i: number = 0;
|
||||
while (target > cursor + lines[i].length) {
|
||||
cursor += lines[i].length + 2;
|
||||
i++;
|
||||
}
|
||||
|
||||
const editorPosition: ViewModels.EditorPosition = {
|
||||
line: i + 1,
|
||||
column: target - cursor + 1,
|
||||
};
|
||||
|
||||
return editorPosition;
|
||||
}
|
||||
|
||||
protected _createBodyEditor() {
|
||||
protected async _createBodyEditor() {
|
||||
const id = this.editorId;
|
||||
const container = document.getElementById(id);
|
||||
const options = {
|
||||
@@ -341,7 +310,7 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
|
||||
};
|
||||
|
||||
container.innerHTML = "";
|
||||
|
||||
const monaco = await loadMonaco();
|
||||
const editor = monaco.editor.create(container, options);
|
||||
this.editor(editor);
|
||||
|
||||
@@ -353,32 +322,4 @@ export default abstract class ScriptTabBase extends TabsBase implements ViewMode
|
||||
const editorModel = this.editor().getModel();
|
||||
this.editorContent(editorModel.getValue());
|
||||
}
|
||||
|
||||
private _setModelMarkers(errors: ViewModels.QueryError[]) {
|
||||
const markers: monaco.editor.IMarkerData[] = errors.map((e) => this._toMarker(e));
|
||||
const editorModel = this.editor().getModel();
|
||||
monaco.editor.setModelMarkers(editorModel, this.tabId, markers);
|
||||
}
|
||||
|
||||
private _resetModelMarkers() {
|
||||
const queryEditorModel = this.editor().getModel();
|
||||
monaco.editor.setModelMarkers(queryEditorModel, this.tabId, []);
|
||||
}
|
||||
|
||||
private _toMarker(error: ViewModels.QueryError): monaco.editor.IMarkerData {
|
||||
const editorModel = this.editor().getModel();
|
||||
const lines: string[] = editorModel.getLinesContent();
|
||||
const start: ViewModels.EditorPosition = ScriptTabBase._toEditorPosition(Number(error.start), lines);
|
||||
const end: ViewModels.EditorPosition = ScriptTabBase._toEditorPosition(Number(error.end), lines);
|
||||
|
||||
return {
|
||||
severity: ScriptTabBase._toSeverity(error.severity),
|
||||
message: error.message,
|
||||
startLineNumber: start.line,
|
||||
startColumn: start.column,
|
||||
endLineNumber: end.line,
|
||||
endColumn: end.column,
|
||||
code: error.code,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,15 @@ export const Tabs = ({ tabs, activeTab }: { tabs: readonly Tab[]; activeTab: Tab
|
||||
<div id="content" className="flexContainer hideOverflows">
|
||||
<div className="nav-tabs-margin">
|
||||
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
||||
{...tabs.map((tab) => <TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />)}
|
||||
{tabs.map((tab) => (
|
||||
<TabNav key={tab.tabId} tab={tab} active={activeTab === tab} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="tabPanesContainer">
|
||||
{...tabs.map((tab) => <TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />)}
|
||||
{tabs.map((tab) => (
|
||||
<TabPane key={tab.tabId} tab={tab} active={activeTab === tab} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ export class AccessibleVerticalList {
|
||||
this.onSelect = onSelect;
|
||||
}
|
||||
|
||||
public onKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
public onKeyDown = (_src: unknown, event: KeyboardEvent): boolean => {
|
||||
const targetContainer: Element = <Element>event.target;
|
||||
if (this.items == null || this.items.length === 0) {
|
||||
// no items so this should be a noop
|
||||
|
||||
@@ -31,9 +31,6 @@ describe("Collection", () => {
|
||||
|
||||
function generateMockCollectionWithDataModel(data: DataModels.Collection): Collection {
|
||||
const mockContainer = {} as Explorer;
|
||||
mockContainer.isPreferredApiMongoDB = ko.computed(() => {
|
||||
return false;
|
||||
});
|
||||
|
||||
mockContainer.isDatabaseNodeOrNoneSelected = () => {
|
||||
return false;
|
||||
|
||||
@@ -127,16 +127,12 @@ export default class Collection implements ViewModels.Collection {
|
||||
this.partitionKey.paths[0]) ||
|
||||
null;
|
||||
|
||||
if (!!container.isPreferredApiMongoDB() && this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) {
|
||||
if (userContext.apiType === "Mongo" && this.partitionKeyProperty && ~this.partitionKeyProperty.indexOf(`"`)) {
|
||||
this.partitionKeyProperty = this.partitionKeyProperty.replace(/["]+/g, "");
|
||||
}
|
||||
|
||||
// TODO #10738269 : Add this logic in a derived class for Mongo
|
||||
if (
|
||||
!!container.isPreferredApiMongoDB() &&
|
||||
this.partitionKeyProperty &&
|
||||
this.partitionKeyProperty.indexOf("$v") > -1
|
||||
) {
|
||||
if (userContext.apiType === "Mongo" && this.partitionKeyProperty && this.partitionKeyProperty.indexOf("$v") > -1) {
|
||||
// From $v.shard.$v.key.$v > shard.key
|
||||
this.partitionKeyProperty = this.partitionKeyProperty.replace(/.\$v/g, "").replace(/\$v./g, "");
|
||||
this.partitionKeyPropertyHeader = "/" + this.partitionKeyProperty;
|
||||
@@ -1123,7 +1119,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
} else if (userContext.apiType === "Gremlin") {
|
||||
this.onGraphDocumentsClick();
|
||||
return;
|
||||
} else if (this.container.isPreferredApiMongoDB()) {
|
||||
} else if (userContext.apiType === "Mongo") {
|
||||
this.onMongoDBDocumentsClick();
|
||||
return;
|
||||
}
|
||||
@@ -1141,7 +1137,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
return "Rows";
|
||||
} else if (userContext.apiType === "Gremlin") {
|
||||
return "Graph";
|
||||
} else if (this.container.isPreferredApiMongoDB()) {
|
||||
} else if (userContext.apiType === "Mongo") {
|
||||
return "Documents";
|
||||
}
|
||||
|
||||
|
||||
@@ -761,7 +761,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
{
|
||||
label: "Rename",
|
||||
iconSrc: NotebookIcon,
|
||||
onClick: () => this.container.renameNotebook(item).then(() => this.triggerRender()),
|
||||
onClick: () => this.container.renameNotebook(item),
|
||||
},
|
||||
{
|
||||
label: "New Directory",
|
||||
@@ -930,7 +930,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
}
|
||||
|
||||
private cleanupDatabasesKoSubs(): void {
|
||||
this.koSubsDatabaseIdMap.keys().forEach((databaseId: string) => {
|
||||
for (const databaseId of this.koSubsDatabaseIdMap.keys()) {
|
||||
this.koSubsDatabaseIdMap.get(databaseId).forEach((sub: ko.Subscription) => sub.dispose());
|
||||
this.koSubsDatabaseIdMap.delete(databaseId);
|
||||
|
||||
@@ -939,7 +939,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
.get(databaseId)
|
||||
.forEach((collectionId: string) => this.cleanupKoSubsForCollection(databaseId, collectionId));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private cleanupCollectionsKoSubs(databaseId: string, existingCollectionIds: string[]): void {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import postRobot from "post-robot";
|
||||
|
||||
export interface IGitHubConnectorParams {
|
||||
state: string;
|
||||
code: string;
|
||||
@@ -6,25 +8,26 @@ export interface IGitHubConnectorParams {
|
||||
export const GitHubConnectorMsgType = "GitHubConnectorMsgType";
|
||||
|
||||
export class GitHubConnector {
|
||||
public start(params: URLSearchParams, window: Window & typeof globalThis) {
|
||||
window.postMessage(
|
||||
public async start(params: URLSearchParams, window: Window & typeof globalThis): Promise<void> {
|
||||
await postRobot.send(
|
||||
window,
|
||||
GitHubConnectorMsgType,
|
||||
{
|
||||
type: GitHubConnectorMsgType,
|
||||
data: {
|
||||
state: params.get("state"),
|
||||
code: params.get("code"),
|
||||
} as IGitHubConnectorParams,
|
||||
},
|
||||
window.location.origin
|
||||
state: params.get("state"),
|
||||
code: params.get("code"),
|
||||
} as IGitHubConnectorParams,
|
||||
{
|
||||
domain: window.location.origin,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var connector = new GitHubConnector();
|
||||
window.addEventListener("load", () => {
|
||||
window.addEventListener("load", async () => {
|
||||
const openerWindow = window.opener;
|
||||
if (openerWindow) {
|
||||
connector.start(new URLSearchParams(document.location.search), openerWindow);
|
||||
await connector.start(new URLSearchParams(document.location.search), openerWindow);
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import ko from "knockout";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { JunoClient } from "../Juno/JunoClient";
|
||||
import { GitHubConnector, IGitHubConnectorParams } from "./GitHubConnector";
|
||||
import { GitHubOAuthService } from "./GitHubOAuthService";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import NotebookManager from "../Explorer/Notebook/NotebookManager";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { JunoClient } from "../Juno/JunoClient";
|
||||
import { IGitHubConnectorParams } from "./GitHubConnector";
|
||||
import { GitHubOAuthService } from "./GitHubOAuthService";
|
||||
|
||||
const sampleDatabaseAccount: DataModels.DatabaseAccount = {
|
||||
id: "id",
|
||||
@@ -85,25 +85,6 @@ describe("GitHubOAuthService", () => {
|
||||
expect(newParams.get("state")).not.toEqual(initialParams.get("state"));
|
||||
});
|
||||
|
||||
it("finishOAuth is called whenever GitHubConnector is started", async () => {
|
||||
const finishOAuthCallback = jest.fn().mockImplementation();
|
||||
gitHubOAuthService.finishOAuth = finishOAuthCallback;
|
||||
|
||||
const params: IGitHubConnectorParams = {
|
||||
state: "state",
|
||||
code: "code",
|
||||
};
|
||||
const searchParams = new URLSearchParams({ ...params });
|
||||
|
||||
const gitHubConnector = new GitHubConnector();
|
||||
gitHubConnector.start(searchParams, window);
|
||||
|
||||
// GitHubConnector uses Window.postMessage and there's no good way to know when the message has received
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
expect(finishOAuthCallback).toBeCalledWith(params);
|
||||
});
|
||||
|
||||
it("finishOAuth updates token", async () => {
|
||||
const data = { key: "value" };
|
||||
const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.OK, data });
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import ko from "knockout";
|
||||
import postRobot from "post-robot";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { handleError } from "../Common/ErrorHandlingUtils";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
|
||||
import { JunoClient } from "../Juno/JunoClient";
|
||||
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
||||
import { logConsoleInfo } from "../Utils/NotificationConsoleUtils";
|
||||
import { GitHubConnectorMsgType, IGitHubConnectorParams } from "./GitHubConnector";
|
||||
|
||||
window.addEventListener("message", (event: MessageEvent) => {
|
||||
if (isInvalidParentFrameOrigin(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = event.data;
|
||||
if (msg.type === GitHubConnectorMsgType) {
|
||||
const params = msg.data as IGitHubConnectorParams;
|
||||
postRobot.on(
|
||||
GitHubConnectorMsgType,
|
||||
{
|
||||
domain: window.location.origin,
|
||||
},
|
||||
(event) => {
|
||||
// Typescript definition for event is wrong. So read params by casting to <any>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const params = (event as any).data as IGitHubConnectorParams;
|
||||
window.dataExplorer.notebookManager?.gitHubOAuthService.finishOAuth(params);
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
export interface IGitHubOAuthToken {
|
||||
// API properties
|
||||
|
||||
@@ -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-collection-pane", params: { data: addCollectionPane} }' />
|
||||
<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: "string-input-pane", params: { data: stringInputPane} }' />
|
||||
<KOCommentIfStart if="isGitHubPaneEnabled" />
|
||||
<div data-bind='component: { name: "github-repos-pane", params: { data: gitHubReposPane } }' />
|
||||
<KOCommentEnd />
|
||||
<KOCommentIfStart if="isPublishNotebookPaneEnabled" />
|
||||
<div data-bind="react: publishNotebookPaneAdapter" />
|
||||
<KOCommentEnd />
|
||||
{showDialog && <Dialog {...dialogProps} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,11 +2,11 @@ import { IResourceProviderClient } from "../ResourceProvider/IResourceProviderCl
|
||||
import { NotebookWorkspace } from "../Contracts/DataModels";
|
||||
|
||||
export class NotebookWorkspaceSettingsProviderClient implements IResourceProviderClient<string> {
|
||||
public async deleteAsync(url: string, apiVersion: string): Promise<void> {
|
||||
public async deleteAsync(_url: string, _apiVersion: string): Promise<void> {
|
||||
throw new Error("Not yet implemented");
|
||||
}
|
||||
|
||||
public async postAsync(url: string, body: any, apiVersion: string): Promise<any> {
|
||||
public async postAsync(_url: string, _body: any, _apiVersion: string): Promise<any> {
|
||||
return Promise.resolve({
|
||||
notebookServerEndpoint: "http://localhost:8888",
|
||||
username: "",
|
||||
@@ -14,37 +14,37 @@ export class NotebookWorkspaceSettingsProviderClient implements IResourceProvide
|
||||
});
|
||||
}
|
||||
|
||||
public async getAsync(url: string, apiVersion: string): Promise<string> {
|
||||
public async getAsync(): Promise<string> {
|
||||
throw new Error("Not yet implemented");
|
||||
}
|
||||
|
||||
public async putAsync(url: string, body: any, apiVersion: string): Promise<string> {
|
||||
public async putAsync(): Promise<string> {
|
||||
throw new Error("Not yet implemented");
|
||||
}
|
||||
|
||||
public async patchAsync(url: string, apiVersion: string): Promise<string> {
|
||||
public async patchAsync(): Promise<string> {
|
||||
throw new Error("Not yet implemented");
|
||||
}
|
||||
}
|
||||
|
||||
export class NotebookWorkspaceResourceProviderClient implements IResourceProviderClient<NotebookWorkspace> {
|
||||
public async deleteAsync(url: string, apiVersion: string): Promise<void> {
|
||||
public async deleteAsync(): Promise<void> {
|
||||
throw new Error("Not yet implemented");
|
||||
}
|
||||
|
||||
public async postAsync(url: string, body: any, apiVersion: string): Promise<NotebookWorkspace> {
|
||||
public async postAsync(): Promise<NotebookWorkspace> {
|
||||
throw new Error("Not yet implemented");
|
||||
}
|
||||
|
||||
public async getAsync(url: string, apiVersion: string): Promise<NotebookWorkspace | NotebookWorkspace[]> {
|
||||
public async getAsync(): Promise<NotebookWorkspace | NotebookWorkspace[]> {
|
||||
throw new Error("Not yet implemented");
|
||||
}
|
||||
|
||||
public async putAsync(url: string, body: any, apiVersion: string): Promise<NotebookWorkspace> {
|
||||
public async putAsync(): Promise<NotebookWorkspace> {
|
||||
throw new Error("Not yet implemented");
|
||||
}
|
||||
|
||||
public async patchAsync(url: string, body: any, apiVersion: string): Promise<NotebookWorkspace> {
|
||||
public async patchAsync(): Promise<NotebookWorkspace> {
|
||||
throw new Error("Not yet implemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,10 +175,7 @@ export class TabRouteHandler {
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
collection &&
|
||||
collection.container &&
|
||||
collection.container.isPreferredApiMongoDB() &&
|
||||
collection.onMongoDBDocumentsClick();
|
||||
userContext.apiType === "Mongo" && collection.onMongoDBDocumentsClick();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -188,10 +185,7 @@ export class TabRouteHandler {
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
collection &&
|
||||
collection.container &&
|
||||
collection.container.isPreferredApiMongoDB() &&
|
||||
collection.onSchemaAnalyzerClick();
|
||||
collection && userContext.apiType === "Mongo" && collection.onSchemaAnalyzerClick();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -228,10 +222,7 @@ export class TabRouteHandler {
|
||||
if (!!matchingTab) {
|
||||
matchingTab.onTabClick();
|
||||
} else {
|
||||
collection &&
|
||||
collection.container &&
|
||||
collection.container.isPreferredApiMongoDB() &&
|
||||
collection.onNewMongoQueryClick(collection, null);
|
||||
userContext.apiType === "Mongo" && collection.onNewMongoQueryClick(collection, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -250,10 +241,7 @@ export class TabRouteHandler {
|
||||
if (!!matchingTab) {
|
||||
matchingTab.onTabClick();
|
||||
} else {
|
||||
collection &&
|
||||
collection.container &&
|
||||
collection.container.isPreferredApiMongoDB() &&
|
||||
collection.onNewMongoShellClick();
|
||||
userContext.apiType === "Mongo" && collection.onNewMongoShellClick();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { HttpHeaders, TerminalQueryParams } from "../Common/Constants";
|
||||
|
||||
const getUrlVars = (): { [key: string]: string } => {
|
||||
const vars: { [key: string]: string } = {};
|
||||
window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, (m, key, value): string => {
|
||||
window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, (_m, key, value): string => {
|
||||
vars[key] = decodeURIComponent(value);
|
||||
return value;
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ interface UserContext {
|
||||
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
|
||||
// This is coming in a future Cosmos ARM API version as a prperty on databaseAccount
|
||||
readonly apiType?: ApiType;
|
||||
apiType?: ApiType;
|
||||
readonly isTryCosmosDBSubscription?: boolean;
|
||||
readonly portalEnv?: PortalEnv;
|
||||
readonly features: Features;
|
||||
@@ -47,8 +47,10 @@ const userContext: UserContext = {
|
||||
};
|
||||
|
||||
function updateUserContext(newContext: Partial<UserContext>): void {
|
||||
if (newContext.databaseAccount) {
|
||||
newContext.apiType = apiType(newContext.databaseAccount);
|
||||
}
|
||||
Object.assign(userContext, newContext);
|
||||
Object.assign(userContext, { apiType: apiType(userContext.databaseAccount) });
|
||||
}
|
||||
|
||||
function apiType(account: DatabaseAccount | undefined): ApiType {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { decryptJWTToken } from "./AuthorizationUtils";
|
||||
import { userContext } from "../UserContext";
|
||||
import { decryptJWTToken } from "./AuthorizationUtils";
|
||||
|
||||
export function getFullName(): string {
|
||||
const authToken = userContext.authorizationToken;
|
||||
const props = decryptJWTToken(authToken);
|
||||
return props.name;
|
||||
}
|
||||
export const getFullName = (): string => {
|
||||
const { authorizationToken } = userContext;
|
||||
const { name } = decryptJWTToken(authorizationToken);
|
||||
return name;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"noEmit": true,
|
||||
"strictNullChecks": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
},
|
||||
"files": [
|
||||
"./src/AuthType.ts",
|
||||
@@ -14,7 +15,6 @@
|
||||
"./src/Common/DeleteFeedback.ts",
|
||||
"./src/Common/DocumentUtility.ts",
|
||||
"./src/Common/EnvironmentUtility.ts",
|
||||
"./src/Common/HashMap.ts",
|
||||
"./src/Common/HeadersUtility.test.ts",
|
||||
"./src/Common/HeadersUtility.ts",
|
||||
"./src/Common/Logger.ts",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
/* eslint-disable no-undef */
|
||||
require("dotenv/config");
|
||||
const path = require("path");
|
||||
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
|
||||
@@ -83,7 +85,8 @@ const typescriptRule = {
|
||||
exclude: /node_modules/,
|
||||
};
|
||||
|
||||
module.exports = function (env = {}, argv = {}) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
module.exports = function (_env = {}, argv = {}) {
|
||||
const mode = argv.mode || "development";
|
||||
const rules = [fontRule, lessRule, imagesRule, cssRule, htmlRule, typescriptRule];
|
||||
const envVars = {
|
||||
@@ -211,6 +214,7 @@ module.exports = function (env = {}, argv = {}) {
|
||||
util: true,
|
||||
tls: "empty",
|
||||
net: "empty",
|
||||
fs: "empty",
|
||||
},
|
||||
output: {
|
||||
chunkFilename: "[name].[chunkhash:6].js",
|
||||
@@ -264,7 +268,7 @@ module.exports = function (env = {}, argv = {}) {
|
||||
target: "https://main.documentdb.ext.azure.com",
|
||||
changeOrigin: true,
|
||||
logLevel: "debug",
|
||||
bypass: function (req, res, proxyOptions) {
|
||||
bypass: (req, res) => {
|
||||
if (req.method === "OPTIONS") {
|
||||
res.statusCode = 200;
|
||||
res.send();
|
||||
@@ -304,8 +308,5 @@ module.exports = function (env = {}, argv = {}) {
|
||||
},
|
||||
},
|
||||
stats: "minimal",
|
||||
node: {
|
||||
fs: "empty",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user