mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-04-01 15:38:45 +01:00
544 lines
17 KiB
TypeScript
544 lines
17 KiB
TypeScript
/**
|
|
* Graph React component
|
|
* Display of properties
|
|
* The mode is controlled by the parent of this component
|
|
*/
|
|
|
|
import * as React from "react";
|
|
import CancelIcon from "../../../../images/cancel.svg";
|
|
import CheckIcon from "../../../../images/check-1.svg";
|
|
import DeleteIcon from "../../../../images/delete.svg";
|
|
import EditIcon from "../../../../images/edit-1.svg";
|
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
|
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
|
|
import { CollapsiblePanel } from "../../Controls/CollapsiblePanel/CollapsiblePanel";
|
|
import { Item } from "../../Controls/InputTypeahead/InputTypeaheadComponent";
|
|
import { ConsoleDataType } from "../../Menus/NotificationConsole/ConsoleData";
|
|
import * as EditorNeighbors from "./EditorNeighborsComponent";
|
|
import { EditorNodePropertiesComponent } from "./EditorNodePropertiesComponent";
|
|
import {
|
|
EditedEdges,
|
|
EditedProperties,
|
|
GraphExplorer,
|
|
GraphHighlightedNodeData,
|
|
PossibleVertex,
|
|
} from "./GraphExplorer";
|
|
import { ReadOnlyNeighborsComponent } from "./ReadOnlyNeighborsComponent";
|
|
import { ReadOnlyNodePropertiesComponent } from "./ReadOnlyNodePropertiesComponent";
|
|
|
|
export enum Mode {
|
|
READONLY_PROP,
|
|
PROPERTY_EDITOR,
|
|
EDIT_SOURCES,
|
|
EDIT_TARGETS,
|
|
}
|
|
|
|
export interface NodePropertiesComponentProps {
|
|
expandedTitle: string;
|
|
isCollapsed: boolean;
|
|
onCollapsedChanged: (newValue: boolean) => void;
|
|
|
|
node: GraphHighlightedNodeData;
|
|
getPkIdFromNodeData: (v: GraphHighlightedNodeData) => string;
|
|
collectionPartitionKeyProperty: string;
|
|
updateVertexProperties: (editedProperties: EditedProperties) => Q.Promise<void>;
|
|
selectNode: (id: string) => void;
|
|
updatePossibleVertices: () => Q.Promise<PossibleVertex[]>;
|
|
possibleEdgeLabels: Item[];
|
|
editGraphEdges: (editedEdges: EditedEdges) => Q.Promise<any>;
|
|
deleteHighlightedNode: () => void;
|
|
onModeChanged: (newMode: Mode) => void;
|
|
viewMode: Mode; // If viewMode is specified in parent, keep state in sync with it
|
|
}
|
|
|
|
interface NodePropertiesComponentState {
|
|
possibleVertices: PossibleVertex[];
|
|
editedProperties: EditedProperties;
|
|
editedSources: EditedEdges;
|
|
editedTargets: EditedEdges;
|
|
isDeleteConfirm: boolean;
|
|
isPropertiesExpanded: boolean;
|
|
isSourcesExpanded: boolean;
|
|
isTargetsExpanded: boolean;
|
|
}
|
|
|
|
export class NodePropertiesComponent extends React.Component<
|
|
NodePropertiesComponentProps,
|
|
NodePropertiesComponentState
|
|
> {
|
|
private static readonly PROPERTIES_COLLAPSED_TITLE = "Properties";
|
|
|
|
public constructor(props: NodePropertiesComponentProps) {
|
|
super(props);
|
|
this.state = {
|
|
editedProperties: {
|
|
pkId: undefined,
|
|
readOnlyProperties: [],
|
|
existingProperties: [],
|
|
addedProperties: [],
|
|
droppedKeys: [],
|
|
},
|
|
editedSources: {
|
|
vertexId: undefined,
|
|
currentNeighbors: [],
|
|
droppedIds: [],
|
|
addedEdges: [],
|
|
},
|
|
editedTargets: {
|
|
vertexId: undefined,
|
|
currentNeighbors: [],
|
|
droppedIds: [],
|
|
addedEdges: [],
|
|
},
|
|
possibleVertices: [],
|
|
isDeleteConfirm: false,
|
|
isPropertiesExpanded: true,
|
|
isSourcesExpanded: true,
|
|
isTargetsExpanded: true,
|
|
};
|
|
}
|
|
|
|
public static getDerivedStateFromProps(props: NodePropertiesComponentProps): Partial<NodePropertiesComponentState> {
|
|
if (props.viewMode !== Mode.READONLY_PROP) {
|
|
return { isDeleteConfirm: false };
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
public render(): JSX.Element {
|
|
if (!this.props.node) {
|
|
return <span />;
|
|
} else {
|
|
return (
|
|
<CollapsiblePanel
|
|
collapsedTitle={NodePropertiesComponent.PROPERTIES_COLLAPSED_TITLE}
|
|
expandedTitle={this.props.expandedTitle}
|
|
isCollapsed={this.props.isCollapsed}
|
|
onCollapsedChanged={this.props.onCollapsedChanged.bind(this)}
|
|
>
|
|
{this.getHeaderFragment()}
|
|
|
|
<div className="rightPaneContent contentScroll">
|
|
<div className="rightPaneContainer">
|
|
{this.getPropertiesFragment()}
|
|
{this.getNeighborsContainer()}
|
|
</div>
|
|
</div>
|
|
</CollapsiblePanel>
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get type option. Limit to string, number or boolean
|
|
* @param value
|
|
*/
|
|
private static getTypeOption(value: any): ViewModels.InputPropertyValueTypeString {
|
|
if (value === undefined) {
|
|
return "null";
|
|
}
|
|
const type = typeof value;
|
|
switch (type) {
|
|
case "number":
|
|
case "boolean":
|
|
return type;
|
|
case "undefined":
|
|
return "null";
|
|
default:
|
|
return "string";
|
|
}
|
|
}
|
|
|
|
private setMode(newMode: Mode): void {
|
|
this.props.onModeChanged(newMode);
|
|
}
|
|
|
|
private saveProperties(): void {
|
|
this.props.updateVertexProperties(this.state.editedProperties).then(() => this.setMode(Mode.READONLY_PROP));
|
|
}
|
|
|
|
private showPropertyEditor(): void {
|
|
const partitionKeyProperty = this.props.collectionPartitionKeyProperty;
|
|
// deep copy highlighted node
|
|
const readOnlyProps: ViewModels.InputProperty[] = [
|
|
{
|
|
key: "label",
|
|
values: [{ value: this.props.node.label, type: "string" }],
|
|
},
|
|
];
|
|
|
|
const existingProps: ViewModels.InputProperty[] = [];
|
|
if (this.props.node.hasOwnProperty("properties")) {
|
|
const hProps = this.props.node["properties"];
|
|
for (const p in hProps) {
|
|
const propValues = hProps[p];
|
|
(p === partitionKeyProperty ? readOnlyProps : existingProps).push({
|
|
key: p,
|
|
values: propValues.map((val) => ({
|
|
value: val.toString(),
|
|
type: NodePropertiesComponent.getTypeOption(val),
|
|
})),
|
|
});
|
|
}
|
|
}
|
|
|
|
const newMode = Mode.PROPERTY_EDITOR;
|
|
this.setState({
|
|
editedProperties: {
|
|
pkId: this.props.getPkIdFromNodeData(this.props.node),
|
|
readOnlyProperties: readOnlyProps,
|
|
existingProperties: existingProps,
|
|
addedProperties: [],
|
|
droppedKeys: [],
|
|
},
|
|
});
|
|
this.props.onModeChanged(newMode);
|
|
}
|
|
|
|
private showSourcesEditor(): void {
|
|
this.props.updatePossibleVertices().then((possibleVertices: PossibleVertex[]) => {
|
|
this.setState({
|
|
possibleVertices: possibleVertices,
|
|
});
|
|
|
|
const editedSources: EditedEdges = {
|
|
vertexId: this.props.node.id,
|
|
currentNeighbors: this.props.node.sources.slice(),
|
|
droppedIds: [],
|
|
addedEdges: [],
|
|
};
|
|
|
|
const newMode = Mode.EDIT_SOURCES;
|
|
this.setState({
|
|
editedProperties: this.state.editedProperties,
|
|
editedSources: editedSources,
|
|
});
|
|
this.props.onModeChanged(newMode);
|
|
});
|
|
}
|
|
|
|
private showTargetsEditor(): void {
|
|
this.props.updatePossibleVertices().then((possibleVertices: PossibleVertex[]) => {
|
|
this.setState({
|
|
possibleVertices: possibleVertices,
|
|
});
|
|
|
|
const editedTargets: EditedEdges = {
|
|
vertexId: this.props.node.id,
|
|
currentNeighbors: this.props.node.targets.slice(),
|
|
droppedIds: [],
|
|
addedEdges: [],
|
|
};
|
|
|
|
const newMode = Mode.EDIT_TARGETS;
|
|
this.setState({
|
|
editedProperties: this.state.editedProperties,
|
|
editedTargets: editedTargets,
|
|
});
|
|
this.props.onModeChanged(newMode);
|
|
});
|
|
}
|
|
|
|
private updateVertexNeighbors(isSource: boolean): void {
|
|
const editedEdges = isSource ? this.state.editedSources : this.state.editedTargets;
|
|
this.props.editGraphEdges(editedEdges).then(
|
|
() => {
|
|
this.setMode(Mode.READONLY_PROP);
|
|
},
|
|
() => {
|
|
GraphExplorer.reportToConsole(ConsoleDataType.Error, "Failed to update Vertex sources.");
|
|
},
|
|
);
|
|
}
|
|
|
|
private onUpdateProperties(editedProperties: EditedProperties): void {
|
|
this.setState({
|
|
editedProperties: editedProperties,
|
|
});
|
|
}
|
|
|
|
private onUpdateEdges(editedEdges: EditedEdges, isSource: boolean): void {
|
|
if (isSource) {
|
|
this.setState({ editedSources: editedEdges });
|
|
} else {
|
|
this.setState({ editedTargets: editedEdges });
|
|
}
|
|
}
|
|
|
|
private setIsDeleteConfirm(state: boolean): void {
|
|
this.setState({
|
|
isDeleteConfirm: state,
|
|
});
|
|
}
|
|
|
|
private discardChanges(): void {
|
|
this.props.onModeChanged(Mode.READONLY_PROP);
|
|
}
|
|
|
|
/**
|
|
* Right-pane expand collapse
|
|
*/
|
|
private expandCollapseProperties(): void {
|
|
// Do not collapse while editing
|
|
if (this.props.viewMode === Mode.PROPERTY_EDITOR && this.state.isPropertiesExpanded) {
|
|
return;
|
|
}
|
|
|
|
const isExpanded = this.state.isPropertiesExpanded;
|
|
this.setState({ isPropertiesExpanded: !isExpanded });
|
|
if (!isExpanded) {
|
|
$("#propertiesContent").slideDown("fast", () => {
|
|
/* Animation complete */
|
|
});
|
|
} else {
|
|
$("#propertiesContent").slideUp("fast", () => {
|
|
/* Animation complete */
|
|
});
|
|
}
|
|
}
|
|
|
|
private expandCollapseSources(): void {
|
|
// Do not collapse while editing
|
|
if (this.props.viewMode === Mode.EDIT_SOURCES && this.state.isSourcesExpanded) {
|
|
return;
|
|
}
|
|
|
|
const isExpanded = this.state.isSourcesExpanded;
|
|
this.setState({ isSourcesExpanded: !isExpanded });
|
|
if (!isExpanded) {
|
|
$("#sourcesContent").slideDown("fast", () => {
|
|
/* Animation complete */
|
|
});
|
|
} else {
|
|
$("#sourcesContent").slideUp("fast", () => {
|
|
/* Animation complete */
|
|
});
|
|
}
|
|
}
|
|
private expandCollapseTargets(): void {
|
|
// Do not collapse while editing
|
|
if (this.props.viewMode === Mode.EDIT_TARGETS && this.state.isTargetsExpanded) {
|
|
return;
|
|
}
|
|
|
|
const isExpanded = this.state.isTargetsExpanded;
|
|
this.setState({ isTargetsExpanded: !isExpanded });
|
|
if (!isExpanded) {
|
|
$("#targetsContent").slideDown("fast", () => {
|
|
/* Animation complete */
|
|
});
|
|
} else {
|
|
$("#targetsContent").slideUp("fast", () => {
|
|
/* Animation complete */
|
|
});
|
|
}
|
|
}
|
|
|
|
private deleteHighlightedNode(): void {
|
|
this.setIsDeleteConfirm(false);
|
|
this.props.deleteHighlightedNode();
|
|
}
|
|
|
|
private getConfirmDeleteButtonsFragment(): JSX.Element {
|
|
if (!this.state.isDeleteConfirm) {
|
|
return (
|
|
<AccessibleElement
|
|
className="rightPaneHeaderTrashIcon rightPaneBtns"
|
|
as="span"
|
|
onActivated={this.setIsDeleteConfirm.bind(this, true)}
|
|
aria-label="Delete this vertex"
|
|
>
|
|
<img src={DeleteIcon} alt="Delete" role="button" />
|
|
</AccessibleElement>
|
|
);
|
|
} else {
|
|
return (
|
|
<span className="deleteConfirm">
|
|
Delete this vertex?
|
|
<AccessibleElement
|
|
className="rightPaneCheckMark rightPaneBtns"
|
|
as="span"
|
|
aria-label="Confirm delete this vertex"
|
|
onActivated={this.deleteHighlightedNode.bind(this)}
|
|
>
|
|
<img src={CheckIcon} alt="Save" />
|
|
</AccessibleElement>
|
|
<AccessibleElement
|
|
className="rightPaneDiscardBtn rightPaneBtns"
|
|
as="span"
|
|
aria-label="Cancel delete this vertex"
|
|
onActivated={this.setIsDeleteConfirm.bind(this, false)}
|
|
>
|
|
<img className="discardBtn" src={CancelIcon} alt="Cancel" />
|
|
</AccessibleElement>
|
|
</span>
|
|
);
|
|
}
|
|
}
|
|
|
|
private getHeaderFragment(): JSX.Element {
|
|
return (
|
|
<div className="rightPaneHeader">
|
|
{this.props.viewMode === Mode.READONLY_PROP && (
|
|
<span className="pull-right">{this.getConfirmDeleteButtonsFragment()}</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Section Header containing the edit/cancel buttons
|
|
*/
|
|
private getSectionHeaderButtonFragment(
|
|
isSectionExpanded: boolean,
|
|
expandClickHandler: () => void,
|
|
currentView: Mode,
|
|
saveClickHandler: () => void,
|
|
): JSX.Element {
|
|
if (isSectionExpanded) {
|
|
return (
|
|
<div className="pull-right">
|
|
{this.props.viewMode === Mode.READONLY_PROP && !this.state.isDeleteConfirm && (
|
|
<AccessibleElement
|
|
className="rightPaneEditIcon rightPaneBtns editBtn"
|
|
as="span"
|
|
aria-label="Edit properties"
|
|
onActivated={expandClickHandler}
|
|
>
|
|
<img src={EditIcon} alt="Edit" role="button" />
|
|
</AccessibleElement>
|
|
)}
|
|
|
|
{this.props.viewMode === currentView && (
|
|
<AccessibleElement
|
|
className="rightPaneCheckMark rightPaneBtns"
|
|
as="span"
|
|
aria-label="Save property changes"
|
|
onActivated={saveClickHandler}
|
|
>
|
|
<img src={CheckIcon} alt="Save" />
|
|
</AccessibleElement>
|
|
)}
|
|
{this.props.viewMode === currentView && (
|
|
<AccessibleElement
|
|
className="rightPaneDiscardBtn rightPaneBtns"
|
|
as="span"
|
|
aria-label="Discard property changes"
|
|
onActivated={this.discardChanges.bind(this)}
|
|
>
|
|
<img className="discardBtn" src={CancelIcon} alt="Cancel" />
|
|
</AccessibleElement>
|
|
)}
|
|
</div>
|
|
);
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
private getPropertiesFragment(): JSX.Element {
|
|
return (
|
|
<React.Fragment>
|
|
{this.getSectionHeaderButtonFragment(
|
|
this.state.isPropertiesExpanded,
|
|
this.showPropertyEditor.bind(this),
|
|
Mode.PROPERTY_EDITOR,
|
|
this.saveProperties.bind(this),
|
|
)}
|
|
<AccessibleElement
|
|
className="sectionHeader"
|
|
as="div"
|
|
aria-label={this.state.isPropertiesExpanded ? "Collapse properties" : "Expand properties"}
|
|
onActivated={this.expandCollapseProperties.bind(this)}
|
|
>
|
|
<span className={this.state.isPropertiesExpanded ? "expanded" : "collapsed"} />
|
|
<span className="sectionTitle">Properties</span>
|
|
</AccessibleElement>
|
|
<div className="sectionContent" id="propertiesContent">
|
|
{(this.props.viewMode === Mode.READONLY_PROP ||
|
|
this.props.viewMode === Mode.EDIT_SOURCES ||
|
|
this.props.viewMode === Mode.EDIT_TARGETS) && <ReadOnlyNodePropertiesComponent node={this.props.node} />}
|
|
|
|
{this.props.viewMode === Mode.PROPERTY_EDITOR && (
|
|
<EditorNodePropertiesComponent
|
|
editedProperties={this.state.editedProperties}
|
|
onUpdateProperties={this.onUpdateProperties.bind(this)}
|
|
/>
|
|
)}
|
|
</div>
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
private getNeighborContentFragment(isSource: boolean): JSX.Element {
|
|
const editViewMode = isSource ? Mode.EDIT_SOURCES : Mode.EDIT_TARGETS;
|
|
const editedNeighbors = isSource ? this.state.editedSources : this.state.editedTargets;
|
|
|
|
if (this.props.viewMode === editViewMode) {
|
|
return (
|
|
<EditorNeighbors.EditorNeighborsComponent
|
|
editedNeighbors={editedNeighbors}
|
|
isSource={isSource}
|
|
possibleVertices={this.state.possibleVertices}
|
|
possibleEdgeLabels={this.props.possibleEdgeLabels}
|
|
onUpdateEdges={this.onUpdateEdges.bind(this)}
|
|
/>
|
|
);
|
|
} else {
|
|
return (
|
|
<ReadOnlyNeighborsComponent node={this.props.node} isSource={isSource} selectNode={this.props.selectNode} />
|
|
);
|
|
}
|
|
}
|
|
|
|
private getNeighborFragment(isSource: boolean): JSX.Element {
|
|
const isNeighborExpanded = isSource ? this.state.isSourcesExpanded : this.state.isTargetsExpanded;
|
|
const showNeighborEditor = isSource ? this.showSourcesEditor.bind(this) : this.showTargetsEditor.bind(this);
|
|
const currentNeighborView = isSource ? Mode.EDIT_SOURCES : Mode.EDIT_TARGETS;
|
|
const expandCollapseNeighbor = isSource
|
|
? this.expandCollapseSources.bind(this)
|
|
: this.expandCollapseTargets.bind(this);
|
|
const sectionLabel = isSource ? "Sources" : "Targets";
|
|
const sectionContentId = isSource ? "sourcesContent" : "targetsContent";
|
|
|
|
return (
|
|
<React.Fragment>
|
|
{this.getSectionHeaderButtonFragment(
|
|
isNeighborExpanded,
|
|
showNeighborEditor,
|
|
currentNeighborView,
|
|
this.updateVertexNeighbors.bind(this, isSource),
|
|
)}
|
|
|
|
<AccessibleElement
|
|
className="sectionHeader"
|
|
as="div"
|
|
aria-label={`${this.state.isPropertiesExpanded ? "Collapse" : "Expand"} ${sectionLabel}`}
|
|
onActivated={expandCollapseNeighbor}
|
|
>
|
|
<span className={isNeighborExpanded ? "expanded" : "collapsed"} />
|
|
<span className="sectionTitle">{sectionLabel}</span>
|
|
</AccessibleElement>
|
|
|
|
<div className="sectionContent" id={sectionContentId}>
|
|
{this.getNeighborContentFragment(isSource)}
|
|
</div>
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
|
|
private getNeighborsContainer(): JSX.Element {
|
|
if (!this.props.node.areNeighborsUnknown) {
|
|
return (
|
|
<React.Fragment>
|
|
{this.getNeighborFragment(true)}
|
|
{this.getNeighborFragment(false)}
|
|
</React.Fragment>
|
|
);
|
|
} else {
|
|
return <React.Fragment />;
|
|
}
|
|
}
|
|
}
|