Merge branch 'master'

This commit is contained in:
sunilyadav840
2021-05-14 13:25:03 +05:30
85 changed files with 1545 additions and 2525 deletions

View File

@@ -84,8 +84,8 @@ src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts
src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts # src/Explorer/Graph/GraphStyleComponent/GraphStyle.test.ts
src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts # src/Explorer/Graph/GraphStyleComponent/GraphStyleComponent.ts
src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.test.ts
src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts
@@ -115,7 +115,8 @@ src/Explorer/Panes/CassandraAddCollectionPane.ts
src/Explorer/Panes/ContextualPaneBase.ts src/Explorer/Panes/ContextualPaneBase.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts
src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts
src/Explorer/Panes/GraphStylingPane.ts # src/Explorer/Panes/GraphStylingPane.ts
# src/Explorer/Panes/NewVertexPane.ts
src/Explorer/Panes/PaneComponents.ts src/Explorer/Panes/PaneComponents.ts
src/Explorer/Panes/RenewAdHocAccessPane.ts src/Explorer/Panes/RenewAdHocAccessPane.ts
src/Explorer/Panes/SetupNotebooksPane.ts src/Explorer/Panes/SetupNotebooksPane.ts

View File

@@ -141,6 +141,7 @@ jobs:
- ./test/graph/container.spec.ts - ./test/graph/container.spec.ts
- ./test/sql/container.spec.ts - ./test/sql/container.spec.ts
- ./test/mongo/container.spec.ts - ./test/mongo/container.spec.ts
- ./test/mongo/container32.spec.ts
- ./test/selfServe/selfServeExample.spec.ts - ./test/selfServe/selfServeExample.spec.ts
- ./test/notebooks/upload.spec.ts - ./test/notebooks/upload.spec.ts
- ./test/sql/resourceToken.spec.ts - ./test/sql/resourceToken.spec.ts

View File

@@ -2099,7 +2099,7 @@ a:link {
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
overflow-x: auto; overflow-x: auto;
overflow-y: auto; overflow-y: hidden;
height: 100%; height: 100%;
} }
@@ -3085,3 +3085,7 @@ settings-pane {
padding-left: @SmallSpace; padding-left: @SmallSpace;
} }
} }
.hiddenMain {
visibility: hidden;
height: 0px;
}

View File

@@ -3,6 +3,7 @@
.resourceTree { .resourceTree {
height: 100%; height: 100%;
width: 20%;
flex: 0 0 auto; flex: 0 0 auto;
.main { .main {
height: 100%; height: 100%;

View File

@@ -1,10 +1,15 @@
.schema-analyzer-cell-outputs { .schema-analyzer-cell-outputs {
padding: 10px; padding: 10px 2px;
} }
// Mimic FluentUI8's DocumentCard style
.schema-analyzer-cell-output { .schema-analyzer-cell-output {
margin-bottom: 20px; margin-bottom: 20px;
padding: 10px; padding: 14px 20px;
border-radius: 2px; border: 1px solid rgb(237, 235, 233);
box-shadow: rgba(0, 0, 0, 13%) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 11%) 0px 0.3px 0.9px 0px; }
.schema-analyzer-cell-output:hover {
border-color: rgb(200, 198, 196);
box-shadow: inset 0 0 0 1px rgb(200, 198, 196)
} }

View File

@@ -0,0 +1,41 @@
import React, { FunctionComponent } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
export interface CollapsedResourceTreeProps {
toggleLeftPaneExpanded: () => void;
isLeftPaneExpanded: boolean;
}
export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps> = ({
toggleLeftPaneExpanded,
isLeftPaneExpanded,
}: CollapsedResourceTreeProps): JSX.Element => {
return (
<div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}>
<div className="main-nav nav">
<ul className="nav">
<li className="resourceTreeCollapse" id="collapseToggleLeftPaneButton" aria-label="Expand Tree">
<span
className="leftarrowCollapsed"
onClick={toggleLeftPaneExpanded}
role="button"
tabIndex={0}
onKeyDown={toggleLeftPaneExpanded}
>
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
</span>
<span
className="collectionCollapsed"
onClick={toggleLeftPaneExpanded}
role="button"
tabIndex={0}
onKeyDown={toggleLeftPaneExpanded}
>
<span data-bind="text: collectionTitle" />
</span>
</li>
</ul>
</div>
</div>
);
};

View File

@@ -94,6 +94,7 @@ export class Flights {
public static readonly MongoIndexEditor = "mongoindexeditor"; public static readonly MongoIndexEditor = "mongoindexeditor";
public static readonly MongoIndexing = "mongoindexing"; public static readonly MongoIndexing = "mongoindexing";
public static readonly AutoscaleTest = "autoscaletest"; public static readonly AutoscaleTest = "autoscaletest";
public static readonly SchemaAnalyzer = "schemaanalyzer";
} }
export class AfecFeatures { export class AfecFeatures {

View File

@@ -0,0 +1,60 @@
import React, { FunctionComponent } from "react";
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
import refreshImg from "../../images/refresh-cosmos.svg";
import { AuthType } from "../AuthType";
import { userContext } from "../UserContext";
export interface ResourceTreeProps {
toggleLeftPaneExpanded: () => void;
isLeftPaneExpanded: boolean;
}
export const ResourceTree: FunctionComponent<ResourceTreeProps> = ({
toggleLeftPaneExpanded,
isLeftPaneExpanded,
}: ResourceTreeProps): JSX.Element => {
return (
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
{/* Collections Window - - Start */}
<div id="mainslide" className="flexContainer">
{/* Collections Window Title/Command Bar - Start */}
<div className="collectiontitle">
<div className="coltitle">
<span className="titlepadcol" data-bind="text: collectionTitle" />
<div className="float-right">
<span
className="padimgcolrefresh"
data-test="refreshTree"
role="button"
data-bind="click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }"
tabIndex={0}
aria-label="Refresh tree"
title="Refresh tree"
>
<img className="refreshcol" src={refreshImg} alt="Refresh tree" />
</span>
<span
className="padimgcolrefresh1"
id="expandToggleLeftPaneButton"
role="button"
onClick={toggleLeftPaneExpanded}
tabIndex={0}
aria-label="Collapse Tree"
title="Collapse Tree"
onKeyDown={toggleLeftPaneExpanded}
>
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
</span>
</div>
</div>
</div>
{userContext.authType === AuthType.ResourceToken ? (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTreeForResourceToken" />
) : (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
)}
</div>
{/* Collections Window - End */}
</div>
);
};

View File

@@ -5,7 +5,7 @@ export interface TooltipProps {
children: string; children: string;
} }
export const InfoTooltip: React.FunctionComponent = ({ children }: TooltipProps) => { export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }: TooltipProps) => {
return ( return (
<span> <span>
<TooltipHost content={children}> <TooltipHost content={children}>

View File

@@ -206,17 +206,14 @@ export enum NeighborType {
BOTH, BOTH,
} }
/** export interface IGraphConfigUiData {
* Set of observable related to graph configuration by user showNeighborType: NeighborType;
*/ nodeProperties: string[];
export interface GraphConfigUiData { nodePropertiesWithNone: string[];
showNeighborType: ko.Observable<NeighborType>; nodeCaptionChoice: string;
nodeProperties: ko.ObservableArray<string>; nodeColorKeyChoice: string;
nodePropertiesWithNone: ko.ObservableArray<string>; nodeIconChoice: string;
nodeCaptionChoice: ko.Observable<string>; nodeIconSet: string;
nodeColorKeyChoice: ko.Observable<string>;
nodeIconChoice: ko.Observable<string>;
nodeIconSet: ko.Observable<string>;
} }
/** /**

View File

@@ -4,26 +4,10 @@ import * as ko from "knockout";
import "./ComponentRegisterer"; import "./ComponentRegisterer";
describe("Component Registerer", () => { describe("Component Registerer", () => {
it("should register input-typeahead component", () => {
expect(ko.components.isRegistered("input-typeahead")).toBe(true);
});
it("should register error-display component", () => {
expect(ko.components.isRegistered("error-display")).toBe(true);
});
it("should register graph-style component", () => {
expect(ko.components.isRegistered("graph-style")).toBe(true);
});
it("should register json-editor component", () => { it("should register json-editor component", () => {
expect(ko.components.isRegistered("json-editor")).toBe(true); expect(ko.components.isRegistered("json-editor")).toBe(true);
}); });
it("should register graph-styling-pane component", () => {
expect(ko.components.isRegistered("graph-styling-pane")).toBe(true);
});
it("should register dynamic-list component", () => { it("should register dynamic-list component", () => {
expect(ko.components.isRegistered("dynamic-list")).toBe(true); expect(ko.components.isRegistered("dynamic-list")).toBe(true);
}); });

View File

@@ -2,16 +2,10 @@ import * as ko from "knockout";
import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent"; import { DiffEditorComponent } from "./Controls/DiffEditor/DiffEditorComponent";
import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent"; import { DynamicListComponent } from "./Controls/DynamicList/DynamicListComponent";
import { EditorComponent } from "./Controls/Editor/EditorComponent"; import { EditorComponent } from "./Controls/Editor/EditorComponent";
import { ErrorDisplayComponent } from "./Controls/ErrorDisplayComponent/ErrorDisplayComponent";
import { InputTypeaheadComponent } from "./Controls/InputTypeahead/InputTypeahead";
import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent"; import { JsonEditorComponent } from "./Controls/JsonEditor/JsonEditorComponent";
import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3"; import { ThroughputInputComponentAutoPilotV3 } from "./Controls/ThroughputInput/ThroughputInputComponentAutoPilotV3";
import { GraphStyleComponent } from "./Graph/GraphStyleComponent/GraphStyleComponent";
import * as PaneComponents from "./Panes/PaneComponents"; import * as PaneComponents from "./Panes/PaneComponents";
ko.components.register("input-typeahead", new InputTypeaheadComponent());
ko.components.register("error-display", new ErrorDisplayComponent());
ko.components.register("graph-style", GraphStyleComponent);
ko.components.register("editor", new EditorComponent()); ko.components.register("editor", new EditorComponent());
ko.components.register("json-editor", new JsonEditorComponent()); ko.components.register("json-editor", new JsonEditorComponent());
ko.components.register("diff-editor", new DiffEditorComponent()); ko.components.register("diff-editor", new DiffEditorComponent());
@@ -21,5 +15,4 @@ ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponent
// Panes // Panes
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent()); ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent()); ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());

View File

@@ -103,7 +103,7 @@ export const Dialog: FunctionComponent<DialogProps> = ({
text: secondaryButtonText, text: secondaryButtonText,
onClick: onSecondaryButtonClick, onClick: onSecondaryButtonClick,
} }
: undefined; : {};
return ( return (
<FluentDialog {...dialogProps}> <FluentDialog {...dialogProps}>

View File

@@ -1,27 +0,0 @@
import template from "./error-display-component.html";
/**
* Helper class for ko component registration
* This component displays an error as designed in:
* https://microsoft.sharepoint.com/teams/DPX/Modern/DocDB/_layouts/15/WopiFrame.aspx?sourcedoc={66864d4a-f925-4cbe-9eb4-79f8d191a115}&action=edit&wd=target%28DocumentDB%20emulator%2Eone%7CE617D0A7-F77C-4968-B75A-1451049F4FEA%2FError%20notification%7CAA1E4BC9-4D72-472C-B40C-2437FA217226%2F%29
* TODO: support "More details"
*/
export class ErrorDisplayComponent {
constructor() {
return {
viewModel: ErrorDisplayViewModel,
template,
};
}
}
/**
* Parameters for this component
*/
interface ErrorDisplayParams {
errorMsg: ko.Observable<string>; // Primary message
}
class ErrorDisplayViewModel {
public constructor(public params: ErrorDisplayParams) {}
}

View File

@@ -1,6 +0,0 @@
<div class="warningErrorContainer" data-bind="visible: !!params.errorMsg()">
<div class="warningErrorContent">
<span><img src="/error_red.svg" alt="Error" /></span>
<span class="settingErrorMsg warningErrorDetailsLinkContainer" data-bind="text: params.errorMsg()"></span>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { CommandButton, FontIcon, FontWeights, ITextProps, Separator, Stack, Text } from "@fluentui/react";
import * as React from "react"; import * as React from "react";
import { Stack, Text, Separator, FontIcon, CommandButton, FontWeights, ITextProps } from "@fluentui/react";
export class GalleryHeaderComponent extends React.Component { export class GalleryHeaderComponent extends React.Component {
private static readonly azureText = "Microsoft Azure"; private static readonly azureText = "Microsoft Azure";
@@ -61,7 +61,7 @@ export class GalleryHeaderComponent extends React.Component {
<Stack.Item> <Stack.Item>
{this.renderHeaderItem( {this.renderHeaderItem(
GalleryHeaderComponent.galleryText, GalleryHeaderComponent.galleryText,
undefined, () => "",
GalleryHeaderComponent.headerItemTextProps GalleryHeaderComponent.headerItemTextProps
)} )}
</Stack.Item> </Stack.Item>

View File

@@ -1,186 +0,0 @@
/**
* How to use this component:
*
* In your html markup, use:
* <input-typeahead params="{
choices:choices,
selection:selection,
inputValue:inputValue,
placeholder:'Enter source',
typeaheadOverrideOptions:typeaheadOverrideOptions
}"></input-typeahead>
* The parameters are documented below.
*
* Notes:
* - dynamic:true by default, this allows choices to change after initialization.
* To turn it off, use:
* typeaheadOverrideOptions: { dynamic:false }
*
*/
import "jquery-typeahead";
import template from "./input-typeahead.html";
/**
* Helper class for ko component registration
*/
export class InputTypeaheadComponent {
constructor() {
return {
viewModel: InputTypeaheadViewModel,
template,
};
}
}
export interface Item {
caption: string;
value: any;
}
/**
* Parameters for this component
*/
interface InputTypeaheadParams {
/**
* List of choices available in the dropdown.
*/
choices: ko.ObservableArray<Item>;
/**
* Gets updated when user clicks on the choice in the dropdown
*/
selection?: ko.Observable<Item>;
/**
* The current string value of <input>
*/
inputValue?: ko.Observable<string>;
/**
* Define what text you want as the input placeholder
*/
placeholder: string;
/**
* Override default jquery-typeahead options
* WARNING: do not override input, source or callback to avoid breaking the components behavior.
*/
typeaheadOverrideOptions?: any;
/**
* This function gets called when pressing ENTER on the input box
*/
submitFct?: (inputValue: string | null, selection: Item | null) => void;
/**
* Typehead comes with a Search button that we normally remove.
* If you want to use it, turn this on
*/
showSearchButton?: boolean;
}
interface OnClickItem {
matchedKey: string;
value: any;
caption: string;
group: string;
}
interface Cache {
inputValue: string | null;
selection: Item | null;
}
class InputTypeaheadViewModel {
private static instanceCount = 0; // Generate unique id for each component's typeahead instance
private instanceNumber: number;
private params: InputTypeaheadParams;
private cache: Cache;
public constructor(params: InputTypeaheadParams) {
this.instanceNumber = InputTypeaheadViewModel.instanceCount++;
this.params = params;
this.params.choices.subscribe(this.initializeTypeahead.bind(this));
this.cache = {
inputValue: null,
selection: null,
};
}
/**
* Must execute once ko is rendered, so that it can find the input element by id
*/
private initializeTypeahead() {
let params = this.params;
let cache = this.cache;
let options: any = {
input: `#${this.getComponentId()}`, //'.input-typeahead',
order: "asc",
minLength: 0,
searchOnFocus: true,
source: {
display: "caption",
data: () => {
return this.params.choices();
},
},
callback: {
onClick: (_node: unknown, _a: unknown, item: OnClickItem) => {
cache.selection = item;
if (params.selection) {
params.selection(item);
}
},
onResult(_node: unknown, query: any) {
cache.inputValue = query;
if (params.inputValue) {
params.inputValue(query);
}
},
},
template: (_query: string, item: any) => {
// Don't display id if caption *IS* the id
return item.caption === item.value
? "<span>{{caption}}</span>"
: "<span><div>{{caption}}</div><div><small>{{value}}</small></div></span>";
},
dynamic: true,
};
// Override options
if (params.typeaheadOverrideOptions) {
for (let p in params.typeaheadOverrideOptions) {
options[p] = params.typeaheadOverrideOptions[p];
}
}
($ as any).typeahead(options);
}
/**
* Get this component id
* @return unique id per instance
*/
private getComponentId(): string {
return `input-typeahead${this.instanceNumber}`;
}
/**
* Executed once ko is done rendering bindings
* Use ko's "template: afterRender" callback to do that without actually using any template.
* Another way is to call it within setTimeout() in constructor.
*/
public afterRender(): void {
this.initializeTypeahead();
}
public submit(): void {
if (this.params.submitFct) {
this.params.submitFct(this.cache.inputValue, this.cache.selection);
}
}
}

View File

@@ -1,19 +0,0 @@
<span class="input-typeahead-container">
<form class="input-typehead" data-bind="submit:submit">
<div class="typeahead__container">
<div class="typeahead__field">
<span class="typeahead__query">
<input
name="q"
type="search"
autocomplete="off"
data-bind="attr: { placeholder: params.placeholder, id:getComponentId() }, value:params.inputValue, template: { afterRender:afterRender() }"
/>
</span>
<span class="typeahead__button" data-bind="visible:params.showSearchButton">
<button type="submit"><span class="typeahead__search-icon"></span></button>
</span>
</div>
</div>
</form>
</span>

View File

@@ -32,115 +32,6 @@ exports[`SettingsComponent renders 1`] = `
"_closeSynapseLinkModalDialog": [Function], "_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function], "_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function], "_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"addCollectionText": [Function], "addCollectionText": [Function],
@@ -1026,32 +917,11 @@ exports[`SettingsComponent renders 1`] = `
}, },
}, },
}, },
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
@@ -1118,7 +988,6 @@ exports[`SettingsComponent renders 1`] = `
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],
}, },
"toggleLeftPaneExpandedKeyPress": [Function],
}, },
"databaseId": "test", "databaseId": "test",
"defaultTtl": [Function], "defaultTtl": [Function],
@@ -1144,115 +1013,6 @@ exports[`SettingsComponent renders 1`] = `
"_closeSynapseLinkModalDialog": [Function], "_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function], "_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function], "_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"addCollectionText": [Function], "addCollectionText": [Function],
@@ -2138,32 +1898,11 @@ exports[`SettingsComponent renders 1`] = `
}, },
}, },
}, },
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
@@ -2230,7 +1969,6 @@ exports[`SettingsComponent renders 1`] = `
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],
}, },
"toggleLeftPaneExpandedKeyPress": [Function],
} }
} }
isAutoPilotSelected={false} isAutoPilotSelected={false}
@@ -2269,115 +2007,6 @@ exports[`SettingsComponent renders 1`] = `
"_closeSynapseLinkModalDialog": [Function], "_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function], "_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function], "_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"addCollectionText": [Function], "addCollectionText": [Function],
@@ -3263,32 +2892,11 @@ exports[`SettingsComponent renders 1`] = `
}, },
}, },
}, },
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
@@ -3355,7 +2963,6 @@ exports[`SettingsComponent renders 1`] = `
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],
}, },
"toggleLeftPaneExpandedKeyPress": [Function],
}, },
"databaseId": "test", "databaseId": "test",
"defaultTtl": [Function], "defaultTtl": [Function],
@@ -3381,115 +2988,6 @@ exports[`SettingsComponent renders 1`] = `
"_closeSynapseLinkModalDialog": [Function], "_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function], "_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function], "_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"addCollectionText": [Function], "addCollectionText": [Function],
@@ -4375,32 +3873,11 @@ exports[`SettingsComponent renders 1`] = `
}, },
}, },
}, },
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
@@ -4467,7 +3944,6 @@ exports[`SettingsComponent renders 1`] = `
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],
}, },
"toggleLeftPaneExpandedKeyPress": [Function],
} }
} }
geospatialConfigType="Geometry" geospatialConfigType="Geometry"

View File

@@ -20,17 +20,13 @@ describe("ThroughputInput Pane", () => {
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it("test Autoscale Mode select", () => { it("should switch mode properly", () => {
wrapper.setProps({ isAutoscaleSelected: true }); wrapper.find('[aria-label="Manual mode"]').simulate("change");
expect(wrapper.find('[aria-label="ruDescription"]').at(0).text()).toBe( expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe(
"Estimate your required RU/s with capacity calculator." "Container throughput (400 - unlimited RU/s)"
); );
expect(wrapper.find('[aria-label="maxRUDescription"]').at(0).text()).toContain("Max RU/s");
});
it("test Manual Mode select", () => { wrapper.find('[aria-label="Autoscale mode"]').simulate("change");
wrapper.setProps({ isAutoscaleSelected: false }); expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe("Container throughput (autoscale)");
expect(wrapper.find('[aria-label="ruDescription"]').at(0).text()).toContain("Estimate your required RU/s with");
expect(wrapper.find('[aria-label="capacityLink"]').at(0).text()).toContain("capacity calculator");
}); });
}); });

View File

@@ -17,8 +17,6 @@ export interface ThroughputInputProps {
setThroughputValue: (throughput: number) => void; setThroughputValue: (throughput: number) => void;
setIsAutoscale: (isAutoscale: boolean) => void; setIsAutoscale: (isAutoscale: boolean) => void;
onCostAcknowledgeChange: (isAcknowledged: boolean) => void; onCostAcknowledgeChange: (isAcknowledged: boolean) => void;
isAutoscaleSelected?: boolean;
throughput?: number;
} }
export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
@@ -27,12 +25,16 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
setThroughputValue, setThroughputValue,
setIsAutoscale, setIsAutoscale,
isSharded, isSharded,
isAutoscaleSelected = true,
throughput = AutoPilotUtils.minAutoPilotThroughput,
onCostAcknowledgeChange, onCostAcknowledgeChange,
}: ThroughputInputProps) => { }: ThroughputInputProps) => {
const [isAutoscaleSelected, setIsAutoScaleSelected] = useState<boolean>(true);
const [throughput, setThroughput] = useState<number>(AutoPilotUtils.minAutoPilotThroughput);
const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false); const [isCostAcknowledged, setIsCostAcknowledged] = useState<boolean>(false);
const [throughputError, setThroughputError] = useState<string>(""); const [throughputError, setThroughputError] = useState<string>("");
setIsAutoscale(isAutoscaleSelected);
setThroughputValue(throughput);
const getThroughputLabelText = (): string => { const getThroughputLabelText = (): string => {
let throughputHeaderText: string; let throughputHeaderText: string;
if (isAutoscaleSelected) { if (isAutoscaleSelected) {
@@ -49,6 +51,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const onThroughputValueChange = (newInput: string): void => { const onThroughputValueChange = (newInput: string): void => {
const newThroughput = parseInt(newInput); const newThroughput = parseInt(newInput);
setThroughput(newThroughput);
setThroughputValue(newThroughput); setThroughputValue(newThroughput);
if (!isSharded && newThroughput > 10000) { if (!isSharded && newThroughput > 10000) {
setThroughputError("Unsharded collections support up to 10,000 RUs"); setThroughputError("Unsharded collections support up to 10,000 RUs");
@@ -82,9 +85,13 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => { const handleOnChangeMode = (event: React.ChangeEvent<HTMLInputElement>, mode: string): void => {
if (mode === "Autoscale") { if (mode === "Autoscale") {
setThroughput(AutoPilotUtils.minAutoPilotThroughput);
setIsAutoScaleSelected(true);
setThroughputValue(AutoPilotUtils.minAutoPilotThroughput); setThroughputValue(AutoPilotUtils.minAutoPilotThroughput);
setIsAutoscale(true); setIsAutoscale(true);
} else { } else {
setThroughput(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoScaleSelected(false);
setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400); setThroughputValue(SharedConstants.CollectionCreation.DefaultCollectionRUs400);
setIsAutoscale(false); setIsAutoscale(false);
} }
@@ -94,7 +101,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
<div className="throughputInputContainer throughputInputSpacing"> <div className="throughputInputContainer throughputInputSpacing">
<Stack horizontal> <Stack horizontal>
<span className="mandatoryStar">*&nbsp;</span> <span className="mandatoryStar">*&nbsp;</span>
<Text variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}> <Text aria-label="Throughput header" variant="small" style={{ lineHeight: "20px", fontWeight: 600 }}>
{getThroughputLabelText()} {getThroughputLabelText()}
</Text> </Text>
<InfoTooltip>{PricingUtils.getRuToolTipText()}</InfoTooltip> <InfoTooltip>{PricingUtils.getRuToolTipText()}</InfoTooltip>

View File

@@ -25,6 +25,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
*  * 
</span> </span>
<Text <Text
aria-label="Throughput header"
key=".0:$.1" key=".0:$.1"
style={ style={
Object { Object {
@@ -35,6 +36,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
variant="small" variant="small"
> >
<span <span
aria-label="Throughput header"
className="css-54" className="css-54"
style={ style={
Object { Object {

View File

@@ -34,6 +34,7 @@ import { updateUserContext, userContext } from "../UserContext";
import { getCollectionName, getDatabaseName, getUploadName } from "../Utils/APITypeUtils"; import { getCollectionName, getDatabaseName, getUploadName } from "../Utils/APITypeUtils";
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils"; import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { stringToBlob } from "../Utils/BlobUtils"; import { stringToBlob } from "../Utils/BlobUtils";
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils"; import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils"; import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
@@ -60,7 +61,6 @@ import { DeleteCollectionConfirmationPane } from "./Panes/DeleteCollectionConfir
import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel"; import { DeleteDatabaseConfirmationPanel } from "./Panes/DeleteDatabaseConfirmationPanel";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane"; import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
import { GitHubReposPanel } from "./Panes/GitHubReposPanel/GitHubReposPanel"; import { GitHubReposPanel } from "./Panes/GitHubReposPanel/GitHubReposPanel";
import GraphStylingPane from "./Panes/GraphStylingPane";
import { LoadQueryPane } from "./Panes/LoadQueryPane/LoadQueryPane"; import { LoadQueryPane } from "./Panes/LoadQueryPane/LoadQueryPane";
import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane"; import { SaveQueryPane } from "./Panes/SaveQueryPane/SaveQueryPane";
import { SettingsPane } from "./Panes/SettingsPane/SettingsPane"; import { SettingsPane } from "./Panes/SettingsPane/SettingsPane";
@@ -133,7 +133,6 @@ export default class Explorer {
public databases: ko.ObservableArray<ViewModels.Database>; public databases: ko.ObservableArray<ViewModels.Database>;
public selectedDatabaseId: ko.Computed<string>; public selectedDatabaseId: ko.Computed<string>;
public selectedCollectionId: ko.Computed<string>; public selectedCollectionId: ko.Computed<string>;
public isLeftPaneExpanded: ko.Observable<boolean>;
public selectedNode: ko.Observable<ViewModels.TreeNode>; public selectedNode: ko.Observable<ViewModels.TreeNode>;
private resourceTree: ResourceTreeAdapter; private resourceTree: ResourceTreeAdapter;
@@ -151,7 +150,6 @@ export default class Explorer {
// Contextual panes // Contextual panes
public addDatabasePane: AddDatabasePane; public addDatabasePane: AddDatabasePane;
public graphStylingPane: GraphStylingPane;
public cassandraAddCollectionPane: CassandraAddCollectionPane; public cassandraAddCollectionPane: CassandraAddCollectionPane;
private gitHubClient: GitHubClient; private gitHubClient: GitHubClient;
public gitHubOAuthService: GitHubOAuthService; public gitHubOAuthService: GitHubOAuthService;
@@ -163,7 +161,6 @@ export default class Explorer {
public isMongoIndexingEnabled: ko.Observable<boolean>; public isMongoIndexingEnabled: ko.Observable<boolean>;
public canExceedMaximumValue: ko.Computed<boolean>; public canExceedMaximumValue: ko.Computed<boolean>;
public isAutoscaleDefaultEnabled: ko.Observable<boolean>; public isAutoscaleDefaultEnabled: ko.Observable<boolean>;
public isSchemaEnabled: ko.Computed<boolean>; public isSchemaEnabled: ko.Computed<boolean>;
// Notebooks // Notebooks
@@ -183,7 +180,6 @@ export default class Explorer {
public openDialog: ExplorerParams["openDialog"]; public openDialog: ExplorerParams["openDialog"];
public closeDialog: ExplorerParams["closeDialog"]; public closeDialog: ExplorerParams["closeDialog"];
private _panes: ContextualPaneBase[] = [];
private _isInitializingNotebooks: boolean; private _isInitializingNotebooks: boolean;
private notebookBasePath: ko.Observable<string>; private notebookBasePath: ko.Observable<string>;
private _arcadiaManager: ArcadiaResourceManager; private _arcadiaManager: ArcadiaResourceManager;
@@ -230,6 +226,7 @@ export default class Explorer {
}); });
} }
}); });
this.isNotebooksEnabledForAccount = ko.observable(false); this.isNotebooksEnabledForAccount = ko.observable(false);
this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons()); this.isNotebooksEnabledForAccount.subscribe((isEnabledForAccount: boolean) => this.refreshCommandBarButtons());
this.isSparkEnabledForAccount = ko.observable(false); this.isSparkEnabledForAccount = ko.observable(false);
@@ -334,7 +331,6 @@ export default class Explorer {
} }
return true; return true;
}); });
this.isLeftPaneExpanded = ko.observable<boolean>(true);
this.selectedNode = ko.observable<ViewModels.TreeNode>(); this.selectedNode = ko.observable<ViewModels.TreeNode>();
this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => { this.selectedNode.subscribe((nodeSelected: ViewModels.TreeNode) => {
// Make sure switching tabs restores tabs display // Make sure switching tabs restores tabs display
@@ -368,7 +364,7 @@ export default class Explorer {
return false; return false;
} }
return userContext.apiType === "Mongo"; return isCapabilityEnabled("EnableMongo");
}); });
this.isServerlessEnabled = ko.computed( this.isServerlessEnabled = ko.computed(
@@ -412,13 +408,6 @@ export default class Explorer {
container: this, container: this,
}); });
this.graphStylingPane = new GraphStylingPane({
id: "graphstylingpane",
visible: ko.observable<boolean>(false),
container: this,
});
this.cassandraAddCollectionPane = new CassandraAddCollectionPane({ this.cassandraAddCollectionPane = new CassandraAddCollectionPane({
id: "cassandraaddcollectionpane", id: "cassandraaddcollectionpane",
visible: ko.observable<boolean>(false), visible: ko.observable<boolean>(false),
@@ -434,7 +423,6 @@ export default class Explorer {
} }
}); });
this._panes = [this.addDatabasePane, this.graphStylingPane, this.cassandraAddCollectionPane];
this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText)); this.addDatabaseText.subscribe((addDatabaseText: string) => this.addDatabasePane.title(addDatabaseText));
this.isTabsContentExpanded = ko.observable(false); this.isTabsContentExpanded = ko.observable(false);
@@ -674,16 +662,8 @@ export default class Explorer {
this.setIsNotificationConsoleExpanded(true); this.setIsNotificationConsoleExpanded(true);
} }
public toggleLeftPaneExpanded() { public collapseConsole(): void {
this.isLeftPaneExpanded(!this.isLeftPaneExpanded()); this.setIsNotificationConsoleExpanded(false);
if (this.isLeftPaneExpanded()) {
document.getElementById("expandToggleLeftPaneButton").focus();
this.splitter.expandLeft();
} else {
document.getElementById("collapseToggleLeftPaneButton").focus();
this.splitter.collapseLeft();
}
} }
public refreshDatabaseForResourceToken(): Q.Promise<any> { public refreshDatabaseForResourceToken(): Q.Promise<any> {
@@ -802,14 +782,6 @@ export default class Explorer {
this.refreshNotebookList(); this.refreshNotebookList();
}; };
public toggleLeftPaneExpandedKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.toggleLeftPaneExpanded();
return false;
}
return true;
};
// Facade // Facade
public provideFeedbackEmail = () => { public provideFeedbackEmail = () => {
window.open(Constants.Urls.feedbackEmail, "_blank"); window.open(Constants.Urls.feedbackEmail, "_blank");
@@ -1064,6 +1036,9 @@ export default class Explorer {
if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) { if (flights.indexOf(Constants.Flights.MongoIndexing) !== -1) {
this.isMongoIndexingEnabled(true); this.isMongoIndexingEnabled(true);
} }
if (flights.indexOf(Constants.Flights.SchemaAnalyzer) !== -1) {
userContext.features.enableSchemaAnalyzer = true;
}
} }
public findSelectedCollection(): ViewModels.Collection { public findSelectedCollection(): ViewModels.Collection {
@@ -1072,10 +1047,6 @@ export default class Explorer {
: this.selectedNode().collection) as ViewModels.Collection; : this.selectedNode().collection) as ViewModels.Collection;
} }
public closeAllPanes(): void {
this._panes.forEach((pane: ContextualPaneBase) => pane.close());
}
public isRunningOnNationalCloud(): boolean { public isRunningOnNationalCloud(): boolean {
return ( return (
userContext.portalEnv === "blackforest" || userContext.portalEnv === "blackforest" ||
@@ -1869,7 +1840,6 @@ export default class Explorer {
public async handleOpenFileAction(path: string): Promise<void> { public async handleOpenFileAction(path: string): Promise<void> {
if (this.isAccountReady() && !(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) { if (this.isAccountReady() && !(await this._containsDefaultNotebookWorkspace(userContext.databaseAccount))) {
this.closeAllPanes();
this._openSetupNotebooksPaneForQuickstart(); this._openSetupNotebooksPaneForQuickstart();
} }
@@ -1987,7 +1957,7 @@ export default class Explorer {
"Add " + getDatabaseName(), "Add " + getDatabaseName(),
<AddDatabasePanel <AddDatabasePanel
explorer={this} explorer={this}
openNotificationConsole={this.expandConsole} openNotificationConsole={() => this.expandConsole()}
closePanel={this.closeSidePanel} closePanel={this.closeSidePanel}
/> />
); );

View File

@@ -1,7 +1,7 @@
import * as sinon from "sinon"; import * as sinon from "sinon";
import { D3ForceGraph, LoadMoreDataAction, D3GraphNodeData } from "./D3ForceGraph";
import { D3Node, D3Link, GraphData } from "../GraphExplorerComponent/GraphData";
import GraphTab from "../../Tabs/GraphTab"; import GraphTab from "../../Tabs/GraphTab";
import { D3Link, D3Node, GraphData } from "../GraphExplorerComponent/GraphData";
import { D3ForceGraph, D3GraphNodeData, LoadMoreDataAction } from "./D3ForceGraph";
describe("D3ForceGraph", () => { describe("D3ForceGraph", () => {
const v1Id = "v1"; const v1Id = "v1";
@@ -68,7 +68,7 @@ describe("D3ForceGraph", () => {
beforeEach(() => { beforeEach(() => {
forceGraph = new D3ForceGraph({ forceGraph = new D3ForceGraph({
graphConfig: GraphTab.createGraphConfig(), igraphConfig: GraphTab.createIGraphConfig(),
onHighlightedNode: sinon.spy(), onHighlightedNode: sinon.spy(),
onLoadMoreData: (action: LoadMoreDataAction): void => {}, onLoadMoreData: (action: LoadMoreDataAction): void => {},
@@ -141,6 +141,7 @@ describe("D3ForceGraph", () => {
const mouseoverEvent = document.createEvent("Events"); const mouseoverEvent = document.createEvent("Events");
mouseoverEvent.initEvent("mouseover", true, false); mouseoverEvent.initEvent("mouseover", true, false);
$(rootNode).find(".node")[0].dispatchEvent(mouseoverEvent); // [0] is v1 vertex $(rootNode).find(".node")[0].dispatchEvent(mouseoverEvent); // [0] is v1 vertex
expect($(rootNode).find(".node")[0]).toBe(1);
// onHighlightedNode is always called once to clear the selection // onHighlightedNode is always called once to clear the selection
expect((forceGraph.params.onHighlightedNode as sinon.SinonSpy).calledTwice).toBe(true); expect((forceGraph.params.onHighlightedNode as sinon.SinonSpy).calledTwice).toBe(true);
@@ -150,7 +151,7 @@ describe("D3ForceGraph", () => {
expect(onHighlightedNode.id).toEqual(v1Id); expect(onHighlightedNode.id).toEqual(v1Id);
}; };
forceGraph.updateGraph(newGraph); forceGraph.updateGraph(newGraph, forceGraph.igraphConfig);
}); });
}); });
}); });

View File

@@ -13,7 +13,7 @@ import _ from "underscore";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { NeighborType } from "../../../Contracts/ViewModels"; import { NeighborType } from "../../../Contracts/ViewModels";
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils"; import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
import { GraphConfig } from "../../Tabs/GraphTab"; import { IGraphConfig } from "./../../Tabs/GraphTab";
import { D3Link, D3Node, GraphData } from "./GraphData"; import { D3Link, D3Node, GraphData } from "./GraphData";
import { GraphExplorer } from "./GraphExplorer"; import { GraphExplorer } from "./GraphExplorer";
@@ -48,21 +48,22 @@ interface ZoomTransform extends Point2D {
export interface D3ForceGraphParameters { export interface D3ForceGraphParameters {
// Graph to parent // Graph to parent
graphConfig: GraphConfig;
onHighlightedNode: (highlightedNode: D3GraphNodeData) => void; // a new node has been highlighted in the graph igraphConfig: IGraphConfig;
onLoadMoreData: (action: LoadMoreDataAction) => void; onHighlightedNode?: (highlightedNode: D3GraphNodeData) => void; // a new node has been highlighted in the graph
onLoadMoreData?: (action: LoadMoreDataAction) => void;
// parent to graph // parent to graph
onInitialized: (instance: GraphRenderer) => void; onInitialized?: (instance: GraphRenderer) => void;
// For unit testing purposes // For unit testing purposes
onGraphUpdated: (timestamp: number) => void; onGraphUpdated?: (timestamp: number) => void;
} }
export interface GraphRenderer { export interface GraphRenderer {
selectNode(id: string): void; selectNode(id: string): void;
resetZoom(): void; resetZoom(): void;
updateGraph(graphData: GraphData<D3Node, D3Link>): void; updateGraph(graphData: GraphData<D3Node, D3Link>, igraphConfigParam?: IGraphConfig): void;
enableHighlight(enable: boolean): void; enableHighlight(enable: boolean): void;
} }
@@ -108,7 +109,7 @@ export class D3ForceGraph implements GraphRenderer {
private viewCenter: Point2D; private viewCenter: Point2D;
// Map a property to a graph node attribute (such as color) // Map a property to a graph node attribute (such as color)
private uniqueValues: (string | number)[]; // keep track of unique values private uniqueValues: (string | number)[] = []; // keep track of unique values
private graphDataWrapper: GraphData<D3Node, D3Link>; private graphDataWrapper: GraphData<D3Node, D3Link>;
// Communication with outside // Communication with outside
@@ -119,9 +120,11 @@ export class D3ForceGraph implements GraphRenderer {
// outside -> Graph // outside -> Graph
private idToSelect: ko.Observable<string>; // Programmatically select node by id outside graph private idToSelect: ko.Observable<string>; // Programmatically select node by id outside graph
private isHighlightDisabled: boolean; private isHighlightDisabled: boolean;
public igraphConfig: IGraphConfig;
public constructor(params: D3ForceGraphParameters) { public constructor(params: D3ForceGraphParameters) {
this.params = params; this.params = params;
this.igraphConfig = this.params.igraphConfig;
this.idToSelect = ko.observable(null); this.idToSelect = ko.observable(null);
this.errorMsgs = ko.observableArray([]); this.errorMsgs = ko.observableArray([]);
this.graphDataWrapper = null; this.graphDataWrapper = null;
@@ -151,7 +154,10 @@ export class D3ForceGraph implements GraphRenderer {
this.g.remove(); this.g.remove();
} }
public updateGraph(newGraph: GraphData<D3Node, D3Link>): void { public updateGraph(newGraph: GraphData<D3Node, D3Link>, igraphConfigParam?: IGraphConfig): void {
if (igraphConfigParam) {
this.igraphConfig = igraphConfigParam;
}
if (!newGraph || !this.simulation) { if (!newGraph || !this.simulation) {
return; return;
} }
@@ -159,7 +165,8 @@ export class D3ForceGraph implements GraphRenderer {
this.graphDataWrapper = new GraphData<D3Node, D3Link>(); this.graphDataWrapper = new GraphData<D3Node, D3Link>();
this.graphDataWrapper.setData(newGraph); this.graphDataWrapper.setData(newGraph);
const key = this.params.graphConfig.nodeColorKey(); const key = this.igraphConfig.nodeColorKey;
if (key !== GraphExplorer.NONE_CHOICE) { if (key !== GraphExplorer.NONE_CHOICE) {
this.updateUniqueValues(key); this.updateUniqueValues(key);
} }
@@ -265,20 +272,7 @@ export class D3ForceGraph implements GraphRenderer {
}); });
}); });
// Redraw if any of these configs change this.redrawGraph();
this.params.graphConfig.nodeColor.subscribe(this.redrawGraph.bind(this));
this.params.graphConfig.nodeColorKey.subscribe((key: string) => {
// Compute colormap
this.uniqueValues = [];
this.updateUniqueValues(key);
this.redrawGraph();
});
this.params.graphConfig.linkColor.subscribe(() => this.redrawGraph());
this.params.graphConfig.showNeighborType.subscribe(() => this.redrawGraph());
this.params.graphConfig.nodeCaption.subscribe(() => this.redrawGraph());
this.params.graphConfig.nodeSize.subscribe(() => this.redrawGraph());
this.params.graphConfig.linkWidth.subscribe(() => this.redrawGraph());
this.params.graphConfig.nodeIconKey.subscribe(() => this.redrawGraph());
this.instantiateSimulation(); this.instantiateSimulation();
} // initialize } // initialize
@@ -371,7 +365,10 @@ export class D3ForceGraph implements GraphRenderer {
*/ */
private shiftGraph(targetPosition: Point2D): Q.Promise<Point2D> { private shiftGraph(targetPosition: Point2D): Q.Promise<Point2D> {
const deferred: Q.Deferred<Point2D> = Q.defer<Point2D>(); const deferred: Q.Deferred<Point2D> = Q.defer<Point2D>();
const offset = { x: this.width / 2 - targetPosition.x, y: this.height / 2 - targetPosition.y }; const offset = {
x: this.width / 2 - targetPosition.x,
y: this.height / 2 - targetPosition.y,
};
this.viewCenter = targetPosition; this.viewCenter = targetPosition;
if (Math.abs(offset.x) > 0.5 && Math.abs(offset.y) > 0.5) { if (Math.abs(offset.x) > 0.5 && Math.abs(offset.y) > 0.5) {
@@ -526,7 +523,10 @@ export class D3ForceGraph implements GraphRenderer {
.transition() .transition()
.duration(D3ForceGraph.TRANSITION_STEP3_MS) .duration(D3ForceGraph.TRANSITION_STEP3_MS)
.attrTween("transform", (d: D3Node) => { .attrTween("transform", (d: D3Node) => {
const finalPos = nodeFinalPositionMap.get(d.id) || { x: viewCenter.x, y: viewCenter.y }; const finalPos = nodeFinalPositionMap.get(d.id) || {
x: viewCenter.x,
y: viewCenter.y,
};
const ix = interpolateNumber(viewCenter.x, finalPos.x); const ix = interpolateNumber(viewCenter.x, finalPos.x);
const iy = interpolateNumber(viewCenter.y, finalPos.y); const iy = interpolateNumber(viewCenter.y, finalPos.y);
return (t: number) => { return (t: number) => {
@@ -626,10 +626,10 @@ export class D3ForceGraph implements GraphRenderer {
this.addNewLinks(); this.addNewLinks();
const nodes = this.simulation.nodes(); const nodes1 = this.simulation.nodes();
this.redrawGraph(); this.redrawGraph();
this.animateBigBang(nodes, newNodes); this.animateBigBang(nodes1, newNodes);
this.simulation.alpha(1).restart(); this.simulation.alpha(1).restart();
this.params.onGraphUpdated(new Date().getTime()); this.params.onGraphUpdated(new Date().getTime());
@@ -657,8 +657,8 @@ export class D3ForceGraph implements GraphRenderer {
.append("path") .append("path")
.attr("class", "link") .attr("class", "link")
.attr("fill", "none") .attr("fill", "none")
.attr("stroke-width", this.params.graphConfig.linkWidth()) .attr("stroke-width", this.igraphConfig.linkWidth)
.attr("stroke", this.params.graphConfig.linkColor()); .attr("stroke", this.igraphConfig.linkColor);
if (D3ForceGraph.useSvgMarkerEnd()) { if (D3ForceGraph.useSvgMarkerEnd()) {
line.attr("marker-end", `url(#${this.getArrowHeadSymbolId()}-marker)`); line.attr("marker-end", `url(#${this.getArrowHeadSymbolId()}-marker)`);
@@ -668,7 +668,7 @@ export class D3ForceGraph implements GraphRenderer {
.append("use") .append("use")
.attr("xlink:href", `#${this.getArrowHeadSymbolId()}-nonMarker`) .attr("xlink:href", `#${this.getArrowHeadSymbolId()}-nonMarker`)
.attr("class", "markerEnd link") .attr("class", "markerEnd link")
.attr("fill", this.params.graphConfig.linkColor()) .attr("fill", this.igraphConfig.linkColor)
.classed(`${this.getArrowHeadSymbolId()}`, true); .classed(`${this.getArrowHeadSymbolId()}`, true);
} }
@@ -724,7 +724,7 @@ export class D3ForceGraph implements GraphRenderer {
.append("circle") .append("circle")
.attr("fill", this.getNodeColor.bind(this)) .attr("fill", this.getNodeColor.bind(this))
.attr("class", "main") .attr("class", "main")
.attr("r", this.params.graphConfig.nodeSize()); .attr("r", this.igraphConfig.nodeSize);
var iconGroup = newNodes var iconGroup = newNodes
.append("g") .append("g")
@@ -749,7 +749,7 @@ export class D3ForceGraph implements GraphRenderer {
self.onNodeClicked(this.parentNode, d); self.onNodeClicked(this.parentNode, d);
} }
}); });
var nodeSize = this.params.graphConfig.nodeSize(); var nodeSize = this.igraphConfig.nodeSize;
var bgsize = nodeSize + 1; var bgsize = nodeSize + 1;
iconGroup iconGroup
@@ -759,7 +759,7 @@ export class D3ForceGraph implements GraphRenderer {
.attr("width", bgsize * 2) .attr("width", bgsize * 2)
.attr("height", bgsize * 2) .attr("height", bgsize * 2)
.attr("fill-opacity", (d: D3Node) => { .attr("fill-opacity", (d: D3Node) => {
return this.params.graphConfig.nodeIconKey() ? 1 : 0; return this.igraphConfig.nodeIconKey ? 1 : 0;
}) })
.attr("class", "icon-background"); .attr("class", "icon-background");
@@ -767,14 +767,13 @@ export class D3ForceGraph implements GraphRenderer {
iconGroup iconGroup
.append("svg:image") .append("svg:image")
.attr("xlink:href", (d: D3Node) => { .attr("xlink:href", (d: D3Node) => {
return D3ForceGraph.computeImageData(d, this.params.graphConfig); return D3ForceGraph.computeImageData(d, this.igraphConfig);
}) })
.attr("x", -nodeSize) .attr("x", -nodeSize)
.attr("y", -nodeSize) .attr("y", -nodeSize)
.attr("height", nodeSize * 2) .attr("height", nodeSize * 2)
.attr("width", nodeSize * 2) .attr("width", nodeSize * 2)
.attr("class", "icon"); .attr("class", "icon");
newNodes newNodes
.append("text") .append("text")
.attr("class", "caption") .attr("class", "caption")
@@ -808,7 +807,7 @@ export class D3ForceGraph implements GraphRenderer {
.attr("x2", 0) .attr("x2", 0)
.attr("y2", gaugeYOffset) .attr("y2", gaugeYOffset)
.style("stroke-width", 1) .style("stroke-width", 1)
.style("stroke", this.params.graphConfig.linkColor()); .style("stroke", this.igraphConfig.linkColor);
parent parent
.append("use") .append("use")
.attr("xlink:href", "#triangleRight") .attr("xlink:href", "#triangleRight")
@@ -877,7 +876,7 @@ export class D3ForceGraph implements GraphRenderer {
.attr("height", gaugeHeight) .attr("height", gaugeHeight)
.style("fill", "white") .style("fill", "white")
.style("stroke-width", 1) .style("stroke-width", 1)
.style("stroke", this.params.graphConfig.linkColor()); .style("stroke", this.igraphConfig.linkColor);
parent parent
.append("rect") .append("rect")
.attr("x", (d: D3Node) => { .attr("x", (d: D3Node) => {
@@ -894,7 +893,7 @@ export class D3ForceGraph implements GraphRenderer {
: 0; : 0;
}) })
.attr("height", gaugeHeight) .attr("height", gaugeHeight)
.style("fill", this.params.graphConfig.nodeColor()) .style("fill", this.igraphConfig.nodeColor)
.attr("visibility", (d: D3Node) => (d._pagination && d._pagination.total ? "visible" : "hidden")); .attr("visibility", (d: D3Node) => (d._pagination && d._pagination.total ? "visible" : "hidden"));
parent parent
.append("text") .append("text")
@@ -971,7 +970,7 @@ export class D3ForceGraph implements GraphRenderer {
const self = this; const self = this;
nodeSelection.selectAll(".loadmore").remove(); nodeSelection.selectAll(".loadmore").remove();
var nodeSize = this.params.graphConfig.nodeSize(); var nodeSize = this.igraphConfig.nodeSize;
const rootSelectionG = nodeSelection const rootSelectionG = nodeSelection
.filter((d: D3Node) => { .filter((d: D3Node) => {
return !!d._isRoot && !!d._pagination; return !!d._isRoot && !!d._pagination;
@@ -995,7 +994,7 @@ export class D3ForceGraph implements GraphRenderer {
this.createLoadMoreControl(missingNeighborNonRootG, nodeSize); this.createLoadMoreControl(missingNeighborNonRootG, nodeSize);
// Don't color icons individually, just the definitions // Don't color icons individually, just the definitions
this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", this.params.graphConfig.nodeColor()); this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", this.igraphConfig.nodeColor);
} }
/** /**
@@ -1032,11 +1031,11 @@ export class D3ForceGraph implements GraphRenderer {
* @param d * @param d
*/ */
private getNodeColor(d: D3Node): string { private getNodeColor(d: D3Node): string {
if (this.params.graphConfig.nodeColorKey()) { if (this.igraphConfig.nodeColorKey) {
const val = GraphData.getNodePropValue(d, this.params.graphConfig.nodeColorKey()); const val = GraphData.getNodePropValue(d, this.igraphConfig.nodeColorKey);
return this.lookupColorFromKey(<string>val); return this.lookupColorFromKey(<string>val);
} else { } else {
return this.params.graphConfig.nodeColor(); return this.igraphConfig.nodeColor;
} }
} }
@@ -1103,12 +1102,12 @@ export class D3ForceGraph implements GraphRenderer {
this.graphDataWrapper.getTargetsForId(nodeId) this.graphDataWrapper.getTargetsForId(nodeId)
); );
} }
})(this.params.graphConfig.showNeighborType()); })(this.igraphConfig.showNeighborType);
return (!neighbors || neighbors.indexOf(d.id) === -1) && d.id !== nodeId; return (!neighbors || neighbors.indexOf(d.id) === -1) && d.id !== nodeId;
}); });
this.g.selectAll(".link").classed("inactive", (l: D3Link) => { this.g.selectAll(".link").classed("inactive", (l: D3Link) => {
switch (this.params.graphConfig.showNeighborType()) { switch (this.igraphConfig.showNeighborType) {
case NeighborType.SOURCES_ONLY: case NeighborType.SOURCES_ONLY:
return (<D3Node>l.target).id !== nodeId; return (<D3Node>l.target).id !== nodeId;
case NeighborType.TARGETS_ONLY: case NeighborType.TARGETS_ONLY:
@@ -1152,7 +1151,7 @@ export class D3ForceGraph implements GraphRenderer {
} }
private retrieveNodeCaption(d: D3Node) { private retrieveNodeCaption(d: D3Node) {
let key = this.params.graphConfig.nodeCaption(); let key = this.igraphConfig.nodeCaption;
let value: string = d.id || d.label; let value: string = d.id || d.label;
if (key) { if (key) {
value = <string>GraphData.getNodePropValue(d, key) || ""; value = <string>GraphData.getNodePropValue(d, key) || "";
@@ -1194,10 +1193,16 @@ export class D3ForceGraph implements GraphRenderer {
} }
private positionLinkEnd(l: D3Link) { private positionLinkEnd(l: D3Link) {
const source: Point2D = { x: (<D3Node>l.source).x, y: (<D3Node>l.source).y }; const source: Point2D = {
const target: Point2D = { x: (<D3Node>l.target).x, y: (<D3Node>l.target).y }; x: (<D3Node>l.source).x,
y: (<D3Node>l.source).y,
};
const target: Point2D = {
x: (<D3Node>l.target).x,
y: (<D3Node>l.target).y,
};
const d1 = D3ForceGraph.calculateControlPoint(source, target); const d1 = D3ForceGraph.calculateControlPoint(source, target);
var radius = this.params.graphConfig.nodeSize() + 3; var radius = this.igraphConfig.nodeSize + 3;
// End // End
const dx = target.x - d1.x; const dx = target.x - d1.x;
@@ -1210,10 +1215,16 @@ export class D3ForceGraph implements GraphRenderer {
} }
private positionLink(l: D3Link) { private positionLink(l: D3Link) {
const source: Point2D = { x: (<D3Node>l.source).x, y: (<D3Node>l.source).y }; const source: Point2D = {
const target: Point2D = { x: (<D3Node>l.target).x, y: (<D3Node>l.target).y }; x: (<D3Node>l.source).x,
y: (<D3Node>l.source).y,
};
const target: Point2D = {
x: (<D3Node>l.target).x,
y: (<D3Node>l.target).y,
};
const d1 = D3ForceGraph.calculateControlPoint(source, target); const d1 = D3ForceGraph.calculateControlPoint(source, target);
var radius = this.params.graphConfig.nodeSize() + 3; var radius = this.igraphConfig.nodeSize + 3;
// Start // Start
var dx = d1.x - source.x; var dx = d1.x - source.x;
@@ -1245,13 +1256,13 @@ export class D3ForceGraph implements GraphRenderer {
return d._isRoot ? "node root" : "node"; return d._isRoot ? "node root" : "node";
}); });
this.applyConfig(this.params.graphConfig); this.applyConfig(this.igraphConfig);
} }
private static computeImageData(d: D3Node, config: GraphConfig): string { private static computeImageData(d: D3Node, config: IGraphConfig): string {
let propValue = <string>GraphData.getNodePropValue(d, config.nodeIconKey()) || ""; let propValue = <string>GraphData.getNodePropValue(d, config.nodeIconKey) || "";
// Trim leading and trailing spaces to make comparison more forgiving. // Trim leading and trailing spaces to make comparison more forgiving.
let value = config.iconsMap()[propValue.trim()]; let value = config.iconsMap[propValue.trim()];
if (!value) { if (!value) {
return undefined; return undefined;
} }
@@ -1261,48 +1272,46 @@ export class D3ForceGraph implements GraphRenderer {
/** /**
* Update graph according to configuration or use default * Update graph according to configuration or use default
*/ */
private applyConfig(config: GraphConfig) { private applyConfig(config: IGraphConfig) {
if (config.nodeIconKey()) { if (config.nodeIconKey) {
this.g this.g
.selectAll(".node .icon") .selectAll(".node .icon")
.attr("xlink:href", (d: D3Node) => { .attr("xlink:href", (d: D3Node) => {
return D3ForceGraph.computeImageData(d, config); return D3ForceGraph.computeImageData(d, config);
}) })
.attr("x", -config.nodeSize()) .attr("x", -config.nodeSize)
.attr("y", -config.nodeSize()) .attr("y", -config.nodeSize)
.attr("height", config.nodeSize() * 2) .attr("height", config.nodeSize * 2)
.attr("width", config.nodeSize() * 2) .attr("width", config.nodeSize * 2)
.attr("class", "icon"); .attr("class", "icon");
} else { } else {
// clear icons // clear icons
this.g.selectAll(".node .icon").attr("xlink:href", undefined); this.g.selectAll(".node .icon").attr("xlink:href", undefined);
} }
this.g.selectAll(".node .icon-background").attr("fill-opacity", (d: D3Node) => { this.g.selectAll(".node .icon-background").attr("fill-opacity", (d: D3Node) => {
return config.nodeIconKey() ? 1 : 0; return config.nodeIconKey ? 1 : 0;
}); });
this.g.selectAll(".node text.caption").text((d: D3Node) => { this.g.selectAll(".node text.caption").text((d: D3Node) => {
return this.retrieveNodeCaption(d); return this.retrieveNodeCaption(d);
}); });
this.g.selectAll(".node circle.main").attr("r", config.nodeSize()); this.g.selectAll(".node circle.main").attr("r", config.nodeSize);
this.g.selectAll(".node text.caption").attr("dx", config.nodeSize() + 2); this.g.selectAll(".node text.caption").attr("dx", config.nodeSize + 2);
this.g.selectAll(".node circle").attr("fill", this.getNodeColor.bind(this)); this.g.selectAll(".node circle").attr("fill", this.getNodeColor.bind(this));
// Can't color nodes individually if using defs // Can't color nodes individually if using defs
this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", this.params.graphConfig.nodeColor()); this.svg.selectAll("#loadMoreIcon ellipse").attr("fill", config.nodeColor);
this.g.selectAll(".link").attr("stroke-width", config.linkWidth);
this.g.selectAll(".link").attr("stroke-width", config.linkWidth()); this.g.selectAll(".link").attr("stroke", config.linkColor);
this.g.selectAll(".link").attr("stroke", config.linkColor());
if (D3ForceGraph.useSvgMarkerEnd()) { if (D3ForceGraph.useSvgMarkerEnd()) {
this.svg this.svg
.select(`#${this.getArrowHeadSymbolId()}-marker`) .select(`#${this.getArrowHeadSymbolId()}-marker`)
.attr("fill", config.linkColor()) .attr("fill", config.linkColor)
.attr("stroke", config.linkColor()); .attr("stroke", config.linkColor);
} else { } else {
this.svg.select(`#${this.getArrowHeadSymbolId()}-nonMarker`).attr("fill", config.linkColor()); this.svg.select(`#${this.getArrowHeadSymbolId()}-nonMarker`).attr("fill", config.linkColor);
} }
// Reset highlight // Reset highlight

View File

@@ -1,20 +1,20 @@
jest.mock("../../../Common/dataAccess/queryDocuments"); jest.mock("../../../Common/dataAccess/queryDocuments");
jest.mock("../../../Common/dataAccess/queryDocumentsPage"); jest.mock("../../../Common/dataAccess/queryDocumentsPage");
import React from "react";
import * as sinon from "sinon";
import { mount, ReactWrapper } from "enzyme"; import { mount, ReactWrapper } from "enzyme";
import * as Q from "q"; import * as Q from "q";
import React from "react";
import * as sinon from "sinon";
import "../../../../externals/jquery.typeahead.min"; import "../../../../externals/jquery.typeahead.min";
import { GraphExplorer, GraphExplorerProps, GraphAccessor, GraphHighlightedNodeData } from "./GraphExplorer";
import * as D3ForceGraph from "./D3ForceGraph";
import { GraphData } from "./GraphData";
import { TabComponent } from "../../Controls/Tabs/TabComponent";
import * as DataModels from "../../../Contracts/DataModels";
import * as StorageUtility from "../../../Shared/StorageUtility";
import GraphTab from "../../Tabs/GraphTab";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments"; import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage"; import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
import * as DataModels from "../../../Contracts/DataModels";
import * as StorageUtility from "../../../Shared/StorageUtility";
import { TabComponent } from "../../Controls/Tabs/TabComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import GraphTab from "../../Tabs/GraphTab";
import * as D3ForceGraph from "./D3ForceGraph";
import { GraphData } from "./GraphData";
import { GraphAccessor, GraphExplorer, GraphExplorerProps, GraphHighlightedNodeData } from "./GraphExplorer";
describe("Check whether query result is vertex array", () => { describe("Check whether query result is vertex array", () => {
it("should reject null as vertex array", () => { it("should reject null as vertex array", () => {
@@ -146,8 +146,8 @@ describe("GraphExplorer", () => {
const gremlinRU = 789.12; const gremlinRU = 789.12;
const createMockProps = (): GraphExplorerProps => { const createMockProps = (): GraphExplorerProps => {
const graphConfig = GraphTab.createGraphConfig(); const igraphConfig = GraphTab.createIGraphConfig();
const graphConfigUi = GraphTab.createGraphConfigUiData(graphConfig); const igraphConfigUi = GraphTab.createIGraphConfigUiData(igraphConfig);
return { return {
onGraphAccessorCreated: (instance: GraphAccessor): void => {}, onGraphAccessorCreated: (instance: GraphAccessor): void => {},
@@ -170,8 +170,9 @@ describe("GraphExplorer", () => {
resourceId: "resourceId", resourceId: "resourceId",
/* TODO Figure out how to make this Knockout-free */ /* TODO Figure out how to make this Knockout-free */
graphConfigUiData: graphConfigUi, igraphConfigUiData: igraphConfigUi,
graphConfig: graphConfig, igraphConfig: igraphConfig,
setIConfigUiData: (data: string[]): void => {},
}; };
}; };

View File

@@ -19,7 +19,7 @@ import { EditorReact } from "../../Controls/Editor/EditorReact";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent"; import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import * as TabComponent from "../../Controls/Tabs/TabComponent"; import * as TabComponent from "../../Controls/Tabs/TabComponent";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import { GraphConfig } from "../../Tabs/GraphTab"; import { IGraphConfig } from "../../Tabs/GraphTab";
import { ArraysByKeyCache } from "./ArraysByKeyCache"; import { ArraysByKeyCache } from "./ArraysByKeyCache";
import * as D3ForceGraph from "./D3ForceGraph"; import * as D3ForceGraph from "./D3ForceGraph";
import { EdgeInfoCache } from "./EdgeInfoCache"; import { EdgeInfoCache } from "./EdgeInfoCache";
@@ -31,10 +31,10 @@ import * as LeftPane from "./LeftPaneComponent";
import { MiddlePaneComponent } from "./MiddlePaneComponent"; import { MiddlePaneComponent } from "./MiddlePaneComponent";
import * as NodeProperties from "./NodePropertiesComponent"; import * as NodeProperties from "./NodePropertiesComponent";
import { QueryContainerComponent } from "./QueryContainerComponent"; import { QueryContainerComponent } from "./QueryContainerComponent";
export interface GraphAccessor { export interface GraphAccessor {
applyFilter: () => void; applyFilter: () => void;
addVertex: (v: ViewModels.NewVertexData) => Q.Promise<void>; addVertex: (v: ViewModels.NewVertexData) => Q.Promise<void>;
shareIGraphConfig: (igraphConfig: IGraphConfig) => void;
} }
export interface GraphExplorerProps { export interface GraphExplorerProps {
@@ -58,9 +58,10 @@ export interface GraphExplorerProps {
onLoadStartKeyChange: (newKey: number) => void; onLoadStartKeyChange: (newKey: number) => void;
resourceId: string; resourceId: string;
/* TODO Figure out how to make this Knockout-free */ igraphConfigUiData: ViewModels.IGraphConfigUiData;
graphConfigUiData: ViewModels.GraphConfigUiData; igraphConfig: IGraphConfig;
graphConfig?: GraphConfig;
setIConfigUiData?: (data: string[]) => void;
} }
export interface GraphHighlightedNodeData { export interface GraphHighlightedNodeData {
@@ -121,6 +122,10 @@ interface GraphExplorerState {
filterQueryError: string; filterQueryError: string;
filterQueryWarning: string; filterQueryWarning: string;
filterQueryStatus: FilterQueryStatus; filterQueryStatus: FilterQueryStatus;
change: string;
igraphConfigUiData: ViewModels.IGraphConfigUiData;
igraphConfig: IGraphConfig;
} }
export interface EditedProperties { export interface EditedProperties {
@@ -218,6 +223,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
private lastReportedIsPropertyEditing: boolean; private lastReportedIsPropertyEditing: boolean;
private lastReportedIsNewVertexDisabled: boolean; private lastReportedIsNewVertexDisabled: boolean;
public getNodeProperties: string[];
public igraphConfigUi: ViewModels.IGraphConfigUiData;
public constructor(props: GraphExplorerProps) { public constructor(props: GraphExplorerProps) {
super(props); super(props);
this.state = { this.state = {
@@ -237,6 +244,9 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
filterQueryError: null, filterQueryError: null,
filterQueryWarning: null, filterQueryWarning: null,
filterQueryStatus: FilterQueryStatus.NoResult, filterQueryStatus: FilterQueryStatus.NoResult,
change: null,
igraphConfigUiData: this.props.igraphConfigUiData,
igraphConfig: this.props.igraphConfig,
}; };
// Not part of React state // Not part of React state
@@ -284,41 +294,27 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.setGremlinParams(); this.setGremlinParams();
} }
/* TODO Make this Knockout-free ! */ const selectedNode = this.state.highlightedNode;
this.props.graphConfigUiData.nodeCaptionChoice.subscribe((key) => {
this.props.graphConfig.nodeCaption(key);
const selectedNode = this.state.highlightedNode;
if (selectedNode) {
this.updatePropertiesPane(selectedNode.id);
}
this.render();
});
this.props.graphConfigUiData.nodeColorKeyChoice.subscribe((val) => {
this.props.graphConfig.nodeColorKey(val === GraphExplorer.NONE_CHOICE ? null : val);
this.render();
});
this.props.graphConfigUiData.showNeighborType.subscribe((val) => {
this.props.graphConfig.showNeighborType(val);
this.render();
});
this.props.graphConfigUiData.nodeIconChoice.subscribe((val) => {
this.updateNodeIcons(val, this.props.graphConfigUiData.nodeIconSet());
this.render();
});
this.props.graphConfigUiData.nodeIconSet.subscribe((val) => {
this.updateNodeIcons(this.props.graphConfigUiData.nodeIconChoice(), val);
this.render();
});
/* *************************************** */
props.onGraphAccessorCreated({ props.onGraphAccessorCreated({
applyFilter: this.submitQuery.bind(this), applyFilter: this.submitQuery.bind(this),
addVertex: this.addVertex.bind(this), addVertex: this.addVertex.bind(this),
shareIGraphConfig: this.shareIGraphConfig.bind(this),
}); });
} // constructor } // constructor
public shareIGraphConfig(igraphConfig: IGraphConfig) {
this.setState({
igraphConfig: { ...igraphConfig },
});
const selectedNode = this.state.highlightedNode;
if (selectedNode) {
this.updatePropertiesPane(selectedNode.id);
this.setResultDisplay(GraphExplorer.TAB_INDEX_GRAPH);
}
}
/** /**
* If pk is a string, return ["pk", "id"] * If pk is a string, return ["pk", "id"]
* else return [pk, "id"] * else return [pk, "id"]
@@ -408,7 +404,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// Update graph (in case property is being shown) // Update graph (in case property is being shown)
this.updateInMemoryGraph(result.data); this.updateInMemoryGraph(result.data);
this.updateGraphData(this.originalGraphData); this.updateGraphData(this.originalGraphData, this.state.igraphConfig);
}) })
.then( .then(
() => { () => {
@@ -446,7 +442,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
// Remove vertex from local cache // Remove vertex from local cache
const graphData = this.originalGraphData; const graphData = this.originalGraphData;
graphData.removeVertex(id, false); graphData.removeVertex(id, false);
this.updateGraphData(graphData); this.updateGraphData(graphData, this.state.igraphConfig);
this.setState({ highlightedNode: null }); this.setState({ highlightedNode: null });
// Remove from root map // Remove from root map
@@ -582,7 +578,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
this.edgeInfoCache.addVertex(vertex); this.edgeInfoCache.addVertex(vertex);
graphData.setAsRoot(vertex.id); graphData.setAsRoot(vertex.id);
this.updateGraphData(graphData); this.updateGraphData(graphData, this.state.igraphConfig);
}; };
vertex._outEdgeIds = vertex._outEdgeIds || []; vertex._outEdgeIds = vertex._outEdgeIds || [];
@@ -788,7 +784,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
graphData.getVertexById(edge.outV)._inEAllLoaded = false; graphData.getVertexById(edge.outV)._inEAllLoaded = false;
} }
this.updateGraphData(graphData); this.updateGraphData(graphData, this.state.igraphConfig);
}, },
(error: string) => { (error: string) => {
GraphExplorer.reportToConsole( GraphExplorer.reportToConsole(
@@ -809,7 +805,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
() => { () => {
let graphData = this.originalGraphData; let graphData = this.originalGraphData;
graphData.removeEdge(edgeId, false); graphData.removeEdge(edgeId, false);
this.updateGraphData(graphData); this.updateGraphData(graphData, this.state.igraphConfig);
}, },
(error: string) => { (error: string) => {
GraphExplorer.reportToConsole( GraphExplorer.reportToConsole(
@@ -858,7 +854,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
if (vertices.length === 0) { if (vertices.length === 0) {
// Clean graph // Clean graph
this.updateGraphData(new GraphData.GraphData()); this.updateGraphData(new GraphData.GraphData(), this.state.igraphConfig);
this.setState({ highlightedNode: null }); this.setState({ highlightedNode: null });
GraphExplorer.reportToConsole(ConsoleDataType.Info, "Query result is empty"); GraphExplorer.reportToConsole(ConsoleDataType.Info, "Query result is empty");
} }
@@ -940,7 +936,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
let vertex = vertices[0]; let vertex = vertices[0];
const graphData = this.originalGraphData; const graphData = this.originalGraphData;
graphData.addVertex(vertex); graphData.addVertex(vertex);
this.updateGraphData(graphData); this.updateGraphData(graphData, this.state.igraphConfig);
this.collectNodeProperties(this.originalGraphData.vertices); this.collectNodeProperties(this.originalGraphData.vertices);
// Keep new vertex selected // Keep new vertex selected
@@ -1121,8 +1117,13 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return rootMap[id]; return rootMap[id];
}) })
); );
if (this.props.graphConfigUiData.nodeProperties().indexOf(GraphExplorer.DISPLAY_DEFAULT_PROPERTY_KEY) !== -1) { if (this.state.igraphConfigUiData.nodeProperties.indexOf(GraphExplorer.DISPLAY_DEFAULT_PROPERTY_KEY) !== -1) {
this.props.graphConfigUiData.nodeCaptionChoice(GraphExplorer.DISPLAY_DEFAULT_PROPERTY_KEY); this.setState({
igraphConfigUiData: {
...this.state.igraphConfigUiData,
nodeCaptionChoice: GraphExplorer.DISPLAY_DEFAULT_PROPERTY_KEY,
},
});
} }
// Let react instantiate and render graph, before updating // Let react instantiate and render graph, before updating
@@ -1139,7 +1140,12 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
*/ */
public updateNodeIcons(nodeProp: string, iconSet: string): void { public updateNodeIcons(nodeProp: string, iconSet: string): void {
if (nodeProp === GraphExplorer.NONE_CHOICE) { if (nodeProp === GraphExplorer.NONE_CHOICE) {
this.props.graphConfig.nodeIconKey(null); this.setState({
igraphConfig: {
...this.state.igraphConfig,
nodeIconKey: undefined,
},
});
return; return;
} }
@@ -1163,8 +1169,13 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}); });
// Update graph configuration // Update graph configuration
this.props.graphConfig.iconsMap(newIconsMap); this.setState({
this.props.graphConfig.nodeIconKey(nodeProp); igraphConfig: {
...this.state.igraphConfig,
iconsMap: newIconsMap,
nodeIconKey: nodeProp,
},
});
}, },
() => { () => {
GraphExplorer.reportToConsole(ConsoleDataType.Error, `Failed to retrieve icons. iconSet:${iconSet}`); GraphExplorer.reportToConsole(ConsoleDataType.Error, `Failed to retrieve icons. iconSet:${iconSet}`);
@@ -1209,7 +1220,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
} }
private getPossibleRootNodes(): LeftPane.CaptionId[] { private getPossibleRootNodes(): LeftPane.CaptionId[] {
const key = this.props.graphConfigUiData.nodeCaptionChoice(); const key = this.state.igraphConfig.nodeCaption;
return $.map( return $.map(
this.state.rootMap, this.state.rootMap,
(value: any, index: number): LeftPane.CaptionId => { (value: any, index: number): LeftPane.CaptionId => {
@@ -1320,7 +1331,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return ""; return "";
} }
const nodeCaption = this.props.graphConfigUiData.nodeCaptionChoice(); const nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
const node = this.originalGraphData.getVertexById(this.state.highlightedNode.id); const node = this.originalGraphData.getVertexById(this.state.highlightedNode.id);
return GraphData.GraphData.getNodePropValue(node, nodeCaption) as string; return GraphData.GraphData.getNodePropValue(node, nodeCaption) as string;
} }
@@ -1410,7 +1421,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null; const highlightedNodeId = this.state.highlightedNode ? this.state.highlightedNode.id : null;
const q = `SELECT c.id, c["${ const q = `SELECT c.id, c["${
this.props.graphConfigUiData.nodeCaptionChoice() || "id" this.state.igraphConfigUiData.nodeCaptionChoice || "id"
}"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`; }"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`;
return this.executeNonPagedDocDbQuery(q).then( return this.executeNonPagedDocDbQuery(q).then(
(documents: DataModels.DocumentId[]) => { (documents: DataModels.DocumentId[]) => {
@@ -1539,9 +1550,14 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
}); });
const values = Object.keys(props); const values = Object.keys(props);
this.props.graphConfigUiData.nodeProperties(values); this.setState({
// TODO This should move out of GraphExplorer igraphConfigUiData: {
this.props.graphConfigUiData.nodePropertiesWithNone([GraphExplorer.NONE_CHOICE].concat(values)); ...this.state.igraphConfigUiData,
nodeProperties: values,
},
});
this.props.setIConfigUiData(values);
} }
/** /**
@@ -1566,9 +1582,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
let sources: NeighborVertexBasicInfo[] = []; let sources: NeighborVertexBasicInfo[] = [];
let targets: NeighborVertexBasicInfo[] = []; let targets: NeighborVertexBasicInfo[] = [];
this.props.onResetDefaultGraphConfigValues(); this.props.onResetDefaultGraphConfigValues();
let nodeCaption = this.props.graphConfigUiData.nodeCaptionChoice(); let nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
this.updateSelectedNodeNeighbors(data.id, nodeCaption, sources, targets); this.updateSelectedNodeNeighbors(data.id, nodeCaption, sources, targets);
let sData: GraphHighlightedNodeData = { let sData: GraphHighlightedNodeData = {
id: data.id, id: data.id,
label: data.label, label: data.label,
@@ -1615,7 +1630,12 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return; return;
} }
let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string; let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
sources.push({ name: caption, id: neighborId, edgeId: edge.id, edgeLabel: p }); sources.push({
name: caption,
id: neighborId,
edgeId: edge.id,
edgeLabel: p,
});
}); });
} }
@@ -1629,7 +1649,12 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
return; return;
} }
let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string; let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
targets.push({ name: caption, id: neighborId, edgeId: edge.id, edgeLabel: p }); targets.push({
name: caption,
id: neighborId,
edgeId: edge.id,
edgeLabel: p,
});
}); });
} }
@@ -1678,14 +1703,17 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
/** /**
* Clone object and keep the original untouched (by d3) * Clone object and keep the original untouched (by d3)
*/ */
private updateGraphData(graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>) { private updateGraphData(
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>,
igraphConfig?: IGraphConfig
) {
this.originalGraphData = graphData; this.originalGraphData = graphData;
let gd = JSON.parse(JSON.stringify(this.originalGraphData)); let gd = JSON.parse(JSON.stringify(this.originalGraphData));
if (!this.d3ForceGraph) { if (!this.d3ForceGraph) {
console.warn("Attempting to update graph, but d3ForceGraph not initialized, yet."); console.warn("Attempting to update graph, but d3ForceGraph not initialized, yet.");
return; return;
} }
this.d3ForceGraph.updateGraph(gd); this.d3ForceGraph.updateGraph(gd, igraphConfig);
} }
public onMiddlePaneInitialized(instance: D3ForceGraph.GraphRenderer): void { public onMiddlePaneInitialized(instance: D3ForceGraph.GraphRenderer): void {
@@ -1694,10 +1722,12 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
private renderMiddlePane(): JSX.Element { private renderMiddlePane(): JSX.Element {
const forceGraphParams: D3ForceGraph.D3ForceGraphParameters = { const forceGraphParams: D3ForceGraph.D3ForceGraphParameters = {
graphConfig: this.props.graphConfig, igraphConfig: this.state.igraphConfig,
onHighlightedNode: this.onHighlightedNode.bind(this), onHighlightedNode: this.onHighlightedNode.bind(this),
onLoadMoreData: this.onLoadMoreData.bind(this), onLoadMoreData: this.onLoadMoreData.bind(this),
onInitialized: (instance: D3ForceGraph.GraphRenderer): void => this.onMiddlePaneInitialized(instance), onInitialized: (instance: D3ForceGraph.GraphRenderer): void => {
this.onMiddlePaneInitialized(instance);
},
onGraphUpdated: this.onGraphUpdated.bind(this), onGraphUpdated: this.onGraphUpdated.bind(this),
}; };

View File

@@ -0,0 +1,74 @@
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import * as ViewModels from "../../../Contracts/ViewModels";
import { IGraphConfig } from "../../Tabs/GraphTab";
import { GraphAccessor, GraphExplorer } from "./GraphExplorer";
interface Parameter {
onIsNewVertexDisabledChange: (isEnabled: boolean) => void;
onGraphAccessorCreated: (instance: GraphAccessor) => void;
onIsFilterQueryLoading: (isFilterQueryLoading: boolean) => void;
onIsValidQuery: (isValidQuery: boolean) => void;
onIsPropertyEditing: (isEditing: boolean) => void;
onIsGraphDisplayed: (isDisplayed: boolean) => void;
onResetDefaultGraphConfigValues: () => void;
collectionPartitionKeyProperty: string;
graphBackendEndpoint: string;
databaseId: string;
collectionId: string;
masterKey: string;
onLoadStartKey: number;
onLoadStartKeyChange: (newKey: number) => void;
resourceId: string;
igraphConfigUiData: ViewModels.IGraphConfigUiData;
igraphConfig: IGraphConfig;
setIConfigUiData?: (data: string[]) => void;
}
interface IGraphExplorerProps {
isChanged: boolean;
}
interface IGraphExplorerStates {
isChangedState: boolean;
}
export interface GraphExplorerAdapter
extends ReactAdapter,
React.Component<IGraphExplorerProps, IGraphExplorerStates> {}
export class GraphExplorerAdapter implements ReactAdapter {
public params: Parameter;
public parameters = {};
public isNewVertexDisabled: boolean;
public constructor(params: Parameter, props?: IGraphExplorerProps) {
this.params = params;
}
public renderComponent(): JSX.Element {
return (
<GraphExplorer
onIsNewVertexDisabledChange={this.params.onIsNewVertexDisabledChange}
onGraphAccessorCreated={this.params.onGraphAccessorCreated}
onIsFilterQueryLoadingChange={this.params.onIsFilterQueryLoading}
onIsValidQueryChange={this.params.onIsValidQuery}
onIsPropertyEditing={this.params.onIsPropertyEditing}
onIsGraphDisplayed={this.params.onIsGraphDisplayed}
onResetDefaultGraphConfigValues={this.params.onResetDefaultGraphConfigValues}
collectionPartitionKeyProperty={this.params.collectionPartitionKeyProperty}
graphBackendEndpoint={this.params.graphBackendEndpoint}
databaseId={this.params.databaseId}
collectionId={this.params.collectionId}
masterKey={this.params.masterKey}
onLoadStartKey={this.params.onLoadStartKey}
onLoadStartKeyChange={this.params.onLoadStartKeyChange}
resourceId={this.params.resourceId}
igraphConfigUiData={this.params.igraphConfigUiData}
igraphConfig={this.params.igraphConfig}
setIConfigUiData={this.params.setIConfigUiData}
/>
);
}
}

View File

@@ -1,51 +0,0 @@
import * as ko from "knockout";
import { GraphStyleComponent, GraphStyleParams } from "./GraphStyleComponent";
import * as ViewModels from "../../../Contracts/ViewModels";
function buildComponent(buttonOptions: any) {
document.body.innerHTML = GraphStyleComponent.template as any;
const vm = new GraphStyleComponent.viewModel(buttonOptions);
ko.applyBindings(vm);
}
describe("Graph Style Component", () => {
let buildParams = (config: ViewModels.GraphConfigUiData): GraphStyleParams => {
return {
config: config,
};
};
afterEach(() => {
ko.cleanNode(document);
});
describe("Rendering", () => {
it("should display proper list of choices passed in component parameters", () => {
const PROP2 = "prop2";
const PROPC = "prop3";
const params = buildParams({
nodeCaptionChoice: ko.observable(null),
nodeIconChoice: ko.observable(null),
nodeColorKeyChoice: ko.observable(null),
nodeIconSet: ko.observable(null),
nodeProperties: ko.observableArray(["prop1", PROP2]),
nodePropertiesWithNone: ko.observableArray(["propa", "propb", PROPC]),
showNeighborType: ko.observable(null),
});
buildComponent(params);
var e: any = document.querySelector(".graphStyle #nodeCaptionChoices");
expect(e.options.length).toBe(2);
expect(e.options[1].value).toBe(PROP2);
e = document.querySelector(".graphStyle #nodeColorKeyChoices");
expect(e.options.length).toBe(3);
expect(e.options[2].value).toBe(PROPC);
e = document.querySelector(".graphStyle #nodeIconChoices");
expect(e.options.length).toBe(3);
expect(e.options[2].value).toBe(PROPC);
});
});
});

View File

@@ -0,0 +1,67 @@
import { render, screen } from "@testing-library/react";
import React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { IGraphConfig } from "../../Tabs/GraphTab";
import { GraphStyleComponent, GraphStyleProps } from "./GraphStyleComponent";
describe("Graph Style Component", () => {
let fakeGraphConfig: IGraphConfig;
let fakeGraphConfigUiData: ViewModels.IGraphConfigUiData;
let props: GraphStyleProps;
beforeEach(() => {
fakeGraphConfig = {
nodeColor: "orange",
nodeColorKey: "node2",
linkColor: "orange",
showNeighborType: 0,
nodeCaption: "node1",
nodeSize: 10,
linkWidth: 1,
nodeIconKey: undefined,
iconsMap: {},
};
fakeGraphConfigUiData = {
nodeCaptionChoice: "node1",
nodeIconChoice: undefined,
nodeColorKeyChoice: "node2",
nodeIconSet: undefined,
nodeProperties: ["node1", "node2", "node3"],
nodePropertiesWithNone: ["none", "node1", "node2", "node3"],
showNeighborType: undefined,
};
props = {
igraphConfig: fakeGraphConfig,
igraphConfigUiData: fakeGraphConfigUiData,
getValues: (): void => undefined,
};
render(<GraphStyleComponent {...props} />);
});
it("should render default property", () => {
const { asFragment } = render(<GraphStyleComponent {...props} />);
expect(asFragment).toMatchSnapshot();
});
it("should render node properties dropdown list ", () => {
const dropDownList = screen.getByText("Show vertex (node) as");
expect(dropDownList).toBeDefined();
});
it("should render Map this property to node color dropdown list", () => {
const nodeColorDropdownList = screen.getByText("Map this property to node color");
expect(nodeColorDropdownList).toBeDefined();
});
it("should render show neighbor options", () => {
const nodeShowNeighborOptions = screen.getByText("Show");
expect(nodeShowNeighborOptions).toBeDefined();
});
it("should call handleOnChange method", () => {
const handleOnChange = jest.fn();
const nodeCaptionDropdownList = screen.getByText("Show vertex (node) as");
nodeCaptionDropdownList.onchange = handleOnChange();
expect(handleOnChange).toHaveBeenCalled();
});
});

View File

@@ -1,103 +0,0 @@
import * as Constants from "../../../Common/Constants";
import * as ViewModels from "../../../Contracts/ViewModels";
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
/**
* Parameters for this component
*/
export interface GraphStyleParams {
config: ViewModels.GraphConfigUiData;
firstFieldHasFocus?: ko.Observable<boolean>;
/**
* Callback triggered when the template is bound to the component (for testing purposes)
*/
onTemplateReady?: () => void;
}
class GraphStyleViewModel extends WaitsForTemplateViewModel {
private params: GraphStyleParams;
public constructor(params: GraphStyleParams) {
super();
super.onTemplateReady((isTemplateReady: boolean) => {
if (isTemplateReady && params.onTemplateReady) {
params.onTemplateReady();
}
});
this.params = params;
}
public onAllNeighborsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.params.config.showNeighborType(ViewModels.NeighborType.BOTH);
event.stopPropagation();
return false;
}
return true;
};
public onSourcesKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.params.config.showNeighborType(ViewModels.NeighborType.SOURCES_ONLY);
event.stopPropagation();
return false;
}
return true;
};
public onTargetsKeyPress = (source: any, event: KeyboardEvent): boolean => {
if (event.keyCode === Constants.KeyCodes.Space || event.keyCode === Constants.KeyCodes.Enter) {
this.params.config.showNeighborType(ViewModels.NeighborType.TARGETS_ONLY);
event.stopPropagation();
return false;
}
return true;
};
}
const template = `
<div id="graphStyle" class="graphStyle" data-bind="setTemplateReady: true, with:params.config">
<div class="seconddivpadding">
<p>Show vertex (node) as</p>
<select id="nodeCaptionChoices" class="formTree paneselect" required data-bind="options:nodeProperties,
value:nodeCaptionChoice, hasFocus: $parent.params.firstFieldHasFocus"></select>
</div>
<div class="seconddivpadding">
<p>Map this property to node color</p>
<select id="nodeColorKeyChoices" class="formTree paneselect" required data-bind="options:nodePropertiesWithNone,
value:nodeColorKeyChoice"></select>
</div>
<div class="seconddivpadding">
<p>Map this property to node icon</p>
<select id="nodeIconChoices" class="formTree paneselect" required data-bind="options:nodePropertiesWithNone,
value:nodeIconChoice"></select>
<input type="text" data-bind="value:nodeIconSet" placeholder="Icon set: blank for collection id" class="nodeIconSet" autocomplete="off" />
</div>
<p class="seconddivpadding">Show</p>
<div class="tabs">
<div class="tab">
<input type="radio" id="tab11" name="graphneighbortype" class="radio" data-bind="checkedValue:2, checked:showNeighborType" />
<label for="tab11" tabindex="0" data-bind="event: { keypress: $parent.onAllNeighborsKeyPress }">All neighbors</label>
</div>
<div class="tab">
<input type="radio" id="tab12" name="graphneighbortype" class="radio" data-bind="checkedValue:0, checked:showNeighborType" />
<label for="tab12" tabindex="0" data-bind="event: { keypress: $parent.onSourcesKeyPress }">Sources</label>
</div>
<div class="tab">
<input type="radio" id="tab13" name="graphneighbortype" class="radio" data-bind="checkedValue:1, checked:showNeighborType" />
<label for="tab13" tabindex="0" data-bind="event: { keypress: $parent.onTargetsKeyPress }">Targets</label>
</div>
</div>
</div>`;
export const GraphStyleComponent = {
viewModel: GraphStyleViewModel,
template,
};

View File

@@ -0,0 +1,131 @@
import { ChoiceGroup, Dropdown, IChoiceGroupOption, IDropdownOption, IDropdownStyles, Stack } from "@fluentui/react";
import React, { FunctionComponent, useEffect, useState } from "react";
import { IGraphConfigUiData, NeighborType } from "../../../Contracts/ViewModels";
import { IGraphConfig } from "../../Tabs/GraphTab";
const IGraphConfigType = {
NODE_CAPTION: "NODE_CAPTION",
NODE_COLOR: "NODE_COLOR",
NODE_ICON: "NODE_ICON",
SHOW_NEIGHBOR_TYPE: "SHOW_NEIGHBOR_TYPE",
};
export interface GraphStyleProps {
igraphConfig: IGraphConfig;
igraphConfigUiData: IGraphConfigUiData;
getValues: (igraphConfig?: IGraphConfig) => void;
}
export const GraphStyleComponent: FunctionComponent<GraphStyleProps> = ({
igraphConfig,
igraphConfigUiData,
getValues,
}: GraphStyleProps): JSX.Element => {
const [igraphConfigState, setIGraphConfig] = useState<IGraphConfig>(igraphConfig);
const [selected, setSelected] = useState<boolean>(false);
const nodePropertiesOptions = igraphConfigUiData.nodeProperties.map((nodeProperty) => ({
key: nodeProperty,
text: nodeProperty,
}));
const nodePropertiesWithNoneOptions = igraphConfigUiData.nodePropertiesWithNone.map((nodePropertyWithNone) => ({
key: nodePropertyWithNone,
text: nodePropertyWithNone,
}));
const showNeighborTypeOptions: IChoiceGroupOption[] = [
{ key: NeighborType.BOTH.toString(), text: "All neighbors" },
{ key: NeighborType.SOURCES_ONLY.toString(), text: "Sources" },
{ key: NeighborType.TARGETS_ONLY.toString(), text: "Targets" },
];
const dropdownStyles: Partial<IDropdownStyles> = {
dropdown: { height: 32, marginRight: 10 },
};
const choiceButtonStyles = {
flexContainer: [
{
selectors: {
".ms-ChoiceField-wrapper label": {
fontSize: 14,
paddingTop: 0,
},
},
},
],
};
useEffect(() => {
if (selected) {
getValues(igraphConfigState);
}
//eslint-disable-next-line
}, [igraphConfigState]);
const handleOnChange = (val: string, igraphConfigType: string) => {
switch (igraphConfigType) {
case IGraphConfigType.NODE_CAPTION:
setSelected(true);
setIGraphConfig({
...igraphConfigState,
nodeCaption: val,
});
break;
case IGraphConfigType.NODE_COLOR:
setSelected(true);
setIGraphConfig({
...igraphConfigState,
nodeColorKey: val,
});
break;
case IGraphConfigType.SHOW_NEIGHBOR_TYPE:
setSelected(true);
setIGraphConfig({
...igraphConfigState,
showNeighborType: parseInt(val),
});
break;
}
};
return (
<Stack>
<div id="graphStyle" className="graphStyle">
<div className="seconddivpadding">
<Dropdown
label="Show vertex (node) as"
options={nodePropertiesOptions}
required
selectedKey={igraphConfigState.nodeCaption}
styles={dropdownStyles}
onChange={(_, options: IDropdownOption) =>
handleOnChange(options.key.toString(), IGraphConfigType.NODE_CAPTION)
}
/>
</div>
<div className="seconddivpadding">
<Dropdown
label="Map this property to node color"
options={nodePropertiesWithNoneOptions}
required
selectedKey={igraphConfigState.nodeColorKey}
styles={dropdownStyles}
onChange={(_, options: IDropdownOption) =>
handleOnChange(options.key.toString(), IGraphConfigType.NODE_COLOR)
}
/>
</div>
<div className="seconddivpadding">
<ChoiceGroup
label="Show"
styles={choiceButtonStyles}
options={showNeighborTypeOptions}
selectedKey={igraphConfigState.showNeighborType.toString()}
onChange={(_, options: IChoiceGroupOption) =>
handleOnChange(options.key.toString(), IGraphConfigType.SHOW_NEIGHBOR_TYPE)
}
/>
</div>
</div>
</Stack>
);
};

View File

@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Graph Style Component should render default property 1`] = `[Function]`;

View File

@@ -1,74 +0,0 @@
<div id="graphStyle" class="graphStyle" data-bind="setTemplateReady: true, with:params.config">
<div class="seconddivpadding">
<p>Show vertex (node) as</p>
<select
id="nodeCaptionChoices"
class="formTree paneselect"
required
data-bind="options:nodeProperties,
value:nodeCaptionChoice, hasFocus: $parent.params.firstFieldHasFocus"
></select>
</div>
<div class="seconddivpadding">
<p>Map this property to node color</p>
<select
id="nodeColorKeyChoices"
class="formTree paneselect"
required
data-bind="options:nodePropertiesWithNone,
value:nodeColorKeyChoice"
></select>
</div>
<div class="seconddivpadding">
<p>Map this property to node icon</p>
<select
id="nodeIconChoices"
class="formTree paneselect"
required
data-bind="options:nodePropertiesWithNone,
value:nodeIconChoice"
></select>
<input
type="text"
data-bind="value:nodeIconSet"
placeholder="Icon set: blank for collection id"
class="nodeIconSet"
autocomplete="off"
/>
</div>
<p class="seconddivpadding">Show</p>
<div class="tabs">
<div class="tab">
<input
type="radio"
id="tab11"
name="graphneighbortype"
class="radio"
data-bind="checkedValue:2, checked:showNeighborType"
/>
<label for="tab11">All neighbors</label>
</div>
<div class="tab">
<input
type="radio"
id="tab12"
name="graphneighbortype"
class="radio"
data-bind="checkedValue:0, checked:showNeighborType"
/>
<label for="tab12">Sources</label>
</div>
<div class="tab">
<input
type="radio"
id="tab13"
name="graphneighbortype"
class="radio"
data-bind="checkedValue:1, checked:showNeighborType"
/>
<label for="tab13">Targets</label>
</div>
</div>
</div>

View File

@@ -0,0 +1,108 @@
import { FileType, IContent, IContentProvider, ServerConfig } from "@nteract/core";
import { Observable, of } from "rxjs";
import { AjaxResponse } from "rxjs/ajax";
import { HttpStatusCodes } from "../../../../Common/Constants";
import { getErrorMessage } from "../../../../Common/ErrorHandlingUtils";
import * as Logger from "../../../../Common/Logger";
export interface InMemoryContentProviderParams {
[path: string]: { readonly: boolean; content: IContent<FileType> };
}
// Nteract relies on `errno` property to figure out the kind of failure
// That's why we need a custom wrapper around Error to include `errno` property
class InMemoryContentProviderError extends Error {
constructor(error: string, public errno: number = InMemoryContentProvider.SelfErrorCode) {
super(error);
}
}
export class InMemoryContentProvider implements IContentProvider {
public static readonly SelfErrorCode = 666;
constructor(private params: InMemoryContentProviderParams) {}
public remove(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "remove");
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public get(_config: ServerConfig, uri: string): Observable<AjaxResponse> {
const item = this.params[uri];
if (item) {
return of(this.createSuccessAjaxResponse(HttpStatusCodes.OK, item.content));
}
return this.errorResponse(`${uri} not found`, "get");
}
public update(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "update");
}
public create(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "create");
}
public save<FT extends FileType>(
_config: ServerConfig, // eslint-disable-line @typescript-eslint/no-unused-vars
uri: string,
model: Partial<IContent<FT>>
): Observable<AjaxResponse> {
const item = this.params[uri];
if (item) {
if (!item.readonly) {
Object.assign(item.content, model);
}
return of(this.createSuccessAjaxResponse(HttpStatusCodes.OK, item.content));
}
return this.errorResponse(`${uri} not found`, "save");
}
public listCheckpoints(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "listCheckpoints");
}
public createCheckpoint(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "createCheckpoint");
}
public deleteCheckpoint(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "deleteCheckpoint");
}
public restoreFromCheckpoint(): Observable<AjaxResponse> {
return this.errorResponse("Not implemented", "restoreFromCheckpoint");
}
private errorResponse(message: string, functionName: string): Observable<AjaxResponse> {
const error = new InMemoryContentProviderError(message);
Logger.logError(error.message, `InMemoryContentProvider/${functionName}`, error.errno);
return of(this.createErrorAjaxResponse(error));
}
private createSuccessAjaxResponse(status: number, content: IContent<FileType>): AjaxResponse {
return {
originalEvent: new Event("no-op"),
xhr: new XMLHttpRequest(),
request: {},
status,
response: content ? content : undefined,
responseText: content ? JSON.stringify(content) : undefined,
responseType: "json",
};
}
private createErrorAjaxResponse(error: InMemoryContentProviderError): AjaxResponse {
return {
originalEvent: new Event("no-op"),
xhr: new XMLHttpRequest(),
request: {},
status: error.errno,
response: error,
responseText: getErrorMessage(error),
responseType: "json",
};
}
}

View File

@@ -0,0 +1,15 @@
// memory://<path>
// Custom scheme for in memory content
export const ContentUriPattern = /memory:\/\/([^/]*)/;
export function fromContentUri(contentUri: string): undefined | string {
const matches = contentUri.match(ContentUriPattern);
if (matches && matches.length > 1) {
return matches[1];
}
return undefined;
}
export function toContentUri(path: string): string {
return `memory://${path}`;
}

View File

@@ -1,11 +1,17 @@
import { ServerConfig, IContentProvider, FileType, IContent, IGetParams } from "@nteract/core"; import { FileType, IContent, IContentProvider, IGetParams, ServerConfig } from "@nteract/core";
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { AjaxResponse } from "rxjs/ajax"; import { AjaxResponse } from "rxjs/ajax";
import { GitHubContentProvider } from "../../../GitHub/GitHubContentProvider"; import { GitHubContentProvider } from "../../../GitHub/GitHubContentProvider";
import * as GitHubUtils from "../../../Utils/GitHubUtils"; import * as GitHubUtils from "../../../Utils/GitHubUtils";
import { InMemoryContentProvider } from "./ContentProviders/InMemoryContentProvider";
import * as InMemoryContentProviderUtils from "./ContentProviders/InMemoryContentProviderUtils";
export class NotebookContentProvider implements IContentProvider { export class NotebookContentProvider implements IContentProvider {
constructor(private gitHubContentProvider: GitHubContentProvider, private jupyterContentProvider: IContentProvider) {} constructor(
private inMemoryContentProvider: InMemoryContentProvider,
private gitHubContentProvider: GitHubContentProvider,
private jupyterContentProvider: IContentProvider
) {}
public remove(serverConfig: ServerConfig, path: string): Observable<AjaxResponse> { public remove(serverConfig: ServerConfig, path: string): Observable<AjaxResponse> {
return this.getContentProvider(path).remove(serverConfig, path); return this.getContentProvider(path).remove(serverConfig, path);
@@ -60,6 +66,10 @@ export class NotebookContentProvider implements IContentProvider {
} }
private getContentProvider(path: string): IContentProvider { private getContentProvider(path: string): IContentProvider {
if (InMemoryContentProviderUtils.fromContentUri(path)) {
return this.inMemoryContentProvider;
}
if (GitHubUtils.fromContentUri(path)) { if (GitHubUtils.fromContentUri(path)) {
return this.gitHubContentProvider; return this.gitHubContentProvider;
} }

View File

@@ -22,13 +22,14 @@ import { getFullName } from "../../Utils/UserUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { ContextualPaneBase } from "../Panes/ContextualPaneBase"; import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane";
// import { GitHubReposPane } from "../Panes/GitHubReposPane";
import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane"; import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { InMemoryContentProvider } from "./NotebookComponent/ContentProviders/InMemoryContentProvider";
import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider"; import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider";
import { SnapshotRequest } from "./NotebookComponent/types"; import { SnapshotRequest } from "./NotebookComponent/types";
import { NotebookContainerClient } from "./NotebookContainerClient"; import { NotebookContainerClient } from "./NotebookContainerClient";
import { NotebookContentClient } from "./NotebookContentClient"; import { NotebookContentClient } from "./NotebookContentClient";
import { SchemaAnalyzerNotebook } from "./SchemaAnalyzer/SchemaAnalyzerUtils";
type NotebookPaneContent = string | ImmutableNotebook; type NotebookPaneContent = string | ImmutableNotebook;
@@ -50,6 +51,7 @@ export default class NotebookManager {
public notebookClient: NotebookContainerClient; public notebookClient: NotebookContainerClient;
public notebookContentClient: NotebookContentClient; public notebookContentClient: NotebookContentClient;
private inMemoryContentProvider: InMemoryContentProvider;
private gitHubContentProvider: GitHubContentProvider; private gitHubContentProvider: GitHubContentProvider;
public gitHubOAuthService: GitHubOAuthService; public gitHubOAuthService: GitHubOAuthService;
public gitHubClient: GitHubClient; public gitHubClient: GitHubClient;
@@ -63,12 +65,20 @@ export default class NotebookManager {
this.gitHubOAuthService = new GitHubOAuthService(this.junoClient); this.gitHubOAuthService = new GitHubOAuthService(this.junoClient);
this.gitHubClient = new GitHubClient(this.onGitHubClientError); this.gitHubClient = new GitHubClient(this.onGitHubClientError);
this.inMemoryContentProvider = new InMemoryContentProvider({
[SchemaAnalyzerNotebook.path]: {
readonly: true,
content: SchemaAnalyzerNotebook,
},
});
this.gitHubContentProvider = new GitHubContentProvider({ this.gitHubContentProvider = new GitHubContentProvider({
gitHubClient: this.gitHubClient, gitHubClient: this.gitHubClient,
promptForCommitMsg: this.promptForCommitMsg, promptForCommitMsg: this.promptForCommitMsg,
}); });
this.notebookContentProvider = new NotebookContentProvider( this.notebookContentProvider = new NotebookContentProvider(
this.inMemoryContentProvider,
this.gitHubContentProvider, this.gitHubContentProvider,
contents.JupyterContentProvider contents.JupyterContentProvider
); );

View File

@@ -53,7 +53,7 @@ export class PromptPure extends React.Component<Props> {
} }
} }
const makeMapStateToProps = (state: CdbAppState, ownProps: ComponentProps): ((state: CdbAppState) => StateProps) => { const makeMapStateToProps = (_state: CdbAppState, ownProps: ComponentProps): ((state: CdbAppState) => StateProps) => {
const mapStateToProps = (state: CdbAppState) => { const mapStateToProps = (state: CdbAppState) => {
const { contentRef, id } = ownProps; const { contentRef, id } = ownProps;
const model = selectors.model(state, { contentRef }); const model = selectors.model(state, { contentRef });

View File

@@ -1,10 +1,9 @@
import { AppState, ContentRef, DocumentRecordProps, selectors } from "@nteract/core";
import { RecordOf } from "immutable";
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import "./CellLabeler.less"; import "./CellLabeler.less";
import { AppState, ContentRef, selectors, DocumentRecordProps } from "@nteract/core";
import { RecordOf } from "immutable";
interface ComponentProps { interface ComponentProps {
id: string; id: string;
contentRef: ContentRef; // TODO: Make this per contentRef? contentRef: ContentRef; // TODO: Make this per contentRef?
@@ -29,7 +28,7 @@ class CellLabeler extends React.Component<ComponentProps & StateProps> {
} }
} }
const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state: AppState) => StateProps) => { const makeMapStateToProps = (_state: AppState, ownProps: ComponentProps): ((state: AppState) => StateProps) => {
const mapStateToProps = (state: AppState) => { const mapStateToProps = (state: AppState) => {
const model = selectors.model(state, { contentRef: ownProps.contentRef }); const model = selectors.model(state, { contentRef: ownProps.contentRef });
const cellOrder = selectors.notebook.cellOrder(model as RecordOf<DocumentRecordProps>); const cellOrder = selectors.notebook.cellOrder(model as RecordOf<DocumentRecordProps>);

View File

@@ -1,4 +1,4 @@
.schemaAnalyzerComponent { .schemaAnalyzer {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;

View File

@@ -1,22 +1,26 @@
import { FontIcon, PrimaryButton, Spinner, SpinnerSize, Stack, Text, TextField } from "@fluentui/react"; import { Spinner, SpinnerSize, Stack } from "@fluentui/react";
import { ImmutableOutput } from "@nteract/commutable"; import { ImmutableExecuteResult, ImmutableOutput } from "@nteract/commutable";
import { actions, AppState, ContentRef, KernelRef, selectors } from "@nteract/core"; import { actions, AppState, ContentRef, KernelRef, selectors } from "@nteract/core";
import Immutable from "immutable"; import Immutable from "immutable";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import loadTransform from "../NotebookComponent/loadTransform"; import loadTransform from "../NotebookComponent/loadTransform";
import SandboxOutputs from "../NotebookRenderer/outputs/SandboxOutputs"; import SandboxOutputs from "../NotebookRenderer/outputs/SandboxOutputs";
import "./SchemaAnalyzerComponent.less"; import "./SchemaAnalyzer.less";
import { DefaultFilter, DefaultSampleSize, SchemaAnalyzerHeader } from "./SchemaAnalyzerHeader";
import { SchemaAnalyzerSplashScreen } from "./SchemaAnalyzerSplashScreen";
interface SchemaAnalyzerComponentPureProps { interface SchemaAnalyzerPureProps {
contentRef: ContentRef; contentRef: ContentRef;
kernelRef: KernelRef; kernelRef: KernelRef;
databaseId: string; databaseId: string;
collectionId: string; collectionId: string;
} }
interface SchemaAnalyzerComponentDispatchProps { interface SchemaAnalyzerDispatchProps {
runCell: (contentRef: ContentRef, cellId: string) => void; runCell: (contentRef: ContentRef, cellId: string) => void;
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void; addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => void;
updateCell: (text: string, id: string, contentRef: ContentRef) => void; updateCell: (text: string, id: string, contentRef: ContentRef) => void;
@@ -24,25 +28,23 @@ interface SchemaAnalyzerComponentDispatchProps {
type OutputType = "rich" | "json"; type OutputType = "rich" | "json";
interface SchemaAnalyzerComponentState { interface SchemaAnalyzerState {
outputType: OutputType; outputType: OutputType;
filter?: string;
isFiltering: boolean; isFiltering: boolean;
sampleSize: string;
} }
type SchemaAnalyzerComponentProps = SchemaAnalyzerComponentPureProps & type SchemaAnalyzerProps = SchemaAnalyzerPureProps & StateProps & SchemaAnalyzerDispatchProps;
StateProps &
SchemaAnalyzerComponentDispatchProps;
export class SchemaAnalyzerComponent extends React.Component< export class SchemaAnalyzer extends React.Component<SchemaAnalyzerProps, SchemaAnalyzerState> {
SchemaAnalyzerComponentProps, private clickAnalyzeTelemetryStartKey: number;
SchemaAnalyzerComponentState
> { constructor(props: SchemaAnalyzerProps) {
constructor(props: SchemaAnalyzerComponentProps) {
super(props); super(props);
this.state = { this.state = {
outputType: "rich", outputType: "rich",
isFiltering: false, isFiltering: false,
sampleSize: DefaultSampleSize,
}; };
} }
@@ -50,34 +52,59 @@ export class SchemaAnalyzerComponent extends React.Component<
loadTransform(this.props); loadTransform(this.props);
} }
private onFilterTextFieldChange = ( private onAnalyzeButtonClick = (filter: string = DefaultFilter, sampleSize: string = this.state.sampleSize) => {
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
this.setState({
filter: newValue,
});
};
private onAnalyzeButtonClick = () => {
const query = { const query = {
command: "listSchema", command: "listSchema",
database: this.props.databaseId, database: this.props.databaseId,
collection: this.props.collectionId, collection: this.props.collectionId,
outputType: this.state.outputType, outputType: this.state.outputType,
filter: this.state.filter, filter,
sampleSize,
}; };
if (this.state.filter) { this.setState({
this.setState({ isFiltering: true,
isFiltering: true, });
});
}
this.props.updateCell(JSON.stringify(query), this.props.firstCellId, this.props.contentRef); this.props.updateCell(JSON.stringify(query), this.props.firstCellId, this.props.contentRef);
this.clickAnalyzeTelemetryStartKey = traceStart(Action.SchemaAnalyzerClickAnalyze, {
database: this.props.databaseId,
collection: this.props.collectionId,
sampleSize,
});
this.props.runCell(this.props.contentRef, this.props.firstCellId); this.props.runCell(this.props.contentRef, this.props.firstCellId);
}; };
private traceClickAnalyzeComplete = (kernelStatus: string, outputs: Immutable.List<ImmutableOutput>) => {
/**
* CosmosMongoKernel always returns 1st output as "text/html"
* This output can be an error stack or information about how many documents were sampled
*/
let firstTextHtmlOutput: string;
if (outputs.size > 0 && outputs.get(0).output_type === "execute_result") {
const executeResult = outputs.get(0) as ImmutableExecuteResult;
firstTextHtmlOutput = executeResult.data["text/html"];
}
const data = {
database: this.props.databaseId,
collection: this.props.collectionId,
firstTextHtmlOutput,
sampleSize: this.state.sampleSize,
numOfOutputs: outputs.size,
kernelStatus,
};
// Only in cases where CosmosMongoKernel runs into an error we get a single output
if (outputs.size === 1) {
traceFailure(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey);
} else {
traceSuccess(Action.SchemaAnalyzerClickAnalyze, data, this.clickAnalyzeTelemetryStartKey);
}
};
render(): JSX.Element { render(): JSX.Element {
const { firstCellId: id, contentRef, kernelStatus, outputs } = this.props; const { firstCellId: id, contentRef, kernelStatus, outputs } = this.props;
if (!id) { if (!id) {
@@ -86,31 +113,22 @@ export class SchemaAnalyzerComponent extends React.Component<
const isKernelBusy = kernelStatus === "busy"; const isKernelBusy = kernelStatus === "busy";
const isKernelIdle = kernelStatus === "idle"; const isKernelIdle = kernelStatus === "idle";
const showSchemaOutput = isKernelIdle && outputs.size > 0; const showSchemaOutput = isKernelIdle && outputs?.size > 0;
if (showSchemaOutput && this.clickAnalyzeTelemetryStartKey) {
this.traceClickAnalyzeComplete(kernelStatus, outputs);
this.clickAnalyzeTelemetryStartKey = undefined;
}
return ( return (
<div className="schemaAnalyzerComponent"> <div className="schemaAnalyzer">
<Stack horizontalAlign="center" tokens={{ childrenGap: 20, padding: 20 }}> <Stack tokens={{ childrenGap: 20, padding: 20 }}>
<Stack.Item grow styles={{ root: { display: "contents" } }}> <SchemaAnalyzerHeader
<Stack horizontal tokens={{ childrenGap: 20 }} styles={{ root: { width: "100%" } }}> isKernelIdle={isKernelIdle}
<Stack.Item grow align="end"> isKernelBusy={isKernelBusy}
<TextField onSampleSizeUpdated={(sampleSize) => this.setState({ sampleSize })}
value={this.state.filter} onAnalyzeButtonClick={this.onAnalyzeButtonClick}
onChange={this.onFilterTextFieldChange} />
label="Filter"
placeholder="{ field: 'value' }"
disabled={!isKernelIdle}
/>
</Stack.Item>
<Stack.Item align="end">
<PrimaryButton
text={isKernelBusy ? "Analyzing..." : "Analyze"}
onClick={this.onAnalyzeButtonClick}
disabled={!isKernelIdle}
/>
</Stack.Item>
</Stack>
</Stack.Item>
{showSchemaOutput ? ( {showSchemaOutput ? (
<SandboxOutputs <SandboxOutputs
@@ -120,32 +138,13 @@ export class SchemaAnalyzerComponent extends React.Component<
outputClassName="schema-analyzer-cell-output" outputClassName="schema-analyzer-cell-output"
/> />
) : this.state.isFiltering ? ( ) : this.state.isFiltering ? (
<Stack.Item> <Spinner styles={{ root: { marginTop: 40 } }} size={SpinnerSize.large} />
{isKernelBusy && <Spinner styles={{ root: { marginTop: 40 } }} size={SpinnerSize.large} />}
</Stack.Item>
) : ( ) : (
<> <SchemaAnalyzerSplashScreen
<Stack.Item> isKernelIdle={isKernelIdle}
<FontIcon iconName="Chart" style={{ fontSize: 100, color: "#43B1E5", marginTop: 40 }} /> isKernelBusy={isKernelBusy}
</Stack.Item> onAnalyzeButtonClick={this.onAnalyzeButtonClick}
<Stack.Item> />
<Text variant="xxLarge">Explore your schema</Text>
</Stack.Item>
<Stack.Item>
<Text variant="large">
Quickly visualize your schema to infer the frequency, types and ranges of fields in your data set.
</Text>
</Stack.Item>
<Stack.Item>
<PrimaryButton
styles={{ root: { fontSize: 18, padding: 30 } }}
text={isKernelBusy ? "Analyzing..." : "Analyze Schema"}
onClick={this.onAnalyzeButtonClick}
disabled={kernelStatus !== "idle"}
/>
</Stack.Item>
<Stack.Item>{isKernelBusy && <Spinner size={SpinnerSize.large} />}</Stack.Item>
</>
)} )}
</Stack> </Stack>
</div> </div>
@@ -229,4 +228,4 @@ const makeMapDispatchToProps = () => {
return mapDispatchToProps; return mapDispatchToProps;
}; };
export default connect(makeMapStateToProps, makeMapDispatchToProps)(SchemaAnalyzerComponent); export default connect(makeMapStateToProps, makeMapDispatchToProps)(SchemaAnalyzer);

View File

@@ -0,0 +1,48 @@
import { actions, createContentRef, createKernelRef, KernelRef } from "@nteract/core";
import * as React from "react";
import { Provider } from "react-redux";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import {
NotebookComponentBootstrapper,
NotebookComponentBootstrapperOptions,
} from "../NotebookComponent/NotebookComponentBootstrapper";
import SchemaAnalyzer from "./SchemaAnalyzer";
import { SchemaAnalyzerNotebook } from "./SchemaAnalyzerUtils";
export class SchemaAnalyzerAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
public parameters: unknown;
private kernelRef: KernelRef;
constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) {
super(options);
if (!this.contentRef) {
this.contentRef = createContentRef();
this.kernelRef = createKernelRef();
this.getStore().dispatch(
actions.fetchContent({
filepath: SchemaAnalyzerNotebook.path,
params: {},
kernelRef: this.kernelRef,
contentRef: this.contentRef,
})
);
}
}
public renderComponent(): JSX.Element {
const props = {
contentRef: this.contentRef,
kernelRef: this.kernelRef,
databaseId: this.databaseId,
collectionId: this.collectionId,
};
return (
<Provider store={this.getStore()}>
<SchemaAnalyzer {...props} />;
</Provider>
);
}
}

View File

@@ -0,0 +1,101 @@
import {
DefaultButton,
Icon,
IRenderFunction,
ITextFieldProps,
PrimaryButton,
Stack,
TextField,
TooltipHost,
} from "@fluentui/react";
import * as React from "react";
type SchemaAnalyzerHeaderProps = {
isKernelIdle: boolean;
isKernelBusy: boolean;
onSampleSizeUpdated: (sampleSize: string) => void;
onAnalyzeButtonClick: (filter: string, sampleSize: string) => void;
};
export const DefaultFilter = "";
export const DefaultSampleSize = "1000";
const FilterPlaceholder = "{ field: 'value' }";
const SampleSizePlaceholder = "1000";
const MinSampleSize = 1;
const MaxSampleSize = 5000;
export const SchemaAnalyzerHeader = ({
isKernelIdle,
isKernelBusy,
onSampleSizeUpdated,
onAnalyzeButtonClick,
}: SchemaAnalyzerHeaderProps): JSX.Element => {
const [filter, setFilter] = React.useState<string>(DefaultFilter);
const [sampleSize, setSampleSize] = React.useState<string>(DefaultSampleSize);
return (
<Stack horizontal tokens={{ childrenGap: 10 }}>
<Stack.Item grow>
<TextField
value={filter}
onChange={(event, newValue) => setFilter(newValue)}
label="Filter"
placeholder={FilterPlaceholder}
disabled={!isKernelIdle}
/>
</Stack.Item>
<Stack.Item>
<TextField
value={sampleSize}
onChange={(event, newValue) => {
const num = Number(newValue);
if (!newValue || (num >= MinSampleSize && num <= MaxSampleSize)) {
setSampleSize(newValue);
onSampleSizeUpdated(newValue);
}
}}
label="Sample size"
onRenderLabel={onSampleSizeWrapDefaultLabelRenderer}
placeholder={SampleSizePlaceholder}
disabled={!isKernelIdle}
/>
</Stack.Item>
<Stack.Item align="end">
<PrimaryButton
text={isKernelBusy ? "Analyzing..." : "Analyze"}
onClick={() => {
const sampleSizeToUse = sampleSize || DefaultSampleSize;
setSampleSize(sampleSizeToUse);
onAnalyzeButtonClick(filter, sampleSizeToUse);
}}
disabled={!isKernelIdle}
styles={{ root: { width: 120 } }}
/>
</Stack.Item>
<Stack.Item align="end">
<DefaultButton
text="Reset"
disabled={!isKernelIdle}
onClick={() => {
setFilter(DefaultFilter);
setSampleSize(DefaultSampleSize);
}}
/>
</Stack.Item>
</Stack>
);
};
const onSampleSizeWrapDefaultLabelRenderer = (
props: ITextFieldProps,
defaultRender: IRenderFunction<ITextFieldProps>
): JSX.Element => {
return (
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 4 }}>
<span>{defaultRender(props)}</span>
<TooltipHost content={`Number of documents to sample between ${MinSampleSize} and ${MaxSampleSize}`}>
<Icon iconName="Info" ariaLabel="Info" />
</TooltipHost>
</Stack>
);
};

View File

@@ -0,0 +1,39 @@
import { FontIcon, PrimaryButton, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import * as React from "react";
type SchemaAnalyzerSplashScreenProps = {
isKernelIdle: boolean;
isKernelBusy: boolean;
onAnalyzeButtonClick: () => void;
};
export const SchemaAnalyzerSplashScreen = ({
isKernelIdle,
isKernelBusy,
onAnalyzeButtonClick,
}: SchemaAnalyzerSplashScreenProps): JSX.Element => {
return (
<Stack horizontalAlign="center" tokens={{ childrenGap: 20, padding: 20 }}>
<Stack.Item>
<FontIcon iconName="Chart" style={{ fontSize: 100, color: "#43B1E5", marginTop: 40 }} />
</Stack.Item>
<Stack.Item>
<Text variant="xxLarge">Explore your schema</Text>
</Stack.Item>
<Stack.Item>
<Text variant="large">
Quickly visualize your schema to infer the frequency, types and ranges of fields in your data set.
</Text>
</Stack.Item>
<Stack.Item>
<PrimaryButton
styles={{ root: { fontSize: 18, padding: 30 } }}
text={isKernelBusy ? "Analyzing..." : "Analyze Schema"}
onClick={() => onAnalyzeButtonClick()}
disabled={!isKernelIdle}
/>
</Stack.Item>
<Stack.Item>{isKernelBusy && <Spinner size={SpinnerSize.large} />}</Stack.Item>
</Stack>
);
};

View File

@@ -0,0 +1,44 @@
import { Notebook } from "@nteract/commutable";
import { IContent } from "@nteract/types";
import * as InMemoryContentProviderUtils from "../NotebookComponent/ContentProviders/InMemoryContentProviderUtils";
const notebookName = "schema-analyzer-component-notebook.ipynb";
const notebookPath = InMemoryContentProviderUtils.toContentUri(notebookName);
const notebook: Notebook = {
cells: [
{
cell_type: "code",
metadata: {},
execution_count: 0,
outputs: [],
source: "",
},
],
metadata: {
kernelspec: {
displayName: "Mongo",
language: "mongocli",
name: "mongo",
},
language_info: {
file_extension: "ipynb",
mimetype: "application/json",
name: "mongo",
version: "1.0",
},
},
nbformat: 4,
nbformat_minor: 4,
};
export const SchemaAnalyzerNotebook: IContent<"notebook"> = {
name: notebookName,
path: notebookPath,
type: "notebook",
writable: true,
created: "",
last_modified: "",
mimetype: "application/x-ipynb+json",
content: notebook,
format: "json",
};

View File

@@ -1,88 +0,0 @@
import { Notebook } from "@nteract/commutable";
import { actions, createContentRef, createKernelRef, IContent, KernelRef } from "@nteract/core";
import * as React from "react";
import { Provider } from "react-redux";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import {
NotebookComponentBootstrapper,
NotebookComponentBootstrapperOptions,
} from "../NotebookComponent/NotebookComponentBootstrapper";
import SchemaAnalyzerComponent from "./SchemaAnalyzerComponent";
export class SchemaAnalyzerComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
public parameters: unknown;
private kernelRef: KernelRef;
constructor(options: NotebookComponentBootstrapperOptions, private databaseId: string, private collectionId: string) {
super(options);
if (!this.contentRef) {
this.contentRef = createContentRef();
this.kernelRef = createKernelRef();
const notebook: Notebook = {
cells: [
{
cell_type: "code",
metadata: {},
execution_count: 0,
outputs: [],
source: "",
},
],
metadata: {
kernelspec: {
displayName: "Mongo",
language: "mongocli",
name: "mongo",
},
language_info: {
file_extension: "ipynb",
mimetype: "application/json",
name: "mongo",
version: "1.0",
},
},
nbformat: 4,
nbformat_minor: 4,
};
const model: IContent<"notebook"> = {
name: "schema-analyzer-component-notebook.ipynb",
path: "schema-analyzer-component-notebook.ipynb",
type: "notebook",
writable: true,
created: "",
last_modified: "",
mimetype: "application/x-ipynb+json",
content: notebook,
format: "json",
};
// Request fetching notebook content
this.getStore().dispatch(
actions.fetchContentFulfilled({
filepath: model.path,
model,
kernelRef: this.kernelRef,
contentRef: this.contentRef,
})
);
}
}
public renderComponent(): JSX.Element {
const props = {
contentRef: this.contentRef,
kernelRef: this.kernelRef,
databaseId: this.databaseId,
collectionId: this.collectionId,
};
return (
<Provider store={this.getStore()}>
<SchemaAnalyzerComponent {...props} />;
</Provider>
);
}
}

View File

@@ -17,7 +17,6 @@ describe("OpenActions", () => {
explorer.onNewCollectionClicked = jest.fn(); explorer.onNewCollectionClicked = jest.fn();
explorer.cassandraAddCollectionPane = {} as CassandraAddCollectionPane; explorer.cassandraAddCollectionPane = {} as CassandraAddCollectionPane;
explorer.cassandraAddCollectionPane.open = jest.fn(); explorer.cassandraAddCollectionPane.open = jest.fn();
explorer.closeAllPanes = () => {};
database = { database = {
id: ko.observable("db"), id: ko.observable("db"),

View File

@@ -140,19 +140,16 @@ function openPane(action: ActionContracts.OpenPane, explorer: Explorer) {
action.paneKind === ActionContracts.PaneKind.AddCollection || action.paneKind === ActionContracts.PaneKind.AddCollection ||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection] (<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.AddCollection]
) { ) {
explorer.closeAllPanes();
explorer.onNewCollectionClicked(); explorer.onNewCollectionClicked();
} else if ( } else if (
action.paneKind === ActionContracts.PaneKind.CassandraAddCollection || action.paneKind === ActionContracts.PaneKind.CassandraAddCollection ||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection] (<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.CassandraAddCollection]
) { ) {
explorer.closeAllPanes();
explorer.cassandraAddCollectionPane.open(); explorer.cassandraAddCollectionPane.open();
} else if ( } else if (
action.paneKind === ActionContracts.PaneKind.GlobalSettings || action.paneKind === ActionContracts.PaneKind.GlobalSettings ||
(<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings] (<any>action).paneKind === ActionContracts.PaneKind[ActionContracts.PaneKind.GlobalSettings]
) { ) {
explorer.closeAllPanes();
explorer.openSettingPane(); explorer.openSettingPane();
} }
} }

View File

@@ -25,7 +25,7 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { getCollectionName } from "../../Utils/APITypeUtils"; import { getCollectionName } from "../../Utils/APITypeUtils";
import { isCapabilityEnabled } from "../../Utils/CapabilityUtils"; import { isCapabilityEnabled, isServerlessAccount } from "../../Utils/CapabilityUtils";
import { getUpsellMessage } from "../../Utils/PricingUtils"; import { getUpsellMessage } from "../../Utils/PricingUtils";
import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent"; import { CollapsibleSectionComponent } from "../Controls/CollapsiblePanel/CollapsibleSectionComponent";
import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput"; import { ThroughputInput } from "../Controls/ThroughputInput/ThroughputInput";
@@ -179,7 +179,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
/> />
{!this.isServerlessAccount() && ( {!isServerlessAccount() && (
<Stack horizontal> <Stack horizontal>
<Checkbox <Checkbox
label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`} label={`Share throughput across ${getCollectionName(true).toLocaleLowerCase()}`}
@@ -204,14 +204,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack> </Stack>
)} )}
{!this.isServerlessAccount() && this.state.isSharedThroughputChecked && ( {!isServerlessAccount() && this.state.isSharedThroughputChecked && (
<ThroughputInput <ThroughputInput
showFreeTierExceedThroughputTooltip={ showFreeTierExceedThroughputTooltip={
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated() this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
} }
isDatabase={true} isDatabase={true}
isAutoscaleSelected={this.isNewDatabaseAutoscale}
throughput={this.newDatabaseThroughput}
isSharded={this.state.isSharded} isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)} setThroughputValue={(throughput: number) => (this.newDatabaseThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)} setIsAutoscale={(isAutoscale: boolean) => (this.isNewDatabaseAutoscale = isAutoscale)}
@@ -391,7 +389,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
onChange={(event: React.ChangeEvent<HTMLInputElement>) => { onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
if ( if (
userContext.apiType !== "Mongo" && userContext.apiType !== "Mongo" &&
this.state.partitionKey === "" && !this.state.partitionKey &&
!event.target.value.startsWith("/") !event.target.value.startsWith("/")
) { ) {
this.setState({ partitionKey: "/" + event.target.value }); this.setState({ partitionKey: "/" + event.target.value });
@@ -403,7 +401,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack> </Stack>
)} )}
{!this.isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && ( {!isServerlessAccount() && !this.state.createNewDatabase && this.isSelectedDatabaseSharedThroughput() && (
<Stack horizontal verticalAlign="center"> <Stack horizontal verticalAlign="center">
<Checkbox <Checkbox
label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`} label={`Provision dedicated throughput for this ${getCollectionName().toLocaleLowerCase()}`}
@@ -437,8 +435,6 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated() this.isFreeTierAccount() && !this.props.explorer.isFirstResourceCreated()
} }
isDatabase={false} isDatabase={false}
isAutoscaleSelected={this.isCollectionAutoscale}
throughput={this.collectionThroughput}
isSharded={this.state.isSharded} isSharded={this.state.isSharded}
setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)} setThroughputValue={(throughput: number) => (this.collectionThroughput = throughput)}
setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)} setIsAutoscale={(isAutoscale: boolean) => (this.isCollectionAutoscale = isAutoscale)}
@@ -745,14 +741,8 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return userContext.databaseAccount?.properties?.enableFreeTier; return userContext.databaseAccount?.properties?.enableFreeTier;
} }
private isServerlessAccount(): boolean {
return userContext.databaseAccount.properties?.capabilities?.some(
(capability) => capability.name === Constants.CapabilityNames.EnableServerless
);
}
private getSharedThroughputDefault(): boolean { private getSharedThroughputDefault(): boolean {
return userContext.subscriptionType !== SubscriptionType.EA && !this.isServerlessAccount(); return userContext.subscriptionType !== SubscriptionType.EA && !isServerlessAccount();
} }
private getFreeTierIndexingText(): string { private getFreeTierIndexingText(): string {
@@ -790,7 +780,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
private shouldShowCollectionThroughputInput(): boolean { private shouldShowCollectionThroughputInput(): boolean {
if (this.isServerlessAccount()) { if (isServerlessAccount()) {
return false; return false;
} }
@@ -820,7 +810,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return false; return false;
} }
if (this.isServerlessAccount()) { if (isServerlessAccount()) {
return false; return false;
} }
@@ -911,6 +901,10 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
} }
private getAnalyticalStorageTtl(): number { private getAnalyticalStorageTtl(): number {
if (!this.isSynapseLinkEnabled()) {
return undefined;
}
if (!this.shouldShowAnalyticalStoreOptions()) { if (!this.shouldShowAnalyticalStoreOptions()) {
return undefined; return undefined;
} }

View File

@@ -1,10 +1,9 @@
import { Checkbox, Text, TextField } from "@fluentui/react"; import { Checkbox, Stack, Text, TextField } from "@fluentui/react";
import React, { FunctionComponent, useEffect, useState } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
import * as Constants from "../../../Common/Constants"; import * as Constants from "../../../Common/Constants";
import { createDatabase } from "../../../Common/dataAccess/createDatabase"; import { createDatabase } from "../../../Common/dataAccess/createDatabase";
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip"; import { InfoTooltip } from "../../../Common/Tooltip/InfoTooltip";
import { configContext, Platform } from "../../../ConfigContext";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import { SubscriptionType } from "../../../Contracts/SubscriptionType"; import { SubscriptionType } from "../../../Contracts/SubscriptionType";
import * as SharedConstants from "../../../Shared/Constants"; import * as SharedConstants from "../../../Shared/Constants";
@@ -12,7 +11,8 @@ import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryCons
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils"; import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../../Utils/PricingUtils"; import { isServerlessAccount } from "../../../Utils/CapabilityUtils";
import { getUpsellMessage } from "../../../Utils/PricingUtils";
import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput"; import { ThroughputInput } from "../../Controls/ThroughputInput/ThroughputInput";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent"; import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
@@ -29,17 +29,10 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
closePanel, closePanel,
openNotificationConsole, openNotificationConsole,
}: AddDatabasePaneProps) => { }: AddDatabasePaneProps) => {
let throughput: number;
let isAutoscaleSelected: boolean;
let isCostAcknowledged: boolean;
const { subscriptionType } = userContext; const { subscriptionType } = userContext;
const getSharedThroughputDefault = !(subscriptionType === SubscriptionType.EA || container.isServerlessEnabled());
const _isAutoPilotSelectedAndWhatTier = (): DataModels.AutoPilotCreationSettings => {
if (isAutoPilotSelected && maxAutoPilotThroughputSet) {
return {
maxThroughput: maxAutoPilotThroughputSet * 1,
};
}
return undefined;
};
const isCassandraAccount: boolean = userContext.apiType === "Cassandra"; const isCassandraAccount: boolean = userContext.apiType === "Cassandra";
const databaseLabel: string = isCassandraAccount ? "keyspace" : "database"; const databaseLabel: string = isCassandraAccount ? "keyspace" : "database";
const collectionsLabel: string = isCassandraAccount ? "tables" : "collections"; const collectionsLabel: string = isCassandraAccount ? "tables" : "collections";
@@ -52,61 +45,13 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
} is a logical container of one or more ${isCassandraAccount ? "tables" : "collections"}`; } is a logical container of one or more ${isCassandraAccount ? "tables" : "collections"}`;
const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`; const databaseLevelThroughputTooltipText = `Provisioned throughput at the ${databaseLabel} level will be shared across all ${collectionsLabel} within the ${databaseLabel}.`;
const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(getSharedThroughputDefault); const [databaseCreateNewShared, setDatabaseCreateNewShared] = useState<boolean>(
const [formErrorsDetails, setFormErrorsDetails] = useState<string>(); subscriptionType !== SubscriptionType.EA && !isServerlessAccount()
);
const [formErrors, setFormErrors] = useState<string>(""); const [formErrors, setFormErrors] = useState<string>("");
const [isAutoPilotSelected, setIsAutoPilotSelected] = useState<boolean>(container.isAutoscaleDefaultEnabled());
const throughputDefaults = container.collectionCreationDefaults.throughput;
const [throughput, setThroughput] = useState<number>(
isAutoPilotSelected ? AutoPilotUtils.minAutoPilotThroughput : throughputDefaults.shared
);
const [throughputSpendAck, setThroughputSpendAck] = useState<boolean>(false);
const canRequestSupport = () => {
if (
configContext.platform !== Platform.Emulator &&
!userContext.isTryCosmosDBSubscription &&
configContext.platform !== Platform.Portal
) {
const offerThroughput: number = throughput;
return offerThroughput <= 100000;
}
return false;
};
const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
const upsellMessage: string = PricingUtils.getUpsellMessage(
userContext.portalEnv,
isFreeTierAccount,
container.isFirstResourceCreated(),
false
);
const upsellAnchorUrl: string = isFreeTierAccount ? Constants.Urls.freeTierInformation : Constants.Urls.cosmosPricing;
const upsellAnchorText: string = isFreeTierAccount ? "Learn more" : "More details";
const maxAutoPilotThroughputSet = AutoPilotUtils.minAutoPilotThroughput;
const canConfigureThroughput = !container.isServerlessEnabled();
const showUpsellMessage = () => {
if (container.isServerlessEnabled()) {
return false;
}
if (isFreeTierAccount) {
return databaseCreateNewShared;
}
return true;
};
const [isExecuting, setIsExecuting] = useState<boolean>(false); const [isExecuting, setIsExecuting] = useState<boolean>(false);
useEffect(() => { const isFreeTierAccount: boolean = userContext.databaseAccount?.properties?.enableFreeTier;
setDatabaseCreateNewShared(getSharedThroughputDefault);
}, [subscriptionType]);
const addDatabasePaneMessage = { const addDatabasePaneMessage = {
database: { database: {
@@ -126,7 +71,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
subscriptionType: SubscriptionType[subscriptionType], subscriptionType: SubscriptionType[subscriptionType],
subscriptionQuotaId: userContext.quotaId, subscriptionQuotaId: userContext.quotaId,
defaultsCheck: { defaultsCheck: {
throughput: throughput, throughput,
flight: userContext.addCollectionFlight, flight: userContext.addCollectionFlight,
}, },
dataExplorerArea: Constants.Areas.ContextualPane, dataExplorerArea: Constants.Areas.ContextualPane,
@@ -139,11 +84,9 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
return; return;
} }
const offerThroughput: number = _computeOfferThroughput();
const addDatabasePaneStartMessage = { const addDatabasePaneStartMessage = {
...addDatabasePaneMessage, ...addDatabasePaneMessage,
offerThroughput, throughput,
}; };
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDatabase, addDatabasePaneStartMessage); const startKey: number = TelemetryProcessor.traceStart(Action.CreateDatabase, addDatabasePaneStartMessage);
setFormErrors(""); setFormErrors("");
@@ -153,18 +96,18 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
databaseId: addDatabasePaneStartMessage.database.id, databaseId: addDatabasePaneStartMessage.database.id,
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared, databaseLevelThroughput: addDatabasePaneStartMessage.database.shared,
}; };
if (isAutoPilotSelected) { if (isAutoscaleSelected) {
createDatabaseParams.autoPilotMaxThroughput = addDatabasePaneStartMessage.offerThroughput; createDatabaseParams.autoPilotMaxThroughput = addDatabasePaneStartMessage.throughput;
} else { } else {
createDatabaseParams.offerThroughput = addDatabasePaneStartMessage.offerThroughput; createDatabaseParams.offerThroughput = addDatabasePaneStartMessage.throughput;
} }
createDatabase(createDatabaseParams).then( createDatabase(createDatabaseParams).then(
() => { () => {
_onCreateDatabaseSuccess(offerThroughput, startKey); _onCreateDatabaseSuccess(throughput, startKey);
}, },
(error: string) => { (error: string) => {
_onCreateDatabaseFailure(error, offerThroughput, startKey); _onCreateDatabaseFailure(error, throughput, startKey);
} }
); );
}; };
@@ -184,7 +127,6 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
setIsExecuting(false); setIsExecuting(false);
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
setFormErrors(errorMessage); setFormErrors(errorMessage);
setFormErrorsDetails(errorMessage);
const addDatabasePaneFailedMessage = { const addDatabasePaneFailedMessage = {
...addDatabasePaneMessage, ...addDatabasePaneMessage,
offerThroughput, offerThroughput,
@@ -194,48 +136,19 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey); TelemetryProcessor.traceFailure(Action.CreateDatabase, addDatabasePaneFailedMessage, startKey);
}; };
const _getThroughput = (): number => {
return isNaN(throughput) ? 0 : Number(throughput);
};
const _computeOfferThroughput = (): number => {
if (!canConfigureThroughput) {
return undefined;
}
return _getThroughput();
};
const _isValid = (): boolean => { const _isValid = (): boolean => {
// TODO add feature flag that disables validation for customers with custom accounts // TODO add feature flag that disables validation for customers with custom accounts
if (isAutoPilotSelected) { if (isAutoscaleSelected) {
const autoPilot = _isAutoPilotSelectedAndWhatTier(); if (!AutoPilotUtils.isValidAutoPilotThroughput(throughput)) {
if (
!autoPilot ||
!autoPilot.maxThroughput ||
!AutoPilotUtils.isValidAutoPilotThroughput(autoPilot.maxThroughput)
) {
setFormErrors( setFormErrors(
`Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput` `Please enter a value greater than ${AutoPilotUtils.minAutoPilotThroughput} for autopilot throughput`
); );
return false; return false;
} }
} }
const throughput = _getThroughput();
if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !throughputSpendAck) { if (throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K && !isCostAcknowledged) {
setFormErrors(`Please acknowledge the estimated daily spend.`); setFormErrors(`Please acknowledge the estimated ${isAutoscaleSelected ? "monthly" : "daily"} spend.`);
return false;
}
const autoscaleThroughput = maxAutoPilotThroughputSet * 1;
if (
isAutoPilotSelected &&
autoscaleThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs100K &&
!throughputSpendAck
) {
setFormErrors(`Please acknowledge the estimated monthly spend.`);
return false; return false;
} }
@@ -250,9 +163,8 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
); );
const props: RightPaneFormProps = { const props: RightPaneFormProps = {
expandConsole: container.expandConsole, expandConsole: openNotificationConsole,
formError: formErrors, formError: formErrors,
formErrorDetail: formErrorsDetails,
isExecuting, isExecuting,
submitButtonText: "OK", submitButtonText: "OK",
onSubmit, onSubmit,
@@ -260,80 +172,65 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
return ( return (
<RightPaneForm {...props}> <RightPaneForm {...props}>
<div className="paneContentContainer" role="dialog" aria-labelledby="databaseTitle"> {!formErrors && isFreeTierAccount && (
{showUpsellMessage && formErrors === "" && ( <PanelInfoErrorComponent
<PanelInfoErrorComponent message={getUpsellMessage(userContext.portalEnv, true, container.isFirstResourceCreated(), true)}
message={upsellMessage} messageType="info"
messageType="info" showErrorDetails={false}
showErrorDetails={false} openNotificationConsole={openNotificationConsole}
openNotificationConsole={openNotificationConsole} link={Constants.Urls.freeTierInformation}
link={upsellAnchorUrl} linkText="Learn more"
linkText={upsellAnchorText} />
)}
<div className="panelMainContent">
<div>
<Stack horizontal>
<span className="mandatoryStar">*</span>
<Text variant="small">{databaseIdLabel}</Text>
<InfoTooltip>{databaseIdTooltipText}</InfoTooltip>
</Stack>
<TextField
id="database-id"
type="text"
aria-required="true"
autoComplete="off"
pattern="[^/?#\\]*[^/?# \\]"
title="May not end with space nor contain characters '\' '/' '#' '?'"
size={40}
aria-label={databaseIdLabel}
placeholder={databaseIdPlaceHolder}
value={databaseId}
onChange={handleonChangeDBId}
style={{ fontSize: 12 }}
styles={{ root: { width: 300 } }}
/> />
)}
<div className="paneMainContent">
<div>
<p>
<span className="mandatoryStar">*</span>
<Text variant="small">{databaseIdLabel}</Text>
<InfoTooltip>{databaseIdTooltipText}</InfoTooltip>
</p>
<TextField <Stack horizontal>
id="database-id" <Checkbox
type="text" title="Provision shared throughput"
aria-required="true" styles={{
autoComplete="off" text: { fontSize: 12 },
pattern="[^/?#\\]*[^/?# \\]" checkbox: { width: 12, height: 12 },
title="May not end with space nor contain characters '\' '/' '#' '?'" label: { padding: 0, alignItems: "center" },
size={40} }}
aria-label={databaseIdLabel} label="Provision throughput"
placeholder={databaseIdPlaceHolder} checked={databaseCreateNewShared}
value={databaseId} onChange={() => setDatabaseCreateNewShared(!databaseCreateNewShared)}
onChange={handleonChangeDBId}
/> />
<InfoTooltip>{databaseLevelThroughputTooltipText}</InfoTooltip>
</Stack>
<div {!isServerlessAccount() && databaseCreateNewShared && (
className="databaseProvision" <ThroughputInput
aria-label="New database provision support" showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container?.isFirstResourceCreated()}
style={{ display: "block ruby" }} isDatabase={true}
> isSharded={databaseCreateNewShared}
<Checkbox setThroughputValue={(newThroughput: number) => (throughput = newThroughput)}
title="Provision shared throughput" setIsAutoscale={(isAutoscale: boolean) => (isAutoscaleSelected = isAutoscale)}
styles={{ onCostAcknowledgeChange={(isAcknowledged: boolean) => (isCostAcknowledged = isAcknowledged)}
checkbox: { width: 12, height: 12 }, />
label: { padding: 0, alignItems: "center" }, )}
}}
label="Provision throughput"
checked={databaseCreateNewShared}
onChange={() => setDatabaseCreateNewShared(!databaseCreateNewShared)}
/>{" "}
<InfoTooltip>{databaseLevelThroughputTooltipText}</InfoTooltip>
</div>
{databaseCreateNewShared && (
<div>
<ThroughputInput
showFreeTierExceedThroughputTooltip={isFreeTierAccount && !container?.isFirstResourceCreated()}
isDatabase={true}
isSharded={databaseCreateNewShared}
isAutoscaleSelected={isAutoPilotSelected}
throughput={throughput}
setThroughputValue={(throughput: number) => setThroughput(throughput)}
setIsAutoscale={(isAutoscale: boolean) => setIsAutoPilotSelected(isAutoscale)}
onCostAcknowledgeChange={(isAcknowledged: boolean) => setThroughputSpendAck(isAcknowledged)}
/>
{canRequestSupport() && (
<p>
<a href="https://aka.ms/cosmosdbfeedback?subject=Cosmos%20DB%20More%20Throughput%20Request">
Contact support{" "}
</a>
for more than <span>{throughputDefaults.unlimitedmax?.toLocaleString()} </span> RU/s.
</p>
)}
</div>
)}
</div>
</div> </div>
</div> </div>
</RightPaneForm> </RightPaneForm>

View File

@@ -9,94 +9,86 @@ exports[`AddDatabasePane Pane should render Default properly 1`] = `
submitButtonText="OK" submitButtonText="OK"
> >
<div <div
aria-labelledby="databaseTitle" className="panelMainContent"
className="paneContentContainer"
role="dialog"
> >
<PanelInfoErrorComponent <div>
link="https://aka.ms/azure-cosmos-db-pricing" <Stack
linkText="More details" horizontal={true}
message="Start at $24/mo per database, multiple containers included" >
messageType="info" <span
openNotificationConsole={[Function]} className="mandatoryStar"
showErrorDetails={false} >
/> *
<div </span>
className="paneMainContent" <Text
> variant="small"
<div> >
<p> Database id
<span </Text>
className="mandatoryStar" <InfoTooltip>
> A database is a logical container of one or more collections
* </InfoTooltip>
</span> </Stack>
<Text <StyledTextFieldBase
variant="small" aria-label="Database id"
> aria-required="true"
Database id autoComplete="off"
</Text> id="database-id"
<InfoTooltip> onChange={[Function]}
A database is a logical container of one or more collections pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]"
</InfoTooltip> placeholder="Type a new database id"
</p> size={40}
<StyledTextFieldBase style={
aria-label="Database id" Object {
aria-required="true" "fontSize": 12,
autoComplete="off" }
id="database-id" }
styles={
Object {
"root": Object {
"width": 300,
},
}
}
title="May not end with space nor contain characters '\\\\' '/' '#' '?'"
type="text"
value=""
/>
<Stack
horizontal={true}
>
<StyledCheckboxBase
checked={true}
label="Provision throughput"
onChange={[Function]} onChange={[Function]}
pattern="[^/?#\\\\\\\\]*[^/?# \\\\\\\\]" styles={
placeholder="Type a new database id"
size={40}
title="May not end with space nor contain characters '\\\\' '/' '#' '?'"
type="text"
value=""
/>
<div
aria-label="New database provision support"
className="databaseProvision"
style={
Object { Object {
"display": "block ruby", "checkbox": Object {
"height": 12,
"width": 12,
},
"label": Object {
"alignItems": "center",
"padding": 0,
},
"text": Object {
"fontSize": 12,
},
} }
} }
> title="Provision shared throughput"
<StyledCheckboxBase />
checked={true} <InfoTooltip>
label="Provision throughput" Provisioned throughput at the database level will be shared across all collections within the database.
onChange={[Function]} </InfoTooltip>
styles={ </Stack>
Object { <ThroughputInput
"checkbox": Object { isDatabase={true}
"height": 12, isSharded={true}
"width": 12, onCostAcknowledgeChange={[Function]}
}, setIsAutoscale={[Function]}
"label": Object { setThroughputValue={[Function]}
"alignItems": "center", />
"padding": 0,
},
}
}
title="Provision shared throughput"
/>
<InfoTooltip>
Provisioned throughput at the database level will be shared across all collections within the database.
</InfoTooltip>
</div>
<div>
<ThroughputInput
isAutoscaleSelected={false}
isDatabase={true}
isSharded={true}
onCostAcknowledgeChange={[Function]}
setIsAutoscale={[Function]}
setThroughputValue={[Function]}
throughput={400}
/>
</div>
</div>
</div> </div>
</div> </div>
</RightPaneForm> </RightPaneForm>

View File

@@ -28,27 +28,21 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
> >
<div <div
onKeyDown={[Function]} onKeyDown={[Function]}
role="button"
tabIndex={-1} tabIndex={-1}
> >
<div <div
className="contextual-pane-out" className="contextual-pane-out"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]}
role="button"
tabIndex={0}
/> />
<div <div
className="contextual-pane" className="contextual-pane"
id="deleteCollectionpane" id="deleteCollectionpane"
onKeyDown={[Function]} onKeyDown={[Function]}
role="button"
style={ style={
Object { Object {
"height": NaN, "height": NaN,
} }
} }
tabIndex={0}
> >
<div <div
className="panelContentWrapper" className="panelContentWrapper"
@@ -1167,16 +1161,14 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
className="formErrors" className="formErrors"
title="" title=""
/> />
<div <a
className="errorLink errorLinkColor" className="errorLink"
hidden={true} hidden={true}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]}
role="link" role="link"
tabIndex={0}
> >
More details More details
</div> </a>
</span> </span>
</div> </div>
</div> </div>
@@ -3669,7 +3661,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
hidden={true} hidden={true}
> >
<img <img
alt="Loader img" alt="loader"
className="dataExplorerLoader" className="dataExplorerLoader"
src="" src=""
/> />

View File

@@ -19,27 +19,21 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
> >
<div <div
onKeyDown={[Function]} onKeyDown={[Function]}
role="button"
tabIndex={-1} tabIndex={-1}
> >
<div <div
className="contextual-pane-out" className="contextual-pane-out"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]}
role="button"
tabIndex={0}
/> />
<div <div
className="contextual-pane" className="contextual-pane"
id="executesprocparamspane" id="executesprocparamspane"
onKeyDown={[Function]} onKeyDown={[Function]}
role="button"
style={ style={
Object { Object {
"height": NaN, "height": NaN,
} }
} }
tabIndex={0}
> >
<div <div
className="panelContentWrapper" className="panelContentWrapper"
@@ -1158,16 +1152,14 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
className="formErrors" className="formErrors"
title="" title=""
/> />
<div <a
className="errorLink errorLinkColor" className="errorLink"
hidden={true} hidden={true}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]}
role="link" role="link"
tabIndex={0}
> >
More details More details
</div> </a>
</span> </span>
</div> </div>
</div> </div>
@@ -8229,7 +8221,7 @@ exports[`Excute Sproc Param Pane should render Default properly 1`] = `
hidden={true} hidden={true}
> >
<img <img
alt="Loader img" alt="loader"
className="dataExplorerLoader" className="dataExplorerLoader"
src="" src=""
/> />

View File

@@ -1,3 +1,7 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/interactive-supports-focus */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/anchor-is-valid */
import { IconButton, PrimaryButton } from "@fluentui/react"; import { IconButton, PrimaryButton } from "@fluentui/react";
import React, { FunctionComponent, ReactNode } from "react"; import React, { FunctionComponent, ReactNode } from "react";
import ErrorRedIcon from "../../../../images/error_red.svg"; import ErrorRedIcon from "../../../../images/error_red.svg";
@@ -67,16 +71,9 @@ export const GenericRightPaneComponent: FunctionComponent<GenericRightPaneProps>
<span className="formErrors" title={formError}> <span className="formErrors" title={formError}>
{formError} {formError}
</span> </span>
<div <a className="errorLink" role="link" hidden={!formErrorDetail} onClick={expandConsole}>
className="errorLink errorLinkColor"
role="link"
hidden={!formErrorDetail}
onClick={expandConsole}
tabIndex={0}
onKeyDown={expandConsole}
>
More details More details
</div> </a>
</span> </span>
</div> </div>
</div> </div>
@@ -104,7 +101,7 @@ export const GenericRightPaneComponent: FunctionComponent<GenericRightPaneProps>
const renderLoadingScreen = (): JSX.Element => { const renderLoadingScreen = (): JSX.Element => {
return ( return (
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!isExecuting}> <div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!isExecuting}>
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} alt="Loader img" /> <img className="dataExplorerLoader" src={LoadingIndicatorIcon} alt="loader" />
</div> </div>
); );
}; };
@@ -117,16 +114,9 @@ export const GenericRightPaneComponent: FunctionComponent<GenericRightPaneProps>
}; };
return ( return (
<div tabIndex={-1} onKeyDown={onKeyDown} role="button"> <div tabIndex={-1} onKeyDown={onKeyDown}>
<div className="contextual-pane-out" onClick={onClose} role="button" tabIndex={0} onKeyDown={onClose}></div> <div className="contextual-pane-out" onClick={onClose}></div>
<div <div className="contextual-pane" id={id} style={{ height: panelHeight }} onKeyDown={onKeyDown}>
className="contextual-pane"
id={id}
style={{ height: panelHeight }}
onKeyDown={onKeyDown}
role="button"
tabIndex={0}
>
<div className="panelContentWrapper"> <div className="panelContentWrapper">
{renderPanelHeader()} {renderPanelHeader()}
{renderErrorSection()} {renderErrorSection()}

View File

@@ -21,115 +21,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"_closeSynapseLinkModalDialog": [Function], "_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function], "_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function], "_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"addCollectionText": [Function], "addCollectionText": [Function],
@@ -1015,32 +906,11 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
}, },
}, },
}, },
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
@@ -1107,7 +977,6 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],
}, },
"toggleLeftPaneExpandedKeyPress": [Function],
}, },
"getRepo": [Function], "getRepo": [Function],
"pinRepo": [Function], "pinRepo": [Function],

View File

@@ -1,59 +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" data-bind="attr: { id: id }">
<!-- Graph Styling form - Start -->
<div class="contextual-pane-in">
<form class="paneContentContainer" data-bind="submit: submit">
<!-- Graph Styling header - Start -->
<div class="firstdivbg headerline">
<span role="heading" aria-level="2">Graph Styling</span>
<div
class="closeImg"
role="button"
aria-label="Close pane"
tabindex="0"
data-bind="click: cancel, event: { keypress: onCloseKeyPress }"
>
<img src="../../../images/close-black.svg" title="Close" alt="Close" />
</div>
</div>
<!-- Graph Styling header - End -->
<!-- Graph Styling errors - Start -->
<div
aria-live="assertive"
class="warningErrorContainer"
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>
<!-- Graph Styling errors - End -->
<!-- Add graph configuration - Start -->
<div class="paneMainContent">
<graph-style
id="graphStyleComponent"
params="{ config:graphConfigUIData, firstFieldHasFocus: firstFieldHasFocus }"
></graph-style>
</div>
<div class="paneFooter">
<div class="leftpanel-okbut"><input type="submit" value="OK" class="btncreatecoll1" /></div>
</div>
<!-- Add Graph configuration - End -->
</form>
</div>
<!-- Graph Styling form - End -->
</div>
</div>

View File

@@ -1,68 +0,0 @@
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import { ContextualPaneBase } from "./ContextualPaneBase";
export default class GraphStylingPane extends ContextualPaneBase {
public graphConfigUIData: ViewModels.GraphConfigUiData;
private remoteConfig: ViewModels.GraphConfigUiData;
constructor(options: ViewModels.PaneOptions) {
super(options);
this.graphConfigUIData = {
showNeighborType: ko.observable(ViewModels.NeighborType.TARGETS_ONLY),
nodeProperties: ko.observableArray([]),
nodePropertiesWithNone: ko.observableArray([]),
nodeCaptionChoice: ko.observable(null),
nodeColorKeyChoice: ko.observable(null),
nodeIconChoice: ko.observable(null),
nodeIconSet: ko.observable(null),
};
this.graphConfigUIData.nodeCaptionChoice.subscribe((val) => {
if (this.remoteConfig) {
this.remoteConfig.nodeCaptionChoice(val);
}
});
this.graphConfigUIData.nodeColorKeyChoice.subscribe((val) => {
if (this.remoteConfig) {
this.remoteConfig.nodeColorKeyChoice(val);
}
});
this.graphConfigUIData.nodeIconChoice.subscribe((val) => {
if (this.remoteConfig) {
this.remoteConfig.nodeIconChoice(val);
}
});
this.graphConfigUIData.nodeIconSet.subscribe((val) => {
if (this.remoteConfig) {
this.remoteConfig.nodeIconSet(val);
}
});
this.graphConfigUIData.showNeighborType.subscribe((val) => {
if (this.remoteConfig) {
this.remoteConfig.showNeighborType(val);
}
});
}
public setData(config: ViewModels.GraphConfigUiData): void {
// Update pane ko's with config's ko
this.graphConfigUIData.nodeIconChoice(config.nodeIconChoice());
this.graphConfigUIData.nodeIconSet(config.nodeIconSet());
this.graphConfigUIData.nodeProperties(config.nodeProperties());
this.graphConfigUIData.nodePropertiesWithNone(config.nodePropertiesWithNone());
this.graphConfigUIData.showNeighborType(config.showNeighborType());
// Make sure these two happen *after* setting the options of the dropdown,
// otherwise, the ko will not get set if the choice is not part of the options
this.graphConfigUIData.nodeCaptionChoice(config.nodeCaptionChoice());
this.graphConfigUIData.nodeColorKeyChoice(config.nodeColorKeyChoice());
this.remoteConfig = config;
}
public close() {
this.remoteConfig = null;
super.close();
}
}

View File

@@ -0,0 +1,37 @@
import React, { FunctionComponent } from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import { GraphStyleComponent } from "../../Graph/GraphStyleComponent/GraphStyleComponent";
import { IGraphConfig } from "../../Tabs/GraphTab";
import { PanelFooterComponent } from "../PanelFooterComponent";
interface GraphStylingProps {
closePanel: () => void;
igraphConfigUiData: ViewModels.IGraphConfigUiData;
igraphConfig: IGraphConfig;
getValues: (igraphConfig?: IGraphConfig) => void;
}
export const GraphStylingPanel: FunctionComponent<GraphStylingProps> = ({
closePanel,
igraphConfigUiData,
igraphConfig,
getValues,
}: GraphStylingProps): JSX.Element => {
const buttonLabel = "Ok";
const submit = () => {
closePanel();
};
return (
<form className="panelFormWrapper" onSubmit={submit}>
<div className="panelMainContent">
<GraphStyleComponent
igraphConfigUiData={igraphConfigUiData}
igraphConfig={igraphConfig}
getValues={getValues}
></GraphStyleComponent>
</div>
<PanelFooterComponent buttonLabel={buttonLabel} />
</form>
);
};

View File

@@ -1,7 +1,5 @@
import AddDatabasePaneTemplate from "./AddDatabasePane.html"; import AddDatabasePaneTemplate from "./AddDatabasePane.html";
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html"; import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
import GraphStylingPaneTemplate from "./GraphStylingPane.html";
export class PaneComponent { export class PaneComponent {
constructor(data: any) { constructor(data: any) {
return data.data; return data.data;
@@ -17,15 +15,6 @@ export class AddDatabasePaneComponent {
} }
} }
export class GraphStylingPaneComponent {
constructor() {
return {
viewModel: PaneComponent,
template: GraphStylingPaneTemplate,
};
}
}
export class CassandraAddCollectionPaneComponent { export class CassandraAddCollectionPaneComponent {
constructor() { constructor() {
return { return {

View File

@@ -8,7 +8,6 @@ export interface PanelInfoErrorProps {
link?: string; link?: string;
linkText?: string; linkText?: string;
openNotificationConsole?: () => void; openNotificationConsole?: () => void;
formError?: boolean;
} }
export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProps> = ({ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProps> = ({
@@ -18,9 +17,8 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
link, link,
linkText, linkText,
openNotificationConsole, openNotificationConsole,
formError = true,
}: PanelInfoErrorProps): JSX.Element => { }: PanelInfoErrorProps): JSX.Element => {
let icon: JSX.Element; let icon: JSX.Element = <Icon iconName="InfoSolid" className="panelLargeInfoIcon" aria-label="Infomation" />;
if (messageType === "error") { if (messageType === "error") {
icon = <Icon iconName="StatusErrorFull" className="panelErrorIcon" aria-label="error" />; icon = <Icon iconName="StatusErrorFull" className="panelErrorIcon" aria-label="error" />;
} else if (messageType === "warning") { } else if (messageType === "warning") {
@@ -30,31 +28,29 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
} }
return ( return (
formError && ( <Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center"> {icon}
{icon} <span className="panelWarningErrorDetailsLinkContainer">
<span className="panelWarningErrorDetailsLinkContainer"> <Text className="panelWarningErrorMessage" variant="small" aria-label="message">
<Text className="panelWarningErrorMessage" variant="small" aria-label="message"> {message}
{message} {link && linkText && (
{link && linkText && ( <Link target="_blank" href={link}>
<Link target="_blank" href={link}> {linkText}
{linkText} </Link>
</Link>
)}
</Text>
{showErrorDetails && (
<div
className="paneErrorLink"
role="link"
onClick={openNotificationConsole}
tabIndex={0}
onKeyDown={openNotificationConsole}
>
More details
</div>
)} )}
</span> </Text>
</Stack> {showErrorDetails && (
) <a
className="paneErrorLink"
role="link"
onClick={openNotificationConsole}
tabIndex={0}
onKeyDown={openNotificationConsole}
>
More details
</a>
)}
</span>
</Stack>
); );
}; };

View File

@@ -9,7 +9,6 @@ const expandConsole = jest.fn();
const props = { const props = {
expandConsole, expandConsole,
formError: "", formError: "",
formErrorDetail: "",
isExecuting: false, isExecuting: false,
submitButtonText: "Load", submitButtonText: "Load",
onSubmit, onSubmit,

View File

@@ -1,12 +1,11 @@
import React, { FunctionComponent, ReactNode } from "react"; import React, { FunctionComponent, ReactNode } from "react";
import { PanelFooterComponent } from "../PanelFooterComponent"; import { PanelFooterComponent } from "../PanelFooterComponent";
import { PanelInfoErrorComponent, PanelInfoErrorProps } from "../PanelInfoErrorComponent"; import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
import { PanelLoadingScreen } from "../PanelLoadingScreen"; import { PanelLoadingScreen } from "../PanelLoadingScreen";
export interface RightPaneFormProps { export interface RightPaneFormProps {
expandConsole: () => void; expandConsole: () => void;
formError: string; formError: string;
formErrorDetail: string;
isExecuting: boolean; isExecuting: boolean;
onSubmit: () => void; onSubmit: () => void;
submitButtonText: string; submitButtonText: string;
@@ -17,7 +16,6 @@ export interface RightPaneFormProps {
export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
expandConsole, expandConsole,
formError, formError,
formErrorDetail,
isExecuting, isExecuting,
onSubmit, onSubmit,
submitButtonText, submitButtonText,
@@ -29,18 +27,17 @@ export const RightPaneForm: FunctionComponent<RightPaneFormProps> = ({
onSubmit(); onSubmit();
}; };
const panelInfoErrorProps: PanelInfoErrorProps = {
messageType: "error",
message: formError,
formError: formError !== "",
showErrorDetails: formErrorDetail !== "",
openNotificationConsole: expandConsole,
};
return ( return (
<> <>
<PanelInfoErrorComponent {...panelInfoErrorProps} />
<form className="panelFormWrapper" onSubmit={handleOnSubmit}> <form className="panelFormWrapper" onSubmit={handleOnSubmit}>
{formError && (
<PanelInfoErrorComponent
messageType="error"
message={formError}
showErrorDetails={true}
openNotificationConsole={expandConsole}
/>
)}
{children} {children}
{!isSubmitButtonHidden && <PanelFooterComponent buttonLabel={submitButtonText} />} {!isSubmitButtonHidden && <PanelFooterComponent buttonLabel={submitButtonText} />}
</form> </form>

View File

@@ -4,18 +4,10 @@ exports[`Right Pane Form should render Default properly 1`] = `
<RightPaneForm <RightPaneForm
expandConsole={[MockFunction]} expandConsole={[MockFunction]}
formError="" formError=""
formErrorDetail=""
isExecuting={false} isExecuting={false}
onSubmit={[MockFunction]} onSubmit={[MockFunction]}
submitButtonText="Load" submitButtonText="Load"
> >
<PanelInfoErrorComponent
formError={false}
message=""
messageType="error"
openNotificationConsole={[MockFunction]}
showErrorDetails={false}
/>
<form <form
className="panelFormWrapper" className="panelFormWrapper"
onSubmit={[Function]} onSubmit={[Function]}

View File

@@ -18,7 +18,6 @@ export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
expandConsole, expandConsole,
closePanel, closePanel,
}: SettingsPaneProps) => { }: SettingsPaneProps) => {
const [formErrors, setFormErrors] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false); const [isExecuting, setIsExecuting] = useState<boolean>(false);
const [pageOption, setPageOption] = useState<string>( const [pageOption, setPageOption] = useState<string>(
LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage) === Constants.Queries.unlimitedItemsPerPage LocalStorageUtility.getEntryNumber(StorageKey.ActualItemPerPage) === Constants.Queries.unlimitedItemsPerPage
@@ -50,7 +49,6 @@ export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
const shouldShowParallelismOption = userContext.apiType !== "Gremlin"; const shouldShowParallelismOption = userContext.apiType !== "Gremlin";
const handlerOnSubmit = (e: MouseEvent<HTMLButtonElement>) => { const handlerOnSubmit = (e: MouseEvent<HTMLButtonElement>) => {
setFormErrors("");
setIsExecuting(true); setIsExecuting(true);
LocalStorageUtility.setEntryNumber( LocalStorageUtility.setEntryNumber(
@@ -104,8 +102,7 @@ export const SettingsPane: FunctionComponent<SettingsPaneProps> = ({
const genericPaneProps: RightPaneFormProps = { const genericPaneProps: RightPaneFormProps = {
expandConsole, expandConsole,
formError: formErrors, formError: "",
formErrorDetail: "",
isExecuting, isExecuting,
submitButtonText: "Apply", submitButtonText: "Apply",
onSubmit: () => handlerOnSubmit(undefined), onSubmit: () => handlerOnSubmit(undefined),

View File

@@ -4,7 +4,6 @@ exports[`Settings Pane should render Default properly 1`] = `
<RightPaneForm <RightPaneForm
expandConsole={[Function]} expandConsole={[Function]}
formError="" formError=""
formErrorDetail=""
isExecuting={false} isExecuting={false}
onSubmit={[Function]} onSubmit={[Function]}
submitButtonText="Apply" submitButtonText="Apply"
@@ -152,7 +151,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
<RightPaneForm <RightPaneForm
expandConsole={[Function]} expandConsole={[Function]}
formError="" formError=""
formErrorDetail=""
isExecuting={false} isExecuting={false}
onSubmit={[Function]} onSubmit={[Function]}
submitButtonText="Apply" submitButtonText="Apply"

View File

@@ -11,115 +11,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"_closeSynapseLinkModalDialog": [Function], "_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function], "_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function], "_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"addCollectionText": [Function], "addCollectionText": [Function],
@@ -1005,32 +896,11 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
}, },
}, },
}, },
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
"isFixedCollectionWithSharedThroughputSupported": [Function], "isFixedCollectionWithSharedThroughputSupported": [Function],
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLeftPaneExpanded": [Function],
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
@@ -1097,7 +967,6 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],
}, },
"toggleLeftPaneExpandedKeyPress": [Function],
} }
} }
inProgressMessage="Creating directory " inProgressMessage="Creating directory "
@@ -1128,27 +997,21 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
> >
<div <div
onKeyDown={[Function]} onKeyDown={[Function]}
role="button"
tabIndex={-1} tabIndex={-1}
> >
<div <div
className="contextual-pane-out" className="contextual-pane-out"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]}
role="button"
tabIndex={0}
/> />
<div <div
className="contextual-pane" className="contextual-pane"
id="stringInputPane" id="stringInputPane"
onKeyDown={[Function]} onKeyDown={[Function]}
role="button"
style={ style={
Object { Object {
"height": NaN, "height": NaN,
} }
} }
tabIndex={0}
> >
<div <div
className="panelContentWrapper" className="panelContentWrapper"
@@ -2267,16 +2130,14 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
className="formErrors" className="formErrors"
title="" title=""
/> />
<div <a
className="errorLink errorLinkColor" className="errorLink"
hidden={true} hidden={true}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]}
role="link" role="link"
tabIndex={0}
> >
More details More details
</div> </a>
</span> </span>
</div> </div>
</div> </div>
@@ -4692,7 +4553,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
hidden={true} hidden={true}
> >
<img <img
alt="Loader img" alt="loader"
className="dataExplorerLoader" className="dataExplorerLoader"
src="" src=""
/> />

View File

@@ -23,27 +23,21 @@ exports[`Table query select Panel should render Default properly 1`] = `
> >
<div <div
onKeyDown={[Function]} onKeyDown={[Function]}
role="button"
tabIndex={-1} tabIndex={-1}
> >
<div <div
className="contextual-pane-out" className="contextual-pane-out"
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]}
role="button"
tabIndex={0}
/> />
<div <div
className="contextual-pane" className="contextual-pane"
id="querySelectPane" id="querySelectPane"
onKeyDown={[Function]} onKeyDown={[Function]}
role="button"
style={ style={
Object { Object {
"height": NaN, "height": NaN,
} }
} }
tabIndex={0}
> >
<div <div
className="panelContentWrapper" className="panelContentWrapper"
@@ -1162,16 +1156,14 @@ exports[`Table query select Panel should render Default properly 1`] = `
className="formErrors" className="formErrors"
title="" title=""
/> />
<div <a
className="errorLink errorLinkColor" className="errorLink"
hidden={true} hidden={true}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]}
role="link" role="link"
tabIndex={0}
> >
More details More details
</div> </a>
</span> </span>
</div> </div>
</div> </div>
@@ -4202,7 +4194,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
hidden={true} hidden={true}
> >
<img <img
alt="Loader img" alt="loader"
className="dataExplorerLoader" className="dataExplorerLoader"
src="" src=""
/> />

View File

@@ -22,15 +22,12 @@ export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({
const [files, setFiles] = useState<FileList>(); const [files, setFiles] = useState<FileList>();
const [formErrors, setFormErrors] = useState<string>(""); const [formErrors, setFormErrors] = useState<string>("");
const [formErrorsDetails, setFormErrorsDetails] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(false); const [isExecuting, setIsExecuting] = useState<boolean>(false);
const submit = () => { const submit = () => {
setFormErrors(""); setFormErrors("");
setFormErrorsDetails("");
if (!files || files.length === 0) { if (!files || files.length === 0) {
setFormErrors("No file specified"); setFormErrors("No file specified. Please input a file.");
setFormErrorsDetails("No file specified. Please input a file.");
logConsoleError(`${errorMessage} -- No file specified. Please input a file.`); logConsoleError(`${errorMessage} -- No file specified. Please input a file.`);
return; return;
} }
@@ -49,7 +46,6 @@ export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({
}, },
(error: string) => { (error: string) => {
setFormErrors(errorMessage); setFormErrors(errorMessage);
setFormErrorsDetails(`${errorMessage}: ${error}`);
logConsoleError(`${errorMessage} ${file.name}: ${error}`); logConsoleError(`${errorMessage} ${file.name}: ${error}`);
} }
) )
@@ -85,7 +81,6 @@ export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({
const genericPaneProps: RightPaneFormProps = { const genericPaneProps: RightPaneFormProps = {
expandConsole, expandConsole,
formError: formErrors, formError: formErrors,
formErrorDetail: formErrorsDetails,
isExecuting: isExecuting, isExecuting: isExecuting,
submitButtonText: "Upload", submitButtonText: "Upload",
onSubmit: submit, onSubmit: submit,

View File

@@ -15,15 +15,14 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explo
const [files, setFiles] = useState<FileList>(); const [files, setFiles] = useState<FileList>();
const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]); const [uploadFileData, setUploadFileData] = useState<UploadDetailsRecord[]>([]);
const [formError, setFormError] = useState<string>(""); const [formError, setFormError] = useState<string>("");
const [formErrorDetail, setFormErrorDetail] = useState<string>("");
const [isExecuting, setIsExecuting] = useState<boolean>(); const [isExecuting, setIsExecuting] = useState<boolean>();
const onSubmit = () => { const onSubmit = () => {
setFormError(""); setFormError("");
if (!files || files.length === 0) { if (!files || files.length === 0) {
setFormError("No files specified"); setFormError("No files were specified. Please input at least one file.");
setFormErrorDetail("No files were specified. Please input at least one file.");
logConsoleError("Could not upload items -- No files were specified. Please input at least one file."); logConsoleError("Could not upload items -- No files were specified. Please input at least one file.");
return;
} }
const selectedCollection = explorer.findSelectedCollection(); const selectedCollection = explorer.findSelectedCollection();
@@ -40,7 +39,6 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explo
(error: Error) => { (error: Error) => {
const errorMessage = getErrorMessage(error); const errorMessage = getErrorMessage(error);
setFormError(errorMessage); setFormError(errorMessage);
setFormErrorDetail(errorMessage);
} }
) )
.finally(() => { .finally(() => {
@@ -55,7 +53,6 @@ export const UploadItemsPane: FunctionComponent<UploadItemsPaneProps> = ({ explo
const genericPaneProps: RightPaneFormProps = { const genericPaneProps: RightPaneFormProps = {
expandConsole: () => explorer.expandConsole(), expandConsole: () => explorer.expandConsole(),
formError, formError,
formErrorDetail,
isExecuting: isExecuting, isExecuting: isExecuting,
submitButtonText: "Upload", submitButtonText: "Upload",
onSubmit, onSubmit,

View File

@@ -4,7 +4,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
<RightPaneForm <RightPaneForm
expandConsole={[Function]} expandConsole={[Function]}
formError="" formError=""
formErrorDetail=""
onSubmit={[Function]} onSubmit={[Function]}
submitButtonText="Upload" submitButtonText="Upload"
> >

View File

@@ -9,115 +9,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"_closeSynapseLinkModalDialog": [Function], "_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function], "_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false, "_isInitializingNotebooks": false,
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"databaseCreateNewShared": [Function],
"databaseId": [Function],
"databaseIdLabel": [Function],
"databaseIdPlaceHolder": [Function],
"databaseIdTooltipText": [Function],
"databaseLevelThroughputTooltipText": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"freeTierExceedThroughputTooltip": [Function],
"id": "adddatabasepane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isTemplateReady": [Function],
"maxAutoPilotThroughputSet": [Function],
"maxThroughputRU": [Function],
"maxThroughputRUText": [Function],
"minThroughputRU": [Function],
"onMoreDetailsKeyPress": [Function],
"requestUnitsUsageCost": [Function],
"ruToolTipText": [Function],
"showUpsellMessage": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"upsellAnchorText": [Function],
"upsellAnchorUrl": [Function],
"upsellMessage": [Function],
"upsellMessageAriaLabel": [Function],
"visible": [Function],
},
GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
CassandraAddCollectionPane {
"autoPilotUsageCost": [Function],
"canConfigureThroughput": [Function],
"canExceedMaximumValue": [Function],
"canRequestSupport": [Function],
"container": [Circular],
"costsVisible": [Function],
"createTableQuery": [Function],
"dedicateTableThroughput": [Function],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"id": "cassandraaddcollectionpane",
"isAutoPilotSelected": [Function],
"isExecuting": [Function],
"isFreeTierAccount": [Function],
"isSharedAutoPilotSelected": [Function],
"isTemplateReady": [Function],
"keyspaceCreateNew": [Function],
"keyspaceHasSharedOffer": [Function],
"keyspaceId": [Function],
"keyspaceIds": [Function],
"keyspaceOffers": Map {},
"keyspaceThroughput": [Function],
"maxThroughputRU": [Function],
"minThroughputRU": [Function],
"requestUnitsUsageCostDedicated": [Function],
"requestUnitsUsageCostShared": [Function],
"ruToolTipText": [Function],
"selectedAutoPilotThroughput": [Function],
"sharedAutoPilotThroughput": [Function],
"sharedThroughputRangeText": [Function],
"sharedThroughputSpendAck": [Function],
"sharedThroughputSpendAckText": [Function],
"sharedThroughputSpendAckVisible": [Function],
"tableId": [Function],
"throughput": [Function],
"throughputRangeText": [Function],
"throughputSpendAck": [Function],
"throughputSpendAckText": [Function],
"throughputSpendAckVisible": [Function],
"title": [Function],
"userTableQuery": [Function],
"visible": [Function],
},
],
"_refreshSparkEnabledStateForAccount": [Function], "_refreshSparkEnabledStateForAccount": [Function],
"_resetNotebookWorkspace": [Function], "_resetNotebookWorkspace": [Function],
"addCollectionText": [Function], "addCollectionText": [Function],
@@ -1003,26 +894,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
}, },
}, },
}, },
"graphStylingPane": GraphStylingPane {
"container": [Circular],
"firstFieldHasFocus": [Function],
"formErrors": [Function],
"formErrorsDetails": [Function],
"graphConfigUIData": Object {
"nodeCaptionChoice": [Function],
"nodeColorKeyChoice": [Function],
"nodeIconChoice": [Function],
"nodeIconSet": [Function],
"nodeProperties": [Function],
"nodePropertiesWithNone": [Function],
"showNeighborType": [Function],
},
"id": "graphstylingpane",
"isExecuting": [Function],
"isTemplateReady": [Function],
"title": [Function],
"visible": [Function],
},
"hasStorageAnalyticsAfecFeature": [Function], "hasStorageAnalyticsAfecFeature": [Function],
"isAccountReady": [Function], "isAccountReady": [Function],
"isAutoscaleDefaultEnabled": [Function], "isAutoscaleDefaultEnabled": [Function],
@@ -1030,7 +901,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"isHostedDataExplorerEnabled": [Function], "isHostedDataExplorerEnabled": [Function],
"isLastCollection": [Function], "isLastCollection": [Function],
"isLastNonEmptyDatabase": [Function], "isLastNonEmptyDatabase": [Function],
"isLeftPaneExpanded": [Function],
"isMongoIndexingEnabled": [Function], "isMongoIndexingEnabled": [Function],
"isNotebookEnabled": [Function], "isNotebookEnabled": [Function],
"isNotebooksEnabledForAccount": [Function], "isNotebooksEnabledForAccount": [Function],
@@ -1099,7 +969,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
"activeTab": [Function], "activeTab": [Function],
"openedTabs": [Function], "openedTabs": [Function],
}, },
"toggleLeftPaneExpandedKeyPress": [Function],
} }
} }
openNotificationConsole={[Function]} openNotificationConsole={[Function]}

View File

@@ -11,8 +11,10 @@ import {
GraphExplorerError, GraphExplorerError,
GraphExplorerProps, GraphExplorerProps,
} from "../Graph/GraphExplorerComponent/GraphExplorer"; } from "../Graph/GraphExplorerComponent/GraphExplorer";
// import { GraphAccessor, GraphExplorer, GraphExplorerError } from "../Graph/GraphExplorerComponent/GraphExplorer";
// import { GraphExplorerAdapter } from "../Graph/GraphExplorerComponent/GraphExplorerAdapter";
import { ContextualPaneBase } from "../Panes/ContextualPaneBase"; import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
import GraphStylingPane from "../Panes/GraphStylingPane"; import { GraphStylingPanel } from "../Panes/GraphStylingPanel/GraphStylingPanel";
import { NewVertexPanel } from "../Panes/NewVertexPanel/NewVertexPanel"; import { NewVertexPanel } from "../Panes/NewVertexPanel/NewVertexPanel";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
export interface GraphIconMap { export interface GraphIconMap {
@@ -31,6 +33,18 @@ export interface GraphConfig {
iconsMap: ko.Observable<GraphIconMap>; iconsMap: ko.Observable<GraphIconMap>;
} }
export interface IGraphConfig {
nodeColor: string;
nodeColorKey: string;
linkColor: string;
showNeighborType: ViewModels.NeighborType;
nodeCaption: string;
nodeSize: number;
linkWidth: number;
nodeIconKey: string;
iconsMap: GraphIconMap;
}
interface GraphTabOptions extends ViewModels.TabOptions { interface GraphTabOptions extends ViewModels.TabOptions {
account: DatabaseAccount; account: DatabaseAccount;
masterKey: string; masterKey: string;
@@ -39,6 +53,7 @@ interface GraphTabOptions extends ViewModels.TabOptions {
collectionPartitionKeyProperty: string; collectionPartitionKeyProperty: string;
} }
// export default class GraphTab extends React.Component<GraphTabProps, GraphTabStates> {
export default class GraphTab extends TabsBase { export default class GraphTab extends TabsBase {
// Graph default configuration // Graph default configuration
public static readonly DEFAULT_NODE_CAPTION = "id"; public static readonly DEFAULT_NODE_CAPTION = "id";
@@ -51,27 +66,24 @@ export default class GraphTab extends TabsBase {
private isPropertyEditing: ko.Observable<boolean>; private isPropertyEditing: ko.Observable<boolean>;
private isGraphDisplayed: ko.Observable<boolean>; private isGraphDisplayed: ko.Observable<boolean>;
private graphAccessor: GraphAccessor; private graphAccessor: GraphAccessor;
private graphConfig: GraphConfig; private igraphConfig: IGraphConfig;
private graphConfigUiData: ViewModels.GraphConfigUiData; private igraphConfigUiData: ViewModels.IGraphConfigUiData;
private isFilterQueryLoading: ko.Observable<boolean>; private isFilterQueryLoading: ko.Observable<boolean>;
private isValidQuery: ko.Observable<boolean>; private isValidQuery: ko.Observable<boolean>;
private graphStylingPane: GraphStylingPane;
private collectionPartitionKeyProperty: string; private collectionPartitionKeyProperty: string;
private contextualPane: ContextualPaneBase; private contextualPane: ContextualPaneBase;
public graphExplorer: GraphExplorer;
public options: GraphTabOptions;
constructor(options: GraphTabOptions) { constructor(options: GraphTabOptions) {
super(options); super(options);
this.graphStylingPane = options.collection && options.collection.container.graphStylingPane;
this.collectionPartitionKeyProperty = options.collectionPartitionKeyProperty; this.collectionPartitionKeyProperty = options.collectionPartitionKeyProperty;
this.isNewVertexDisabled = ko.observable(false); this.isNewVertexDisabled = ko.observable(false);
this.isPropertyEditing = ko.observable(false); this.isPropertyEditing = ko.observable(false);
this.isGraphDisplayed = ko.observable(false); this.isGraphDisplayed = ko.observable(false);
this.graphAccessor = undefined; this.graphAccessor = undefined;
this.graphConfig = GraphTab.createGraphConfig(); this.igraphConfig = GraphTab.createIGraphConfig();
// TODO Merge this with this.graphConfig this.igraphConfigUiData = GraphTab.createIGraphConfigUiData(this.igraphConfig);
this.graphConfigUiData = GraphTab.createGraphConfigUiData(this.graphConfig);
this.graphExplorerProps = { this.graphExplorerProps = {
onGraphAccessorCreated: (instance: GraphAccessor): void => { onGraphAccessorCreated: (instance: GraphAccessor): void => {
this.graphAccessor = instance; this.graphAccessor = instance;
@@ -88,9 +100,9 @@ export default class GraphTab extends TabsBase {
this.isGraphDisplayed(isDisplayed); this.isGraphDisplayed(isDisplayed);
this.updateNavbarWithTabsButtons(); this.updateNavbarWithTabsButtons();
}, },
onResetDefaultGraphConfigValues: () => this.setDefaultGraphConfigValues(), onResetDefaultGraphConfigValues: () => this.setDefaultIGraphConfigValues(),
graphConfig: this.graphConfig, igraphConfig: this.igraphConfig,
graphConfigUiData: this.graphConfigUiData, igraphConfigUiData: this.igraphConfigUiData,
onIsFilterQueryLoadingChange: (isFilterQueryLoading: boolean): void => onIsFilterQueryLoadingChange: (isFilterQueryLoading: boolean): void =>
this.isFilterQueryLoading(isFilterQueryLoading), this.isFilterQueryLoading(isFilterQueryLoading),
onIsValidQueryChange: (isValidQuery: boolean): void => this.isValidQuery(isValidQuery), onIsValidQueryChange: (isValidQuery: boolean): void => this.isValidQuery(isValidQuery),
@@ -106,10 +118,12 @@ export default class GraphTab extends TabsBase {
} }
}, },
resourceId: options.account.id, resourceId: options.account.id,
setIConfigUiData: this.setIGraphConfigUiData,
}; };
this.isFilterQueryLoading = ko.observable(false); this.isFilterQueryLoading = ko.observable(false);
this.isValidQuery = ko.observable(true); this.isValidQuery = ko.observable(true);
// this.setCaption = this.setCaption.bind(this);
} }
public static getGremlinEndpoint(account: DatabaseAccount): string { public static getGremlinEndpoint(account: DatabaseAccount): string {
@@ -170,60 +184,76 @@ export default class GraphTab extends TabsBase {
); );
} }
public openStyling(): void { public openStyling(): void {
this.setDefaultGraphConfigValues(); this.collection.container.openSidePanel(
// Update the styling pane with this instance "Graph Style",
this.graphStylingPane.setData(this.graphConfigUiData); <GraphStylingPanel
this.graphStylingPane.open(); closePanel={this.collection.container.closeSidePanel}
igraphConfigUiData={this.igraphConfigUiData}
igraphConfig={this.igraphConfig}
getValues={(igraphConfig?: IGraphConfig): void => {
this.igraphConfig = igraphConfig;
this.graphAccessor.shareIGraphConfig(igraphConfig);
}}
/>
);
} }
public static createGraphConfig(): GraphConfig { setIGraphConfigUiData = (val: string[]): void => {
if (val.length > 0) {
this.igraphConfigUiData = {
showNeighborType: ViewModels.NeighborType.TARGETS_ONLY,
nodeProperties: val,
nodePropertiesWithNone: [GraphExplorer.NONE_CHOICE].concat(val),
nodeCaptionChoice: this.igraphConfig.nodeCaption,
nodeColorKeyChoice: "None",
nodeIconChoice: "Node",
nodeIconSet: "",
};
}
};
public static createIGraphConfig(): IGraphConfig {
return { return {
nodeColor: ko.observable(GraphTab.NODE_COLOR), nodeColor: GraphTab.NODE_COLOR,
nodeColorKey: ko.observable(undefined), nodeColorKey: "None",
linkColor: ko.observable(GraphTab.LINK_COLOR), linkColor: GraphTab.LINK_COLOR,
showNeighborType: ko.observable(ViewModels.NeighborType.TARGETS_ONLY), showNeighborType: ViewModels.NeighborType.TARGETS_ONLY,
nodeCaption: ko.observable(GraphTab.DEFAULT_NODE_CAPTION), nodeCaption: GraphTab.DEFAULT_NODE_CAPTION,
nodeSize: ko.observable(GraphTab.NODE_SIZE), nodeSize: GraphTab.NODE_SIZE,
linkWidth: ko.observable(GraphTab.LINK_WIDTH), linkWidth: GraphTab.LINK_WIDTH,
nodeIconKey: ko.observable(undefined), nodeIconKey: undefined,
iconsMap: ko.observable({}), iconsMap: {},
}; };
} }
public static createGraphConfigUiData(graphConfig: GraphConfig): ViewModels.GraphConfigUiData { public static createIGraphConfigUiData(igraphConfig: IGraphConfig): ViewModels.IGraphConfigUiData {
return { return {
showNeighborType: ko.observable(graphConfig.showNeighborType()), showNeighborType: igraphConfig.showNeighborType,
nodeProperties: ko.observableArray([]), nodeProperties: [],
nodePropertiesWithNone: ko.observableArray([]), nodePropertiesWithNone: [],
nodeCaptionChoice: ko.observable(graphConfig.nodeCaption()), nodeCaptionChoice: igraphConfig.nodeCaption,
nodeColorKeyChoice: ko.observable(graphConfig.nodeColorKey()), nodeColorKeyChoice: igraphConfig.nodeIconKey,
nodeIconChoice: ko.observable(graphConfig.nodeIconKey()), nodeIconChoice: igraphConfig.nodeIconKey,
nodeIconSet: ko.observable(undefined), nodeIconSet: undefined,
}; };
} }
/** private setDefaultIGraphConfigValues() {
* Make sure graph config values are not undefined
*/
private setDefaultGraphConfigValues() {
// Assign default values if undefined // Assign default values if undefined
if ( if (this.igraphConfigUiData.nodeCaptionChoice === undefined && this.igraphConfigUiData.nodeProperties.length > 1) {
this.graphConfigUiData.nodeCaptionChoice() === undefined && this.igraphConfigUiData.nodeCaptionChoice = this.igraphConfigUiData.nodeProperties[0];
this.graphConfigUiData.nodeProperties().length > 1
) {
this.graphConfigUiData.nodeCaptionChoice(this.graphConfigUiData.nodeProperties()[0]);
} }
if ( if (
this.graphConfigUiData.nodeColorKeyChoice() === undefined && this.igraphConfigUiData.nodeColorKeyChoice === undefined &&
this.graphConfigUiData.nodePropertiesWithNone().length > 1 this.igraphConfigUiData.nodePropertiesWithNone.length > 1
) { ) {
this.graphConfigUiData.nodeColorKeyChoice(this.graphConfigUiData.nodePropertiesWithNone()[0]); this.igraphConfigUiData.nodeColorKeyChoice = this.igraphConfigUiData.nodePropertiesWithNone[0];
} }
if ( if (
this.graphConfigUiData.nodeIconChoice() === undefined && this.igraphConfigUiData.nodeIconChoice === undefined &&
this.graphConfigUiData.nodePropertiesWithNone().length > 1 this.igraphConfigUiData.nodePropertiesWithNone.length > 1
) { ) {
this.graphConfigUiData.nodeIconChoice(this.graphConfigUiData.nodePropertiesWithNone()[0]); this.igraphConfigUiData.nodeIconChoice = this.igraphConfigUiData.nodePropertiesWithNone[0];
} }
} }
protected getTabsButtons(): CommandButtonComponentProps[] { protected getTabsButtons(): CommandButtonComponentProps[] {

View File

@@ -1,13 +1,16 @@
import { SchemaAnalyzerComponentAdapter } from "../Notebook/SchemaAnalyzerComponent/SchemaAnalyzerComponentAdapter"; import * as Constants from "../../Common/Constants";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor";
import { SchemaAnalyzerAdapter } from "../Notebook/SchemaAnalyzer/SchemaAnalyzerAdapter";
import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase"; import NotebookTabBase, { NotebookTabBaseOptions } from "./NotebookTabBase";
export default class SchemaAnalyzerTab extends NotebookTabBase { export default class SchemaAnalyzerTab extends NotebookTabBase {
public readonly html = '<div data-bind="react:schemaAnalyzerComponentAdapter" style="height: 100%"></div>'; public readonly html = '<div data-bind="react:schemaAnalyzerAdapter" style="height: 100%"></div>';
private schemaAnalyzerComponentAdapter: SchemaAnalyzerComponentAdapter; private schemaAnalyzerAdapter: SchemaAnalyzerAdapter;
constructor(options: NotebookTabBaseOptions) { constructor(options: NotebookTabBaseOptions) {
super(options); super(options);
this.schemaAnalyzerComponentAdapter = new SchemaAnalyzerComponentAdapter( this.schemaAnalyzerAdapter = new SchemaAnalyzerAdapter(
{ {
contentRef: undefined, contentRef: undefined,
notebookClient: NotebookTabBase.clientManager, notebookClient: NotebookTabBase.clientManager,
@@ -17,6 +20,21 @@ export default class SchemaAnalyzerTab extends NotebookTabBase {
); );
} }
public onActivate(): void {
traceSuccess(
Action.Tab,
{
databaseName: this.collection?.databaseId,
collectionName: this.collection?.id,
dataExplorerArea: Constants.Areas.Tab,
tabTitle: "Schema",
},
this.onLoadStartKey
);
super.onActivate();
}
protected buildCommandBarOptions(): void { protected buildCommandBarOptions(): void {
this.updateNavbarWithTabsButtons(); this.updateNavbarWithTabsButtons();
} }

View File

@@ -90,6 +90,7 @@ export default class Collection implements ViewModels.Collection {
public storedProceduresFocused: ko.Observable<boolean>; public storedProceduresFocused: ko.Observable<boolean>;
public userDefinedFunctionsFocused: ko.Observable<boolean>; public userDefinedFunctionsFocused: ko.Observable<boolean>;
public triggersFocused: ko.Observable<boolean>; public triggersFocused: ko.Observable<boolean>;
private isOfferRead: boolean;
constructor(container: Explorer, databaseId: string, data: DataModels.Collection) { constructor(container: Explorer, databaseId: string, data: DataModels.Collection) {
this.nodeKind = "Collection"; this.nodeKind = "Collection";
@@ -201,6 +202,7 @@ export default class Collection implements ViewModels.Collection {
this.isStoredProceduresExpanded = ko.observable<boolean>(false); this.isStoredProceduresExpanded = ko.observable<boolean>(false);
this.isUserDefinedFunctionsExpanded = ko.observable<boolean>(false); this.isUserDefinedFunctionsExpanded = ko.observable<boolean>(false);
this.isTriggersExpanded = ko.observable<boolean>(false); this.isTriggersExpanded = ko.observable<boolean>(false);
this.isOfferRead = false;
} }
public expandCollapseCollection() { public expandCollapseCollection() {
@@ -509,7 +511,7 @@ export default class Collection implements ViewModels.Collection {
this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer); this.selectedSubnodeKind(ViewModels.CollectionTabKind.SchemaAnalyzer);
const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default; const SchemaAnalyzerTab = await (await import("../Tabs/SchemaAnalyzerTab")).default;
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
description: "Mongo Schema node", description: "Schema node",
databaseName: this.databaseId, databaseName: this.databaseId,
collectionName: this.id(), collectionName: this.id(),
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
@@ -1143,7 +1145,7 @@ export default class Collection implements ViewModels.Collection {
} }
public async loadOffer(): Promise<void> { public async loadOffer(): Promise<void> {
if (!this.container.isServerlessEnabled() && !this.offer()) { if (!this.isOfferRead && !this.container.isServerlessEnabled() && !this.offer()) {
const startKey: number = TelemetryProcessor.traceStart(Action.LoadOffers, { const startKey: number = TelemetryProcessor.traceStart(Action.LoadOffers, {
databaseName: this.databaseId, databaseName: this.databaseId,
collectionName: this.id(), collectionName: this.id(),
@@ -1158,6 +1160,7 @@ export default class Collection implements ViewModels.Collection {
try { try {
this.offer(await readCollectionOffer(params)); this.offer(await readCollectionOffer(params));
this.usageSizeInKB(await getCollectionUsageSizeInKB(this.databaseId, this.id())); this.usageSizeInKB(await getCollectionUsageSizeInKB(this.databaseId, this.id()));
this.isOfferRead = true;
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadOffers, Action.LoadOffers,

View File

@@ -30,6 +30,7 @@ export default class Database implements ViewModels.Database {
public isDatabaseShared: ko.Computed<boolean>; public isDatabaseShared: ko.Computed<boolean>;
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>; public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
public junoClient: JunoClient; public junoClient: JunoClient;
private isOfferRead: boolean;
constructor(container: Explorer, data: any) { constructor(container: Explorer, data: any) {
this.nodeKind = "Database"; this.nodeKind = "Database";
@@ -45,6 +46,7 @@ export default class Database implements ViewModels.Database {
return this.offer && !!this.offer(); return this.offer && !!this.offer();
}); });
this.junoClient = new JunoClient(); this.junoClient = new JunoClient();
this.isOfferRead = false;
} }
public onSettingsClick = () => { public onSettingsClick = () => {
@@ -214,12 +216,13 @@ export default class Database implements ViewModels.Database {
} }
public async loadOffer(): Promise<void> { public async loadOffer(): Promise<void> {
if (!this.container.isServerlessEnabled() && !this.offer()) { if (!this.isOfferRead && !this.container.isServerlessEnabled() && !this.offer()) {
const params: DataModels.ReadDatabaseOfferParams = { const params: DataModels.ReadDatabaseOfferParams = {
databaseId: this.id(), databaseId: this.id(),
databaseResourceId: this.self, databaseResourceId: this.self,
}; };
this.offer(await readDatabaseOffer(params)); this.offer(await readDatabaseOffer(params));
this.isOfferRead = true;
} }
} }

View File

@@ -1,5 +1,5 @@
import * as ko from "knockout";
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react"; import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
import * as ko from "knockout";
import * as React from "react"; import * as React from "react";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg"; import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import DeleteIcon from "../../../images/delete.svg"; import DeleteIcon from "../../../images/delete.svg";
@@ -273,7 +273,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection), contextMenu: ResourceTreeContextMenuButtonFactory.createCollectionContextMenuButton(this.container, collection),
}); });
if (userContext.apiType === "Mongo" && userContext.features.enableSchemaAnalyzer) { if (
userContext.apiType === "Mongo" &&
this.container.isNotebookEnabled() &&
userContext.features.enableSchemaAnalyzer
) {
children.push({ children.push({
label: "Schema (Preview)", label: "Schema (Preview)",
onClick: collection.onSchemaAnalyzerClick.bind(collection), onClick: collection.onSchemaAnalyzerClick.bind(collection),

View File

@@ -14,8 +14,6 @@ import "../externals/jquery.typeahead.min.js";
import "../images/CosmosDB_rgb_ui_lighttheme.ico"; import "../images/CosmosDB_rgb_ui_lighttheme.ico";
import "../images/favicon.ico"; import "../images/favicon.ico";
import hdeConnectImage from "../images/HdeConnectCosmosDB.svg"; import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
import arrowLeftImg from "../images/imgarrowlefticon.svg";
import refreshImg from "../images/refresh-cosmos.svg";
import "../less/documentDB.less"; import "../less/documentDB.less";
import "../less/forms.less"; import "../less/forms.less";
import "../less/infobox.less"; import "../less/infobox.less";
@@ -27,7 +25,8 @@ import "../less/TableStyles/EntityEditor.less";
import "../less/TableStyles/fulldatatables.less"; import "../less/TableStyles/fulldatatables.less";
import "../less/TableStyles/queryBuilder.less"; import "../less/TableStyles/queryBuilder.less";
import "../less/tree.less"; import "../less/tree.less";
import { AuthType } from "./AuthType"; import { CollapsedResourceTree } from "./Common/CollapsedResourceTree";
import { ResourceTree } from "./Common/ResourceTree";
import "./Explorer/Controls/Accordion/AccordionComponent.less"; import "./Explorer/Controls/Accordion/AccordionComponent.less";
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
import { Dialog, DialogProps } from "./Explorer/Controls/Dialog"; import { Dialog, DialogProps } from "./Explorer/Controls/Dialog";
@@ -54,7 +53,6 @@ import { useSidePanel } from "./hooks/useSidePanel";
import { useTabs } from "./hooks/useTabs"; import { useTabs } from "./hooks/useTabs";
import "./Libs/jquery"; import "./Libs/jquery";
import "./Shared/appInsights"; import "./Shared/appInsights";
import { userContext } from "./UserContext";
initializeIcons(); initializeIcons();
@@ -63,6 +61,7 @@ const App: React.FunctionComponent = () => {
const [notificationConsoleData, setNotificationConsoleData] = useState(undefined); const [notificationConsoleData, setNotificationConsoleData] = useState(undefined);
//TODO: Refactor so we don't need to pass the id to remove a console data //TODO: Refactor so we don't need to pass the id to remove a console data
const [inProgressConsoleDataIdToBeDeleted, setInProgressConsoleDataIdToBeDeleted] = useState(""); const [inProgressConsoleDataIdToBeDeleted, setInProgressConsoleDataIdToBeDeleted] = useState("");
const [isLeftPaneExpanded, setIsLeftPaneExpanded] = useState<boolean>(true);
const [dialogProps, setDialogProps] = useState<DialogProps>(); const [dialogProps, setDialogProps] = useState<DialogProps>();
const [showDialog, setShowDialog] = useState<boolean>(false); const [showDialog, setShowDialog] = useState<boolean>(false);
@@ -92,6 +91,15 @@ const App: React.FunctionComponent = () => {
const config = useConfig(); const config = useConfig();
const explorer = useKnockoutExplorer(config?.platform, explorerParams); const explorer = useKnockoutExplorer(config?.platform, explorerParams);
const toggleLeftPaneExpanded = () => {
setIsLeftPaneExpanded(!isLeftPaneExpanded);
if (isLeftPaneExpanded) {
document.getElementById("expandToggleLeftPaneButton").focus();
} else {
document.getElementById("collapseToggleLeftPaneButton").focus();
}
};
if (!explorer) { if (!explorer) {
return <LoadingExplorer />; return <LoadingExplorer />;
} }
@@ -107,96 +115,13 @@ const App: React.FunctionComponent = () => {
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree"> <div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
<div className="collectionsTreeWithSplitter"> <div className="collectionsTreeWithSplitter">
{/* Collections Tree Expanded - Start */} {/* Collections Tree Expanded - Start */}
<div <ResourceTree toggleLeftPaneExpanded={toggleLeftPaneExpanded} isLeftPaneExpanded={isLeftPaneExpanded} />
id="main"
className="main"
data-bind="
visible: isLeftPaneExpanded()"
>
{/* Collections Window - - Start */}
<div id="mainslide" className="flexContainer">
{/* Collections Window Title/Command Bar - Start */}
<div className="collectiontitle">
<div className="coltitle">
<span className="titlepadcol" data-bind="text: collectionTitle" />
<div className="float-right">
<span
className="padimgcolrefresh"
data-test="refreshTree"
role="button"
data-bind="
click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }"
tabIndex={0}
aria-label="Refresh tree"
title="Refresh tree"
>
<img
className="refreshcol"
src={refreshImg}
data-bind="attr: { alt: refreshTreeTitle }"
alt="Refresh"
/>
</span>
<span
className="padimgcolrefresh1"
id="expandToggleLeftPaneButton"
role="button"
data-bind="
click: toggleLeftPaneExpanded, event: { keypress: toggleLeftPaneExpandedKeyPress }"
tabIndex={0}
aria-label="Collapse Tree"
title="Collapse Tree"
>
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
</span>
</div>
</div>
</div>
{userContext.authType === AuthType.ResourceToken ? (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTreeForResourceToken" />
) : (
<div style={{ overflowY: "auto" }} data-bind="react:resourceTree" />
)}
</div>
{/* Collections Window - End */}
</div>
{/* Collections Tree Expanded - End */} {/* Collections Tree Expanded - End */}
{/* Collections Tree Collapsed - Start */} {/* Collections Tree Collapsed - Start */}
<div <CollapsedResourceTree
id="mini" toggleLeftPaneExpanded={toggleLeftPaneExpanded}
className="mini toggle-mini" isLeftPaneExpanded={isLeftPaneExpanded}
data-bind="visible: !isLeftPaneExpanded() />
attr: { style: { width: collapsedResourceTreeWidth }}"
>
<div className="main-nav nav">
<ul className="nav">
<li
className="resourceTreeCollapse"
id="collapseToggleLeftPaneButton"
data-bind="event: { keypress: toggleLeftPaneExpandedKeyPress }"
aria-label="Expand Tree"
>
<span
className="leftarrowCollapsed"
data-bind="
click: toggleLeftPaneExpanded"
>
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
</span>
<span
className="collectionCollapsed"
data-bind="
click: toggleLeftPaneExpanded"
>
<span
data-bind="
text: collectionTitle"
/>
</span>
</li>
</ul>
</div>
</div>
{/* Collections Tree Collapsed - End */} {/* Collections Tree Collapsed - End */}
</div> </div>
{/* Splitter - Start */} {/* Splitter - Start */}
@@ -230,7 +155,6 @@ const App: React.FunctionComponent = () => {
isConsoleExpanded={isNotificationConsoleExpanded} isConsoleExpanded={isNotificationConsoleExpanded}
/> />
<div data-bind='component: { name: "add-database-pane", params: {data: addDatabasePane} }' /> <div data-bind='component: { name: "add-database-pane", params: {data: addDatabasePane} }' />
<div data-bind='component: { name: "graph-styling-pane", params: { data: graphStylingPane} }' />
<div data-bind='component: { name: "cassandra-add-collection-pane", params: { data: cassandraAddCollectionPane} }' /> <div data-bind='component: { name: "cassandra-add-collection-pane", params: { data: cassandraAddCollectionPane} }' />
{showDialog && <Dialog {...dialogProps} />} {showDialog && <Dialog {...dialogProps} />}
</div> </div>

View File

@@ -8,7 +8,7 @@ export type Features = {
readonly enableReactPane: boolean; readonly enableReactPane: boolean;
readonly enableRightPanelV2: boolean; readonly enableRightPanelV2: boolean;
readonly enableSchema: boolean; readonly enableSchema: boolean;
readonly enableSchemaAnalyzer: boolean; enableSchemaAnalyzer: boolean;
readonly enableSDKoperations: boolean; readonly enableSDKoperations: boolean;
readonly enableSpark: boolean; readonly enableSpark: boolean;
readonly enableTtl: boolean; readonly enableTtl: boolean;

View File

@@ -116,6 +116,7 @@ export enum Action {
NotebooksGalleryPublishedCount, NotebooksGalleryPublishedCount,
SelfServe, SelfServe,
ExpandAddCollectionPaneAdvancedSection, ExpandAddCollectionPaneAdvancedSection,
SchemaAnalyzerClickAnalyze,
} }
export const ActionModifiers = { export const ActionModifiers = {

View File

@@ -1,4 +1,7 @@
import * as Constants from "../Common/Constants";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
export const isCapabilityEnabled = (capabilityName: string): boolean => export const isCapabilityEnabled = (capabilityName: string): boolean =>
userContext.databaseAccount?.properties?.capabilities?.some((capability) => capability.name === capabilityName); userContext.databaseAccount?.properties?.capabilities?.some((capability) => capability.name === capabilityName);
export const isServerlessAccount = (): boolean => isCapabilityEnabled(Constants.CapabilityNames.EnableServerless);

View File

@@ -18,7 +18,7 @@ export async function fetchPhoto(accessToken: string): Promise<Blob | void> {
} }
export function useGraphPhoto(graphToken: string): string { export function useGraphPhoto(graphToken: string): string {
const [photo, setPhoto] = useState<string>(); const [photo, setPhoto] = useState<string>("");
useEffect(() => { useEffect(() => {
if (graphToken) { if (graphToken) {

View File

@@ -18,7 +18,7 @@ export const useSidePanel = (): SidePanelHooks => {
setHeaderText(headerText); setHeaderText(headerText);
setPanelContent(panelContent); setPanelContent(panelContent);
setIsPanelOpen(true); setIsPanelOpen(true);
setOnCloseCallback({ callback: onClose }); !!onClose && setOnCloseCallback({ callback: onClose });
}; };
const closeSidePanel = (): void => { const closeSidePanel = (): void => {

View File

@@ -0,0 +1,37 @@
import { jest } from "@jest/globals";
import "expect-playwright";
import { safeClick } from "../utils/safeClick";
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared";
jest.setTimeout(240000);
test("Mongo CRUD", async () => {
const databaseId = generateDatabaseNameWithTimestamp();
const containerId = generateUniqueName("container");
await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner");
await page.waitForSelector("iframe");
const explorer = page.frame({
name: "explorer",
});
// Create new database and collection
await explorer.click('[data-test="New Collection"]');
await explorer.fill('[aria-label="New database id"]', databaseId);
await explorer.fill('[aria-label="Collection id"]', containerId);
await explorer.fill('[aria-label="Shard key"]', "pk");
await explorer.click("#sidePanelOkButton");
await safeClick(explorer, `.nodeItem >> text=${databaseId}`);
await safeClick(explorer, `.nodeItem >> text=${containerId}`);
// Delete database and collection
await safeClick(explorer, `[data-test="${containerId}"] [aria-label="More"]`);
await safeClick(explorer, 'button[role="menuitem"]:has-text("Delete Collection")');
await explorer.fill('text=* Confirm by typing the collection id >> input[type="text"]', containerId);
await explorer.click('[aria-label="Submit"]');
await explorer.click(`[data-test="${databaseId}"] [aria-label="More"]`);
await explorer.click('button[role="menuitem"]:has-text("Delete Database")');
await explorer.click('text=* Confirm by typing the database id >> input[type="text"]');
await explorer.fill('text=* Confirm by typing the database id >> input[type="text"]', databaseId);
await explorer.click("#sidePanelOkButton");
await expect(explorer).not.toHaveText(".dataResourceTree", databaseId);
await expect(explorer).not.toHaveText(".dataResourceTree", containerId);
});

View File

@@ -36,8 +36,8 @@
"./src/Contracts/SelfServeContracts.ts", "./src/Contracts/SelfServeContracts.ts",
"./src/Contracts/SubscriptionType.ts", "./src/Contracts/SubscriptionType.ts",
"./src/Contracts/Versions.ts", "./src/Contracts/Versions.ts",
"./src/Explorer/Controls/Dialog.tsx",
"./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts", "./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts",
"./src/Explorer/Controls/InputTypeahead/InputTypeahead.ts",
"./src/Explorer/Controls/SmartUi/InputUtils.ts", "./src/Explorer/Controls/SmartUi/InputUtils.ts",
"./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts", "./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts",
"./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts", "./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts",
@@ -53,12 +53,16 @@
"./src/Explorer/Notebook/NotebookContentClient.ts", "./src/Explorer/Notebook/NotebookContentClient.ts",
"./src/Explorer/Notebook/NotebookContentItem.ts", "./src/Explorer/Notebook/NotebookContentItem.ts",
"./src/Explorer/Notebook/NotebookRenderer/AzureTheme.tsx", "./src/Explorer/Notebook/NotebookRenderer/AzureTheme.tsx",
"./src/Explorer/Notebook/NotebookRenderer/Prompt.tsx",
"./src/Explorer/Notebook/NotebookRenderer/PromptContent.tsx",
"./src/Explorer/Notebook/NotebookRenderer/decorators/CellCreator.tsx", "./src/Explorer/Notebook/NotebookRenderer/decorators/CellCreator.tsx",
"./src/Explorer/Notebook/NotebookRenderer/decorators/CellLabeler.tsx",
"./src/Explorer/Notebook/NotebookUtil.ts", "./src/Explorer/Notebook/NotebookUtil.ts",
"./src/Explorer/OpenFullScreen.test.tsx", "./src/Explorer/OpenFullScreen.test.tsx",
"./src/Explorer/OpenFullScreen.tsx", "./src/Explorer/OpenFullScreen.tsx",
"./src/Explorer/Panes/PaneComponents.ts", "./src/Explorer/Panes/PaneComponents.ts",
"./src/Explorer/Panes/PanelFooterComponent.tsx", "./src/Explorer/Panes/PanelFooterComponent.tsx",
"./src/Explorer/Panes/PanelInfoErrorComponent.tsx",
"./src/Explorer/Panes/PanelLoadingScreen.tsx", "./src/Explorer/Panes/PanelLoadingScreen.tsx",
"./src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts", "./src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts",
"./src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts", "./src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts",
@@ -74,6 +78,8 @@
"./src/Index.ts", "./src/Index.ts",
"./src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts", "./src/NotebookWorkspaceManager/NotebookWorkspaceResourceProviderMockClients.ts",
"./src/Platform/Hosted/Authorization.ts", "./src/Platform/Hosted/Authorization.ts",
"./src/Platform/Hosted/Components/MeControl.test.tsx",
"./src/Platform/Hosted/Components/MeControl.tsx",
"./src/Platform/Hosted/Components/SignInButton.tsx", "./src/Platform/Hosted/Components/SignInButton.tsx",
"./src/Platform/Hosted/extractFeatures.test.ts", "./src/Platform/Hosted/extractFeatures.test.ts",
"./src/Platform/Hosted/extractFeatures.ts", "./src/Platform/Hosted/extractFeatures.ts",
@@ -107,6 +113,7 @@
"./src/Utils/WindowUtils.ts", "./src/Utils/WindowUtils.ts",
"./src/hooks/useDirectories.tsx", "./src/hooks/useDirectories.tsx",
"./src/hooks/useFullScreenURLs.tsx", "./src/hooks/useFullScreenURLs.tsx",
"./src/hooks/useGraphPhoto.tsx",
"./src/hooks/useObservable.ts", "./src/hooks/useObservable.ts",
"./src/i18n.ts", "./src/i18n.ts",
"./src/quickstart.ts", "./src/quickstart.ts",
@@ -115,13 +122,16 @@
], ],
"include": [ "include": [
"src/CellOutputViewer/transforms/**/*", "src/CellOutputViewer/transforms/**/*",
"src/Common/Tooltip/**/*",
"src/Controls/**/*", "src/Controls/**/*",
"src/Definitions/**/*", "src/Definitions/**/*",
"src/Explorer/Controls/ErrorDisplayComponent/**/*", "src/Explorer/Controls/ErrorDisplayComponent/**/*",
"src/Explorer/Controls/Header/**/*",
"src/Explorer/Controls/RadioSwitchComponent/**/*", "src/Explorer/Controls/RadioSwitchComponent/**/*",
"src/Explorer/Controls/ResizeSensorReactComponent/**/*", "src/Explorer/Controls/ResizeSensorReactComponent/**/*",
"src/Explorer/Graph/GraphExplorerComponent/__mocks__/**/*", "src/Explorer/Graph/GraphExplorerComponent/__mocks__/**/*",
"src/Explorer/Notebook/NotebookComponent/__mocks__/**/*", "src/Explorer/Notebook/NotebookComponent/__mocks__/**/*",
"src/Explorer/Panes/RightPaneForm/**/*",
"src/Libs/**/*", "src/Libs/**/*",
"src/Localization/**/*", "src/Localization/**/*",
"src/Platform/Emulator/**/*", "src/Platform/Emulator/**/*",