import * as ko from "knockout"; import * as Q from "q"; import * as ViewModels from "../../Contracts/ViewModels"; import TabsBase from "./TabsBase"; import Toolbar from "../Controls/Toolbar/Toolbar"; import { GraphExplorerAdapter } from "../Graph/GraphExplorerComponent/GraphExplorerAdapter"; import { GraphAccessor, GraphExplorerError } from "../Graph/GraphExplorerComponent/GraphExplorer"; import NewVertexIcon from "../../../images/NewVertex.svg"; import StyleIcon from "../../../images/Style.svg"; import GraphStylingPane from "../Panes/GraphStylingPane"; import NewVertexPane from "../Panes/NewVertexPane"; export interface GraphIconMap { [key: string]: { data: string; format: string }; } export interface GraphConfig { nodeColor: ko.Observable; nodeColorKey: ko.Observable; // map property to node color. Takes precedence over nodeColor unless null linkColor: ko.Observable; showNeighborType: ko.Observable; nodeCaption: ko.Observable; nodeSize: ko.Observable; linkWidth: ko.Observable; nodeIconKey: ko.Observable; iconsMap: ko.Observable; } export default class GraphTab extends TabsBase implements ViewModels.Tab { // 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; private isPropertyEditing: ko.Observable; private isGraphDisplayed: ko.Observable; private graphAccessor: GraphAccessor; public toolbarViewModel: ko.Observable; private graphConfig: GraphConfig; private graphConfigUiData: ViewModels.GraphConfigUiData; private isFilterQueryLoading: ko.Observable; private isValidQuery: ko.Observable; private newVertexPane: NewVertexPane; private graphStylingPane: GraphStylingPane; private collectionPartitionKeyProperty: string; constructor(options: ViewModels.GraphTabOptions) { super(options); this.newVertexPane = options.collection && options.collection.container.newVertexPane; 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); this.graphAccessor = null; 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, documentClientUtility: this.documentClientUtility, collectionRid: this.rid, collectionSelfLink: options.selfLink, graphBackendEndpoint: GraphTab.getGremlinEndpoint(options.account), databaseId: options.databaseId, collectionId: options.collectionId, masterKey: options.masterKey, onLoadStartKey: options.onLoadStartKey, onLoadStartKeyChange: (onLoadStartKey: number): void => { if (onLoadStartKey == null) { this.onLoadStartKey = null; } }, resourceId: options.account.id }); this.isFilterQueryLoading = ko.observable(false); this.isValidQuery = ko.observable(true); this.documentClientUtility = options.documentClientUtility; this.toolbarViewModel = ko.observable(); } public static getGremlinEndpoint(account: ViewModels.DatabaseAccount): string { return account.properties.gremlinEndpoint ? GraphTab.sanitizeHost(account.properties.gremlinEndpoint) : `${account.name}.graphs.azure.com:443/`; } public onTabClick(): Q.Promise { return super.onTabClick().then(() => { 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 { this.newVertexPane.open(); this.newVertexPane.setPartitionKeyProperty(this.collectionPartitionKeyProperty); // TODO Must update GraphExplorer properties this.newVertexPane.subscribeOnSubmitCreate((result: ViewModels.NewVertexData) => { this.newVertexPane.formErrors(null); this.newVertexPane.formErrorsDetails(null); this.graphAccessor.addVertex(result).then( () => { this.newVertexPane.cancel(); }, (error: GraphExplorerError) => { this.newVertexPane.formErrors(error.title); if (!!error.details) { this.newVertexPane.formErrorsDetails(error.details); } } ); }); } public openStyling() { 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), nodeColorKey: ko.observable(null), 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), nodeIconKey: ko.observable(null), 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()), nodeIconSet: ko.observable(null) }; } /** * Make sure graph config values are not null */ private setDefaultGraphConfigValues() { // Assign default values if null if (this.graphConfigUiData.nodeCaptionChoice() === null && this.graphConfigUiData.nodeProperties().length > 1) { this.graphConfigUiData.nodeCaptionChoice(this.graphConfigUiData.nodeProperties()[0]); } if ( this.graphConfigUiData.nodeColorKeyChoice() === null && this.graphConfigUiData.nodePropertiesWithNone().length > 1 ) { this.graphConfigUiData.nodeColorKeyChoice(this.graphConfigUiData.nodePropertiesWithNone()[0]); } if ( this.graphConfigUiData.nodeIconChoice() === null && this.graphConfigUiData.nodePropertiesWithNone().length > 1 ) { this.graphConfigUiData.nodeIconChoice(this.graphConfigUiData.nodePropertiesWithNone()[0]); } } protected getTabsButtons(): ViewModels.NavbarButtonConfig[] { const label = "New Vertex"; const buttons: ViewModels.NavbarButtonConfig[] = [ { 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(); } }