2021-01-20 09:15:01 -06:00
|
|
|
import * as ko from "knockout";
|
2021-04-22 20:17:59 +05:30
|
|
|
import React from "react";
|
2021-01-20 09:15:01 -06:00
|
|
|
import NewVertexIcon from "../../../images/NewVertex.svg";
|
|
|
|
import StyleIcon from "../../../images/Style.svg";
|
|
|
|
import { DatabaseAccount } from "../../Contracts/DataModels";
|
2021-04-22 20:17:59 +05:30
|
|
|
import * as ViewModels from "../../Contracts/ViewModels";
|
2021-01-20 09:15:01 -06:00
|
|
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
2021-04-22 20:17:59 +05:30
|
|
|
import { GraphAccessor, GraphExplorerError } from "../Graph/GraphExplorerComponent/GraphExplorer";
|
|
|
|
import { GraphExplorerAdapter } from "../Graph/GraphExplorerComponent/GraphExplorerAdapter";
|
|
|
|
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
|
|
|
|
import GraphStylingPane from "../Panes/GraphStylingPane";
|
|
|
|
import { NewVertexPanel } from "../Panes/NewVertexPanel/NewVertexPanel";
|
|
|
|
import TabsBase from "./TabsBase";
|
2021-01-20 09:15:01 -06:00
|
|
|
export interface GraphIconMap {
|
|
|
|
[key: string]: { data: string; format: string };
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface GraphConfig {
|
|
|
|
nodeColor: ko.Observable<string>;
|
2021-04-22 20:17:59 +05:30
|
|
|
nodeColorKey: ko.Observable<string>; // map property to node color. Takes precedence over nodeColor unless undefined
|
2021-01-20 09:15:01 -06:00
|
|
|
linkColor: ko.Observable<string>;
|
|
|
|
showNeighborType: ko.Observable<ViewModels.NeighborType>;
|
|
|
|
nodeCaption: ko.Observable<string>;
|
|
|
|
nodeSize: ko.Observable<number>;
|
|
|
|
linkWidth: ko.Observable<number>;
|
|
|
|
nodeIconKey: ko.Observable<string>;
|
|
|
|
iconsMap: ko.Observable<GraphIconMap>;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface GraphTabOptions extends ViewModels.TabOptions {
|
|
|
|
account: DatabaseAccount;
|
|
|
|
masterKey: string;
|
|
|
|
collectionId: string;
|
|
|
|
databaseId: string;
|
|
|
|
collectionPartitionKeyProperty: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export default class GraphTab extends TabsBase {
|
2021-04-23 19:53:48 -07:00
|
|
|
public readonly html =
|
|
|
|
'<div class="graphExplorerContainer" role="tabpanel" data-bind="react:graphExplorerAdapter, attr: {id: tabId}"></div>';
|
2021-01-20 09:15:01 -06:00
|
|
|
// Graph default configuration
|
|
|
|
public static readonly DEFAULT_NODE_CAPTION = "id";
|
|
|
|
private static readonly LINK_COLOR = "#aaa";
|
|
|
|
private static readonly NODE_SIZE = 10;
|
|
|
|
private static readonly NODE_COLOR = "orange";
|
|
|
|
private static readonly LINK_WIDTH = 1;
|
|
|
|
private graphExplorerAdapter: GraphExplorerAdapter;
|
|
|
|
private isNewVertexDisabled: ko.Observable<boolean>;
|
|
|
|
private isPropertyEditing: ko.Observable<boolean>;
|
|
|
|
private isGraphDisplayed: ko.Observable<boolean>;
|
|
|
|
private graphAccessor: GraphAccessor;
|
|
|
|
private graphConfig: GraphConfig;
|
|
|
|
private graphConfigUiData: ViewModels.GraphConfigUiData;
|
|
|
|
private isFilterQueryLoading: ko.Observable<boolean>;
|
|
|
|
private isValidQuery: ko.Observable<boolean>;
|
|
|
|
private graphStylingPane: GraphStylingPane;
|
|
|
|
private collectionPartitionKeyProperty: string;
|
2021-04-22 20:17:59 +05:30
|
|
|
private contextualPane: ContextualPaneBase;
|
2021-01-20 09:15:01 -06:00
|
|
|
|
|
|
|
constructor(options: GraphTabOptions) {
|
|
|
|
super(options);
|
|
|
|
|
|
|
|
this.graphStylingPane = options.collection && options.collection.container.graphStylingPane;
|
|
|
|
this.collectionPartitionKeyProperty = options.collectionPartitionKeyProperty;
|
|
|
|
|
|
|
|
this.isNewVertexDisabled = ko.observable(false);
|
|
|
|
this.isPropertyEditing = ko.observable(false);
|
|
|
|
this.isGraphDisplayed = ko.observable(false);
|
2021-04-22 20:17:59 +05:30
|
|
|
this.graphAccessor = undefined;
|
2021-01-20 09:15:01 -06:00
|
|
|
this.graphConfig = GraphTab.createGraphConfig();
|
|
|
|
// TODO Merge this with this.graphConfig
|
|
|
|
this.graphConfigUiData = GraphTab.createGraphConfigUiData(this.graphConfig);
|
|
|
|
this.graphExplorerAdapter = new GraphExplorerAdapter({
|
|
|
|
onGraphAccessorCreated: (instance: GraphAccessor): void => {
|
|
|
|
this.graphAccessor = instance;
|
|
|
|
},
|
|
|
|
onIsNewVertexDisabledChange: (isDisabled: boolean): void => {
|
|
|
|
this.isNewVertexDisabled(isDisabled);
|
|
|
|
this.updateNavbarWithTabsButtons();
|
|
|
|
},
|
|
|
|
onIsPropertyEditing: (isEditing: boolean) => {
|
|
|
|
this.isPropertyEditing(isEditing);
|
|
|
|
this.updateNavbarWithTabsButtons();
|
|
|
|
},
|
|
|
|
onIsGraphDisplayed: (isDisplayed: boolean) => {
|
|
|
|
this.isGraphDisplayed(isDisplayed);
|
|
|
|
this.updateNavbarWithTabsButtons();
|
|
|
|
},
|
|
|
|
onResetDefaultGraphConfigValues: () => this.setDefaultGraphConfigValues(),
|
|
|
|
graphConfig: this.graphConfig,
|
|
|
|
graphConfigUiData: this.graphConfigUiData,
|
|
|
|
onIsFilterQueryLoading: (isFilterQueryLoading: boolean): void => this.isFilterQueryLoading(isFilterQueryLoading),
|
|
|
|
onIsValidQuery: (isValidQuery: boolean): void => this.isValidQuery(isValidQuery),
|
|
|
|
collectionPartitionKeyProperty: options.collectionPartitionKeyProperty,
|
|
|
|
graphBackendEndpoint: GraphTab.getGremlinEndpoint(options.account),
|
|
|
|
databaseId: options.databaseId,
|
|
|
|
collectionId: options.collectionId,
|
|
|
|
masterKey: options.masterKey,
|
|
|
|
onLoadStartKey: options.onLoadStartKey,
|
|
|
|
onLoadStartKeyChange: (onLoadStartKey: number): void => {
|
2021-04-22 20:17:59 +05:30
|
|
|
if (onLoadStartKey === undefined) {
|
|
|
|
this.onLoadStartKey = undefined;
|
2021-01-20 09:15:01 -06:00
|
|
|
}
|
|
|
|
},
|
|
|
|
resourceId: options.account.id,
|
|
|
|
});
|
|
|
|
|
|
|
|
this.isFilterQueryLoading = ko.observable(false);
|
|
|
|
this.isValidQuery = ko.observable(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static getGremlinEndpoint(account: DatabaseAccount): string {
|
|
|
|
return account.properties.gremlinEndpoint
|
|
|
|
? GraphTab.sanitizeHost(account.properties.gremlinEndpoint)
|
|
|
|
: `${account.name}.graphs.azure.com:443/`;
|
|
|
|
}
|
|
|
|
|
|
|
|
public onTabClick(): void {
|
|
|
|
super.onTabClick();
|
|
|
|
this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Graph);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removing leading http|https and remove trailing /
|
|
|
|
* @param url
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
private static sanitizeHost(url: string): string {
|
|
|
|
if (!url) {
|
|
|
|
return url;
|
|
|
|
}
|
|
|
|
return url.replace(/^(http|https):\/\//, "").replace(/\/$/, "");
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Command bar */
|
|
|
|
private showNewVertexEditor(): void {
|
2021-04-22 20:17:59 +05:30
|
|
|
this.collection.container.openSidePanel(
|
|
|
|
"New Vertex",
|
|
|
|
<NewVertexPanel
|
|
|
|
explorer={this.collection.container}
|
|
|
|
partitionKeyPropertyProp={this.collectionPartitionKeyProperty}
|
|
|
|
openNotificationConsole={() => this.collection.container.expandConsole()}
|
|
|
|
onSubmit={(
|
|
|
|
result: ViewModels.NewVertexData,
|
|
|
|
onError: (errorMessage: string) => void,
|
|
|
|
onSuccess: () => void
|
|
|
|
): void => {
|
|
|
|
this.graphAccessor.addVertex(result).then(
|
|
|
|
() => {
|
|
|
|
onSuccess();
|
|
|
|
this.contextualPane.cancel();
|
|
|
|
},
|
|
|
|
(error: GraphExplorerError) => {
|
|
|
|
onError(error.title);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
2021-01-20 09:15:01 -06:00
|
|
|
}
|
2021-04-22 20:17:59 +05:30
|
|
|
public openStyling(): void {
|
2021-01-20 09:15:01 -06:00
|
|
|
this.setDefaultGraphConfigValues();
|
|
|
|
// Update the styling pane with this instance
|
|
|
|
this.graphStylingPane.setData(this.graphConfigUiData);
|
|
|
|
this.graphStylingPane.open();
|
|
|
|
}
|
|
|
|
|
|
|
|
public static createGraphConfig(): GraphConfig {
|
|
|
|
return {
|
|
|
|
nodeColor: ko.observable(GraphTab.NODE_COLOR),
|
2021-04-22 20:17:59 +05:30
|
|
|
nodeColorKey: ko.observable(undefined),
|
2021-01-20 09:15:01 -06:00
|
|
|
linkColor: ko.observable(GraphTab.LINK_COLOR),
|
|
|
|
showNeighborType: ko.observable(ViewModels.NeighborType.TARGETS_ONLY),
|
|
|
|
nodeCaption: ko.observable(GraphTab.DEFAULT_NODE_CAPTION),
|
|
|
|
nodeSize: ko.observable(GraphTab.NODE_SIZE),
|
|
|
|
linkWidth: ko.observable(GraphTab.LINK_WIDTH),
|
2021-04-22 20:17:59 +05:30
|
|
|
nodeIconKey: ko.observable(undefined),
|
2021-01-20 09:15:01 -06:00
|
|
|
iconsMap: ko.observable({}),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public static createGraphConfigUiData(graphConfig: GraphConfig): ViewModels.GraphConfigUiData {
|
|
|
|
return {
|
|
|
|
showNeighborType: ko.observable(graphConfig.showNeighborType()),
|
|
|
|
nodeProperties: ko.observableArray([]),
|
|
|
|
nodePropertiesWithNone: ko.observableArray([]),
|
|
|
|
nodeCaptionChoice: ko.observable(graphConfig.nodeCaption()),
|
|
|
|
nodeColorKeyChoice: ko.observable(graphConfig.nodeColorKey()),
|
|
|
|
nodeIconChoice: ko.observable(graphConfig.nodeIconKey()),
|
2021-04-22 20:17:59 +05:30
|
|
|
nodeIconSet: ko.observable(undefined),
|
2021-01-20 09:15:01 -06:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-04-22 20:17:59 +05:30
|
|
|
* Make sure graph config values are not undefined
|
2021-01-20 09:15:01 -06:00
|
|
|
*/
|
|
|
|
private setDefaultGraphConfigValues() {
|
2021-04-22 20:17:59 +05:30
|
|
|
// Assign default values if undefined
|
|
|
|
if (
|
|
|
|
this.graphConfigUiData.nodeCaptionChoice() === undefined &&
|
|
|
|
this.graphConfigUiData.nodeProperties().length > 1
|
|
|
|
) {
|
2021-01-20 09:15:01 -06:00
|
|
|
this.graphConfigUiData.nodeCaptionChoice(this.graphConfigUiData.nodeProperties()[0]);
|
|
|
|
}
|
|
|
|
if (
|
2021-04-22 20:17:59 +05:30
|
|
|
this.graphConfigUiData.nodeColorKeyChoice() === undefined &&
|
2021-01-20 09:15:01 -06:00
|
|
|
this.graphConfigUiData.nodePropertiesWithNone().length > 1
|
|
|
|
) {
|
|
|
|
this.graphConfigUiData.nodeColorKeyChoice(this.graphConfigUiData.nodePropertiesWithNone()[0]);
|
|
|
|
}
|
|
|
|
if (
|
2021-04-22 20:17:59 +05:30
|
|
|
this.graphConfigUiData.nodeIconChoice() === undefined &&
|
2021-01-20 09:15:01 -06:00
|
|
|
this.graphConfigUiData.nodePropertiesWithNone().length > 1
|
|
|
|
) {
|
|
|
|
this.graphConfigUiData.nodeIconChoice(this.graphConfigUiData.nodePropertiesWithNone()[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
protected getTabsButtons(): CommandButtonComponentProps[] {
|
|
|
|
const label = "New Vertex";
|
|
|
|
const buttons: CommandButtonComponentProps[] = [
|
|
|
|
{
|
|
|
|
iconSrc: NewVertexIcon,
|
|
|
|
iconAlt: label,
|
|
|
|
onCommandClick: () => this.showNewVertexEditor(),
|
|
|
|
commandButtonLabel: label,
|
|
|
|
ariaLabel: label,
|
|
|
|
hasPopup: false,
|
|
|
|
disabled: this.isNewVertexDisabled(),
|
|
|
|
},
|
|
|
|
];
|
|
|
|
buttons.push();
|
|
|
|
if (this.isGraphDisplayed()) {
|
|
|
|
const label = "Style";
|
|
|
|
buttons.push({
|
|
|
|
iconSrc: StyleIcon,
|
|
|
|
iconAlt: label,
|
|
|
|
onCommandClick: () => this.openStyling(),
|
|
|
|
commandButtonLabel: label,
|
|
|
|
ariaLabel: label,
|
|
|
|
hasPopup: false,
|
|
|
|
disabled: this.isPropertyEditing(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return buttons;
|
|
|
|
}
|
|
|
|
protected buildCommandBarOptions(): void {
|
|
|
|
ko.computed(() => ko.toJSON([this.isNewVertexDisabled])).subscribe(() => this.updateNavbarWithTabsButtons());
|
|
|
|
this.updateNavbarWithTabsButtons();
|
|
|
|
}
|
|
|
|
}
|