aboutsummaryrefslogtreecommitdiffstats
path: root/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components
diff options
context:
space:
mode:
authorMichael Lando <ml636r@att.com>2017-02-19 12:57:33 +0200
committerMichael Lando <ml636r@att.com>2017-02-19 13:47:13 +0200
commitefa037d34be7b1570efdc767c79fad8d4005f10e (patch)
treecf1036ba2728dea8a61492b678fa91954e629403 /dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components
parentf5f13c4f6b6fe3b4d98e349dfd7db59339803436 (diff)
Add new code new version
Change-Id: Ic02a76313503b526f17c3df29eb387a29fe6a42a Signed-off-by: Michael Lando <ml636r@att.com>
Diffstat (limited to 'dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components')
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/application/Application.jsx268
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/Diagram.jsx896
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/components/popup/Popup.jsx94
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/diagram.html56
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/fragment.html18
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/lifeline.html19
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/message.html29
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/occurrence.html7
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/title.html3
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/dialog/Dialog.jsx222
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/Editor.jsx171
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/Designer.jsx403
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/actions/Actions.jsx471
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/Lifeline.jsx264
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/LifelineNew.jsx112
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/Lifelines.jsx136
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/Message.jsx587
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/MessageNew.jsx106
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/Messages.jsx143
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/metadata/Metadata.jsx34
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/source/Source.jsx86
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/toolbar/Toolbar.jsx275
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/export/Export.jsx31
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/icons/Icon.jsx41
-rw-r--r--dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/overlay/Overlay.jsx61
25 files changed, 4533 insertions, 0 deletions
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/application/Application.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/application/Application.jsx
new file mode 100644
index 0000000000..20b06922c8
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/application/Application.jsx
@@ -0,0 +1,268 @@
+
+import React from 'react';
+
+import Common from '../../common/Common';
+import Logger from '../../common/Logger';
+import Diagram from '../diagram/Diagram';
+import Dialog from '../dialog/Dialog';
+import Editor from '../editor/Editor';
+import Export from '../export/Export';
+import Overlay from '../overlay/Overlay';
+
+/**
+ * Application controller, also a view.
+ */
+export default class Application extends React.Component {
+
+ /**
+ * Construct application view.
+ * @param props element properties.
+ * @param context react context.
+ */
+ constructor(props, context) {
+ super(props, context);
+
+ this.sequencer = Common.assertNotNull(props.sequencer);
+ this.model = this.sequencer.getModel();
+ this.metamodel = this.sequencer.getMetamodel();
+ this.options = props.options;
+ Logger.setLevel(this.options.unwrap().log.level);
+
+ // Bindings.
+
+ this.showInfoDialog = this.showInfoDialog.bind(this);
+ this.showEditDialog = this.showEditDialog.bind(this);
+ this.showConfirmDialog = this.showConfirmDialog.bind(this);
+ this.hideOverlay = this.hideOverlay.bind(this);
+ this.onMouseMove = this.onMouseMove.bind(this);
+ this.onMouseUp = this.onMouseUp.bind(this);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Get application options.
+ * @returns JSON options, see Options.js.
+ */
+ getOptions() {
+ return this.options.unwrap();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Set diagram name.
+ * @param n diagram (human-readable) name.
+ */
+ setName(n) {
+ this.diagram.setName(n);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Set diagram model.
+ * @param model diagram instance.
+ */
+ setModel(model) {
+
+ Common.assertNotNull(model);
+
+ this.model = model;
+
+ if (this.editor) {
+ this.editor.render();
+ }
+
+ if (this.diagram) {
+ this.diagram.render();
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Get Model wrapper.
+ * @returns Model.
+ */
+ getModel() {
+ return this.model;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Get SVG element.
+ * @returns {*}
+ */
+ getSVG() {
+ return this.diagram.getSVG();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Get top-level widget. Provides the demo toolbar with access to the public API.
+ * @returns {*}
+ */
+ getSequencer() {
+ return this.sequencer;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Present info dialog.
+ * @param msg info message.
+ */
+ showInfoDialog(msg) {
+ this.dialog.showInfoDialog(msg);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Present error dialog.
+ * @param msg error message.
+ */
+ showErrorDialog(msg) {
+ this.dialog.showErrorDialog(msg);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Present confirmation dialog.
+ * @param msg info message.
+ * @param cb callback function to be invoked on OK.
+ */
+ showConfirmDialog(msg, cb) {
+ Common.assertType(cb, 'Function');
+ this.dialog.showConfirmDialog(msg, cb);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Present edit (textarea) dialog.
+ * @param msg prompt.
+ * @param text current edit text.
+ * @param cb callback function to be invoked on OK, taking the updated text
+ * as an argument.
+ */
+ showEditDialog(msg, text, cb) {
+ this.dialog.showEditDialog(msg, text, cb);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Select lifeline by ID.
+ * @param id lifeline ID.
+ */
+ selectLifeline(id) {
+ if (this.editor) {
+ this.editor.selectLifeline(id);
+ }
+ if (this.diagram) {
+ this.diagram.selectLifeline(id);
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Select message by ID.
+ * @param id message ID.
+ */
+ selectMessage(id) {
+ if (this.editor) {
+ this.editor.selectMessage(id);
+ }
+ if (this.diagram) {
+ this.diagram.selectMessage(id);
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * (Re)render just the diagram.
+ */
+ renderDiagram() {
+ this.diagram.redraw();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Show overlay between application and modal dialog.
+ */
+ showOverlay() {
+ if (this.overlay) {
+ this.overlay.setVisible(true);
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Hide overlay between application and modal dialog.
+ */
+ hideOverlay() {
+ if (this.overlay) {
+ this.overlay.setVisible(false);
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Capture mouse move events, for resize.
+ * @param event move event.
+ */
+ onMouseMove(event) {
+ if (this.editor) {
+ this.editor.onMouseMove(event);
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Propagate mouse event to the editor that manages the resize.
+ */
+ onMouseUp() {
+ if (this.editor) {
+ this.editor.onMouseUp();
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Render current model state.
+ */
+ render() {
+
+ return (
+
+ <div className="asdcs-control" onMouseMove={this.onMouseMove} onMouseUp={this.onMouseUp}>
+
+ <Editor application={this} ref={(r) => { this.editor = r; }} />
+ <Diagram application={this} ref={(r) => { this.diagram = r; }} />
+ <Dialog application={this} ref={(r) => { this.dialog = r; }} />
+ <Export />
+ <Overlay application={this} ref={(r) => { this.overlay = r; }} />
+
+ </div>
+ );
+ }
+
+}
+
+/** React properties. */
+Application.propTypes = {
+ options: React.PropTypes.object.isRequired,
+ sequencer: React.PropTypes.object.isRequired,
+};
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/Diagram.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/Diagram.jsx
new file mode 100644
index 0000000000..f2da7a5a1b
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/Diagram.jsx
@@ -0,0 +1,896 @@
+
+import React from 'react';
+import _template from 'lodash/template';
+import _merge from 'lodash/merge';
+import d3 from 'd3';
+
+import Common from '../../common/Common';
+import Logger from '../../common/Logger';
+import Popup from './components/popup/Popup';
+
+/**
+ * SVG diagram view.
+ */
+export default class Diagram extends React.Component {
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Construct React view.
+ * @param props properties.
+ * @param context context.
+ */
+ constructor(props, context) {
+ super(props, context);
+
+ this.application = Common.assertNotNull(props.application);
+ this.options = this.application.getOptions().diagram;
+
+ this.events = {};
+ this.state = {
+ height: 0,
+ width: 0,
+ };
+
+ this.templates = {
+ diagram: _template(require('./templates/diagram.html')),
+ lifeline: _template(require('./templates/lifeline.html')),
+ message: _template(require('./templates/message.html')),
+ occurrence: _template(require('./templates/occurrence.html')),
+ fragment: _template(require('./templates/fragment.html')),
+ title: _template(require('./templates/title.html')),
+ };
+
+ this.handleResize = this.handleResize.bind(this);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Set diagram name.
+ * @param n name.
+ */
+ setName(n) {
+ this.svg.select('').text(n);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Get SVG from diagram.
+ * @returns {*|string}
+ */
+ getSVG() {
+ const svg = this.svg.node().outerHTML;
+ return svg.replace('<svg ', '<svg xmlns="http://www.w3.org/2000/svg" ');
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Select message by ID.
+ * @param id message ID.
+ */
+ selectMessage(id) {
+ const sel = this.svg.selectAll('g.asdcs-diagram-message-container');
+ sel.classed('asdcs-active', false);
+ sel.selectAll('rect.asdcs-diagram-message-bg').attr('filter', null);
+ if (id) {
+ const parent = this.svg.select(`g.asdcs-diagram-message-container[data-id="${id}"]`);
+ parent.classed('asdcs-active', true);
+ parent.selectAll('rect.asdcs-diagram-message-bg').attr('filter', 'url(#asdcsSvgHighlight)');
+ }
+ this._showNotesPopup(id);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Select lifeline by ID.
+ * @param id lifeline ID.
+ */
+ selectLifeline(id) {
+ const sel = this.svg.selectAll('g.asdcs-diagram-lifeline-container');
+ sel.classed('asdcs-active', false);
+ sel.selectAll('rect').attr('filter', null);
+ if (id) {
+ const parent = this.svg.select(`g.asdcs-diagram-lifeline-container[data-id="${id}"]`);
+ parent.selectAll('rect').attr('filter', 'url(#asdcsSvgHighlight)');
+ parent.classed('asdcs-active', true);
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle resize, including initial sizing.
+ */
+ handleResize() {
+ if (this.wrapper) {
+ const height = this.wrapper.offsetHeight;
+ const width = this.wrapper.offsetWidth;
+ if (this.state.height !== height || this.state.width !== width) {
+ this.setState({ height, width });
+ }
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * (Re)render diagram.
+ */
+ render() {
+
+ const model = this.application.getModel();
+ const modelJSON = model.unwrap();
+ const name = modelJSON.diagram.metadata.name;
+ const options = this.application.getOptions();
+ const titleHeight = options.diagram.title.height;
+ const titleClass = (titleHeight && titleHeight > 0) ? `height:${titleHeight}` : 'asdcs-hidden';
+
+ return (
+ <div className="asdcs-diagram">
+ <div className={`asdcs-diagram-name ${titleClass}`}>{name}</div>
+ <div className="asdcs-diagram-svg" ref={(r) => { this.wrapper = r; }}></div>
+ <Popup visible={false} ref={(r) => { this.popup = r; }} />
+ </div>
+ );
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ redraw() {
+ this.updateSVG();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Initial render.
+ */
+ componentDidMount() {
+ window.addEventListener('resize', this.handleResize);
+ this.updateSVG();
+
+ // Insurance:
+
+ setTimeout(() => {
+ this.handleResize();
+ }, 500);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.handleResize);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Render on update.
+ */
+ componentDidUpdate() {
+ this.updateSVG();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Redraw SVG diagram. So far it's fast enough that it doesn't seem to matter whether
+ * it's completely redrawn.
+ */
+ updateSVG() {
+
+ if (!this.svg) {
+ const svgparams = _merge({}, this.options.svg);
+ this.wrapper.innerHTML = this.templates.diagram(svgparams);
+ this.svg = d3.select(this.wrapper).select('svg');
+ }
+
+ if (this.state.height === 0) {
+
+ // We'll get a resize event, and the height will be non-zero when it's actually time.
+
+ return;
+ }
+
+ if (this.state.height && this.state.width) {
+ const margin = this.options.svg.margin;
+ const x = -margin;
+ const y = -margin;
+ const height = this.state.height + (margin * 2);
+ const width = this.state.width + (margin * 2);
+ const viewBox = `${x} ${y} ${width} ${height}`;
+ this.svg.attr('viewBox', viewBox);
+ }
+
+
+ // If we've already rendered, then save the current scale/translate so that we
+ // can reapply it after rendering.
+
+ const gContentSelection = this.svg.selectAll('g.asdcs-diagram-content');
+ if (gContentSelection.size() === 1) {
+ const transform = gContentSelection.attr('transform');
+ if (transform) {
+ this.savedTransform = transform;
+ }
+ }
+
+ // Empty the document. We're starting again.
+
+ this.svg.selectAll('.asdcs-diagram-content').remove();
+
+ // Extract the model.
+
+ const model = this.application.getModel();
+ if (!model) {
+ return;
+ }
+ const modelJSON = model.unwrap();
+
+ // Extract dimension options.
+
+ const header = this.options.lifelines.header;
+ const spacing = this.options.lifelines.spacing;
+
+ // Make separate container elements so that we can control Z order.
+
+ const gContent = this.svg.append('g').attr('class', 'asdcs-diagram-content');
+ const gLifelines = gContent.append('g').attr('class', 'asdcs-diagram-lifelines');
+ const gCanvas = gContent.append('g').attr('class', 'asdcs-diagram-canvas');
+ gCanvas.append('g').attr('class', 'asdcs-diagram-occurrences');
+ gCanvas.append('g').attr('class', 'asdcs-diagram-fragments');
+ gCanvas.append('g').attr('class', 'asdcs-diagram-messages');
+
+ // Lifelines -----------------------------------------------------------------------------------
+
+ const actorsById = {};
+ const positionsByMessageId = {};
+ const lifelines = [];
+ for (const actor of modelJSON.diagram.lifelines) {
+ const x = (header.width / 2) + (lifelines.length * spacing.horizontal);
+ Diagram._processLifeline(actor, x);
+ lifelines.push({ x, actor });
+ actorsById[actor.id] = actor;
+ }
+
+ // Messages ------------------------------------------------------------------------------------
+
+ // Analyze occurrence information.
+
+ const occurrences = model.analyzeOccurrences();
+ const fragments = model.analyzeFragments();
+ let y = this.options.lifelines.header.height + spacing.vertical;
+ let messageIndex = 0;
+ for (const step of modelJSON.diagram.steps) {
+ if (step.message) {
+ positionsByMessageId[step.message.id] = positionsByMessageId[step.message.id] || {};
+ positionsByMessageId[step.message.id].y = y;
+ this._drawMessage(gCanvas, step.message, y, actorsById,
+ positionsByMessageId, ++messageIndex, occurrences, fragments);
+ }
+ y += spacing.vertical;
+ }
+
+ // ---------------------------------------------------------------------------------------------
+
+ // Draw the actual (dashed) lifelines in a background <g>.
+
+ this._drawLifelines(gLifelines, lifelines, y);
+
+ // Initialize mouse event handlers.
+
+ this._initMouseEvents(gLifelines, gCanvas);
+
+ // Scale to fit.
+
+ const bb = gContent.node().getBBox();
+ this._initZoom(gContent, bb.width, bb.height);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Draw message into SVG canvas.
+ * @param gCanvas container.
+ * @param message message to be rendered.
+ * @param y current y position.
+ * @param actorsById actor lookup.
+ * @param positionsByMessageId x- and y-position of each message.
+ * @param messageIndex where we are in the set of messages to be rendered.
+ * @param oData occurrences info.
+ * @param fData fragments info.
+ * @private
+ */
+ _drawMessage(gCanvas, message, y, actorsById, positionsByMessageId,
+ messageIndex, oData, fData) {
+
+ Common.assertNotNull(oData);
+
+ const request = message.type === 'request';
+ const fromActor = request ? actorsById[message.from] : actorsById[message.to];
+ const toActor = request ? actorsById[message.to] : actorsById[message.from];
+
+ if (!fromActor) {
+ Logger.warn(`Cannot draw message ${JSON.stringify(message)}: 'from' not found.`);
+ return;
+ }
+
+ if (!toActor) {
+ Logger.warn(`Cannot draw message ${JSON.stringify(message)}: 'to' not found.`);
+ return;
+ }
+
+ // Occurrences. --------------------------------------------------------------------------------
+
+ if (message.occurrence) {
+ Logger.debug(`Found occurrence for ${message.name}: ${JSON.stringify(message.occurrence)}`);
+ }
+ const activeTo = Diagram._calcActive(oData, toActor.id);
+ this._drawOccurrence(gCanvas, oData, positionsByMessageId, fromActor, message.id);
+ this._drawOccurrence(gCanvas, oData, positionsByMessageId, toActor, message.id);
+ const activeFrom = Diagram._calcActive(oData, fromActor.id);
+
+ // Messages. -----------------------------------------------------------------------------------
+
+ const gMessages = gCanvas.select('g.asdcs-diagram-messages');
+
+ // Save positions for later.
+
+ const positions = positionsByMessageId[message.id];
+ positions.x0 = fromActor.x;
+ positions.x1 = toActor.x;
+
+ // Calculate.
+
+ const leftToRight = fromActor.x < toActor.x;
+ const loopback = (message.to === message.from);
+ const x1 = this._calcMessageX(activeTo, toActor.x, true, leftToRight);
+ const x0 = loopback ? x1 : this._calcMessageX(activeFrom, fromActor.x, false, leftToRight);
+
+ let messagePath;
+ if (loopback) {
+
+ // To self.
+
+ messagePath = `M${x1},${y}`;
+ messagePath = `${messagePath} L${x1 + 200},${y}`;
+ messagePath = `${messagePath} L${x1 + 200},${y + 50}`;
+ messagePath = `${messagePath} L${x1},${y + 50}`;
+ } else {
+
+ // Between lifelines.
+
+ messagePath = `M${x0},${y}`;
+ messagePath = `${messagePath} L${x1},${y}`;
+ }
+
+ const styles = Diagram._getMessageStyles(message);
+
+ // Split message over lines.
+
+ const messageWithPrefix = `${messageIndex}. ${message.name}`;
+ const maxLines = this.options.messages.label.maxLines;
+ const wrapWords = this.options.messages.label.wrapWords;
+ const wrapLines = this.options.messages.label.wrapLines;
+ const messageLines = Common.tokenize(messageWithPrefix, wrapWords, wrapLines, maxLines);
+
+ const messageTxt = this.templates.message({
+ id: message.id,
+ classes: styles.css,
+ marker: styles.marker,
+ dasharray: styles.dasharray,
+ labels: messageLines,
+ lines: maxLines,
+ path: messagePath,
+ index: messageIndex,
+ x0, x1, y,
+ });
+
+ const messageEl = Common.txt2dom(messageTxt);
+ const gMessage = gMessages.append('g');
+ Common.dom2svg(messageEl, gMessage);
+
+ // Set the background's bounding box to that of the text,
+ // so that they fit snugly.
+
+ const labelBB = gMessage.select('.asdcs-diagram-message-label').node().getBBox();
+ gMessage.select('.asdcs-diagram-message-label-bg')
+ .attr('x', labelBB.x)
+ .attr('y', labelBB.y)
+ .attr('height', labelBB.height)
+ .attr('width', labelBB.width);
+
+ // Fragments. ----------------------------------------------------------------------------------
+
+ const fragment = fData[message.id];
+ if (fragment) {
+
+ // It ends on this message.
+
+ this._drawFragment(gCanvas, fragment, positionsByMessageId);
+
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Draw a single occurrence.
+ * @param gCanvas container.
+ * @param oData occurrence data.
+ * @param positionsByMessageId map of y positions by message ID.
+ * @param actor wrapper containing lifeline ID (.id), position (.x) and name (.name).
+ * @param messageId message identifier.
+ * @private
+ */
+ _drawOccurrence(gCanvas, oData, positionsByMessageId, actor, messageId) {
+
+ Common.assertType(oData, 'Object');
+ Common.assertType(positionsByMessageId, 'Object');
+ Common.assertType(actor, 'Object');
+ Common.assertType(messageId, 'String');
+
+ const gOccurrences = gCanvas.select('g.asdcs-diagram-occurrences');
+
+ const oOptions = this.options.lifelines.occurrences;
+ const oWidth = oOptions.width;
+ const oHalfWidth = oWidth / 2;
+ const oForeshortening = oOptions.foreshortening;
+ const oMarginTop = oOptions.marginTop;
+ const oMarginBottom = oOptions.marginBottom;
+ const o = oData[actor.id];
+
+ const active = Diagram._calcActive(oData, actor.id);
+
+ const x = (actor.x - oHalfWidth) + (active * oWidth);
+ const positions = positionsByMessageId[messageId];
+ const y = positions.y;
+
+ let draw = true;
+ if (o) {
+
+ if (o.start[messageId]) {
+
+ // Starting, but drawing nothing until we find the end.
+
+ o.active.push(messageId);
+ draw = false;
+
+ } else if (active > 0) {
+
+ const startMessageId = o.stop[messageId];
+ if (startMessageId) {
+
+ // OK, it ends here. Draw the occurrence box.
+
+ o.active.pop();
+ const foreshorteningY = active * oForeshortening;
+ const startY = positionsByMessageId[startMessageId].y;
+ const height = ((oMarginTop + oMarginBottom) + (y - startY)) - (foreshorteningY * 2);
+ const oProps = {
+ x: (actor.x - oHalfWidth) + ((active - 1) * oWidth),
+ y: ((startY - oMarginTop) + foreshorteningY),
+ height,
+ width: oWidth,
+ };
+
+ const occurrenceTxt = this.templates.occurrence(oProps);
+ const occurrenceEl = Common.txt2dom(occurrenceTxt);
+ Common.dom2svg(occurrenceEl, gOccurrences.append('g'));
+
+ }
+ draw = false;
+ }
+ }
+
+ if (draw) {
+
+ // Seems this is a singleton occurrence. We just draw a wee box around it.
+
+ const height = (oMarginTop + oMarginBottom);
+ const occurrenceProperties = { x, y: y - oMarginTop, height, width: oWidth };
+ const defaultTxt = this.templates.occurrence(occurrenceProperties);
+ const defaultEl = Common.txt2dom(defaultTxt);
+ Common.dom2svg(defaultEl, gOccurrences.append('g'));
+ }
+
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Draw box(es) around fragment(s).
+ * @param gCanvas container.
+ * @param fragment fragment definition, corresponding to its final (stop) message.
+ * @param positionsByMessageId message dimensions.
+ * @private
+ */
+ _drawFragment(gCanvas, fragment, positionsByMessageId) {
+
+ const optFragments = this.options.fragments;
+ const gFragments = gCanvas.select('g.asdcs-diagram-fragments');
+ const p1 = positionsByMessageId[fragment.stop];
+ if (p1 && fragment.start && fragment.start.length > 0) {
+
+ for (const start of fragment.start) {
+
+ const message = this.application.getModel().getMessageById(start);
+ const bounds = this._calcFragmentBounds(message, fragment, positionsByMessageId);
+ if (bounds) {
+
+ const maxLines = this.options.fragments.label.maxLines;
+ const wrapWords = this.options.fragments.label.wrapWords;
+ const wrapLines = this.options.fragments.label.wrapLines;
+ const lines = Common.tokenize(message.fragment.guard, wrapWords, wrapLines, maxLines);
+
+ const params = {
+ id: start,
+ x: bounds.x0 - optFragments.leftMargin,
+ y: bounds.y0 - optFragments.topMargin,
+ height: (bounds.y1 - bounds.y0) + optFragments.heightMargin,
+ width: (bounds.x1 - bounds.x0) + optFragments.widthMargin,
+ operator: (message.fragment.operator || 'alt'),
+ lines,
+ };
+
+ const fragmentTxt = this.templates.fragment(params);
+ const fragmentEl = Common.txt2dom(fragmentTxt);
+ const gFragment = gFragments.append('g');
+ Common.dom2svg(fragmentEl, gFragment);
+
+ const labelBB = gFragment.select('.asdcs-diagram-fragment-guard').node().getBBox();
+ gFragment.select('.asdcs-diagram-fragment-guard-bg')
+ .attr('x', labelBB.x)
+ .attr('y', labelBB.y)
+ .attr('height', labelBB.height)
+ .attr('width', labelBB.width);
+
+ } else {
+ Logger.warn(`Bad fragment: ${JSON.stringify(fragment)}`);
+ }
+ }
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ _calcFragmentBounds(startMessage, fragment, positionsByMessageId) {
+ if (startMessage) {
+ const steps = this.application.getModel().unwrap().diagram.steps;
+ const bounds = { x0: 99999, x1: 0, y0: 99999, y1: 0 };
+ let foundStart = false;
+ let foundStop = false;
+ for (const step of steps) {
+ const message = step.message;
+ if (message) {
+ if (message.id === startMessage.id) {
+ foundStart = true;
+ }
+ if (foundStart && !foundStop) {
+ const positions = positionsByMessageId[message.id];
+ if (positions) {
+ bounds.x0 = Math.min(bounds.x0, Math.min(positions.x0, positions.x1));
+ bounds.y0 = Math.min(bounds.y0, positions.y);
+ bounds.x1 = Math.max(bounds.x1, Math.max(positions.x0, positions.x1));
+ bounds.y1 = Math.max(bounds.y1, positions.y);
+ } else {
+ // This probably means it hasn't been recorded yet, which is fine, because
+ // we draw fragments from where they END.
+ foundStop = true;
+ }
+ }
+
+ if (message.id === fragment.stop) {
+ foundStop = true;
+ }
+ }
+ }
+ return bounds;
+ }
+ return undefined;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Draw all lifelines.
+ * @param gLifelines lifelines container.
+ * @param lifelines lifelines definitions.
+ * @param y height.
+ * @private
+ */
+ _drawLifelines(gLifelines, lifelines, y) {
+
+ const maxLines = this.options.lifelines.header.maxLines;
+ const wrapWords = this.options.lifelines.header.wrapWords;
+ const wrapLines = this.options.lifelines.header.wrapLines;
+
+ for (const lifeline of lifelines) {
+ const lines = Common.tokenize(lifeline.actor.name, wrapWords, wrapLines, maxLines);
+ const lifelineTxt = this.templates.lifeline({
+ x: lifeline.x,
+ y0: 0,
+ y1: y,
+ lines,
+ rows: maxLines,
+ headerHeight: this.options.lifelines.header.height,
+ headerWidth: this.options.lifelines.header.width,
+ id: lifeline.actor.id,
+ });
+
+ const lifelineEl = Common.txt2dom(lifelineTxt);
+ Common.dom2svg(lifelineEl, gLifelines.append('g'));
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Initialize all mouse events.
+ * @param gLifelines lifelines container.
+ * @param gCanvas top-level canvas container.
+ * @private
+ */
+ _initMouseEvents(gLifelines, gCanvas) {
+
+ const self = this;
+ const source = 'asdcs';
+ const origin = `${window.location.protocol}//${window.location.host}`;
+
+ let timer;
+ gLifelines.selectAll('.asdcs-diagram-lifeline-selectable')
+ .on('mouseenter', function f() {
+ timer = setTimeout(() => {
+ self.application.selectLifeline(d3.select(this.parentNode).attr('data-id'));
+ }, 150);
+ })
+ .on('mouseleave', () => {
+ clearTimeout(timer);
+ self.application.selectLifeline();
+ })
+ .on('click', function f() {
+ const id = d3.select(this.parentNode).attr('data-id');
+ window.postMessage({ source, id, type: 'lifeline' }, origin);
+ });
+
+ gLifelines.selectAll('.asdcs-diagram-lifeline-heading-box')
+ .on('mouseenter', function f() {
+ timer = setTimeout(() => {
+ self.application.selectLifeline(d3.select(this.parentNode).attr('data-id'));
+ }, 150);
+ })
+ .on('mouseleave', () => {
+ clearTimeout(timer);
+ self.application.selectLifeline();
+ })
+ .on('click', function f() {
+ const id = d3.select(this.parentNode).attr('data-id');
+ window.postMessage({ source, id, type: 'lifelineHeader' }, origin);
+ });
+
+ gCanvas.selectAll('.asdcs-diagram-message-selectable')
+ .on('mouseenter', function f() {
+ self.events.message = { x: d3.event.pageX, y: d3.event.pageY };
+ timer = setTimeout(() => {
+ self.application.selectMessage(d3.select(this.parentNode).attr('data-id'));
+ }, 200);
+ })
+ .on('mouseleave', () => {
+ delete self.events.message;
+ clearTimeout(timer);
+ self.application.selectMessage();
+ })
+ .on('click', function f() {
+ const id = d3.select(this.parentNode).attr('data-id');
+ window.postMessage({ source, id, type: 'message' }, origin);
+ });
+
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Get CSS classes to be applied to a message, according to whether request/response
+ * or synchronous/asynchronous.
+ * @param message message being rendered.
+ * @returns CSS class name(s).
+ * @private
+ */
+ static _getMessageStyles(message) {
+
+ let marker = 'asdcsDiagramArrowSolid';
+ let dasharray = '';
+ let css = 'asdcs-diagram-message';
+ if (message.type === 'request') {
+ css = `${css} asdcs-diagram-message-request`;
+ } else {
+ css = `${css} asdcs-diagram-message-response`;
+ marker = 'asdcsDiagramArrowOpen';
+ dasharray = '30, 10';
+ }
+
+ if (message.asynchronous) {
+ css = `${css} asdcs-diagram-message-asynchronous`;
+ marker = 'asdcsDiagramArrowOpen';
+ } else {
+ css = `${css} asdcs-diagram-message-synchronous`;
+ }
+
+ return { css, marker, dasharray };
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Initialize or reinitialize zoom. This sets the initial zoom in the case of
+ * a re-rendering, and initializes the eventhandling in all cases.
+ *
+ * It does some fairly risky parsing of the 'transform' attribute, assuming that it
+ * can contain scale() and translate(). But only the zoom handler and us are writing
+ * the transform values, so that's probably OK.
+ *
+ * @param gContent container.
+ * @param width diagram width.
+ * @param height diagram height.
+ * @private
+ */
+ _initZoom(gContent, width, height) {
+
+ const zoomed = function zoomed() {
+ gContent.attr('transform',
+ `translate(${d3.event.translate})scale(${d3.event.scale})`);
+ };
+
+ const viewWidth = this.state.width || this.options.svg.width;
+ const viewHeight = this.state.height || this.options.svg.height;
+ const scaleMinimum = this.options.svg.scale.minimum;
+ const scaleWidth = viewWidth / width;
+ const scaleHeight = viewHeight / height;
+
+ let scale = scaleMinimum;
+ if (this.options.svg.scale.width) {
+ scale = Math.max(scale, scaleWidth);
+ }
+ if (this.options.svg.scale.height) {
+ scale = Math.min(scale, scaleHeight);
+ }
+
+ scale = Math.max(scale, scaleMinimum);
+
+ let translate = [0, 0];
+ if (this.savedTransform) {
+ const s = this.savedTransform;
+ const scaleStart = s.indexOf('scale(');
+ if (scaleStart !== -1) {
+ scale = parseFloat(s.substring(scaleStart + 6, s.length - 1));
+ }
+ const translateStart = s.indexOf('translate(');
+ if (translateStart !== -1) {
+ const spec = s.substring(translateStart + 10, s.indexOf(')', translateStart));
+ const tokens = spec.split(',');
+ translate = [parseFloat(tokens[0]), parseFloat(tokens[1])];
+ }
+
+ gContent.attr('transform', this.savedTransform);
+ } else {
+ gContent.attr('transform', `scale(${scale})`);
+ }
+
+ const zoom = d3.behavior.zoom()
+ .scale(scale)
+ .scaleExtent([scaleMinimum, 10])
+ .translate(translate)
+ .on('zoom', zoomed);
+ this.svg.call(zoom);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Hide from the linter the fact that we're modifying the lifeline.
+ * @param lifeline to be updated with X position.
+ * @param x X position.
+ * @private
+ */
+ static _processLifeline(lifeline, x) {
+ const actor = lifeline;
+ actor.id = actor.id || actor.name;
+ actor.x = x;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Derive active occurrences for lifeline.
+ * @param oData occurrences data.
+ * @param lifelineId lifeline to be analyzed.
+ * @returns {number}
+ * @private
+ */
+ static _calcActive(oData, lifelineId) {
+ const o = oData[lifelineId];
+ let active = 0;
+ if (o && o.active) {
+ active = o.active.length;
+ }
+ return active;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Derive the X position of an occurrence on a lifeline, taking into account how
+ * many occurrences are active.
+ * @param active active count.
+ * @param x lifeline X position; basis for offset.
+ * @param arrow whether this is the arrow (to) end.
+ * @param leftToRight whether this message goes left-to-right.
+ * @returns {*} calculated X position for occurrence left-hand side.
+ * @private
+ */
+ _calcMessageX(active, x, arrow, leftToRight) {
+ const width = this.options.lifelines.occurrences.width;
+ const halfWidth = width / 2;
+ const active0 = Math.max(0, active - 1);
+ let calculated = x + (active0 * width);
+ if (arrow) {
+ // End (ARROW).
+ if (leftToRight) {
+ calculated -= halfWidth;
+ } else {
+ calculated += halfWidth;
+ }
+ } else {
+ // Start (NOT ARROW).
+ if (leftToRight) {
+ calculated += halfWidth;
+ } else {
+ calculated -= halfWidth;
+ }
+ }
+
+ return calculated;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Show popup upon hovering over a messages that has associated notes.
+ * @param id
+ * @private
+ */
+ _showNotesPopup(id) {
+ if (this.popup) {
+ if (id) {
+ const message = this.application.getModel().getMessageById(id);
+ if (message && message.notes && message.notes.length > 0 && this.events.message) {
+ this.popup.setState({
+ visible: true,
+ left: this.events.message.x - 50,
+ top: this.events.message.y + 20,
+ notes: message.notes[0],
+ });
+ }
+ } else {
+ this.popup.setState({ visible: false, notes: '' });
+ }
+ }
+ }
+}
+
+
+Diagram.propTypes = {
+ application: React.PropTypes.object.isRequired,
+};
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/components/popup/Popup.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/components/popup/Popup.jsx
new file mode 100644
index 0000000000..08c6da1e76
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/components/popup/Popup.jsx
@@ -0,0 +1,94 @@
+
+
+import React from 'react';
+
+import Icon from '../../../icons/Icon';
+import iconEdit from '../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/edit.svg';
+
+/**
+ * A hover-over popup. It shows notes, but perhaps will be put to other uses.
+ * @param props React properties.
+ * @returns {XML}
+ * @constructor
+ */
+export default class Popup extends React.Component {
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Construct react view.
+ * @param props element properties (of which there are none).
+ * @param context react context.
+ */
+ constructor(props, context) {
+ super(props, context);
+ this.state = {
+ top: 0,
+ left: 0,
+ visible: false,
+ notes: '',
+ };
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Render view.
+ * @returns {XML}
+ */
+ render() {
+
+ // Build CSS + styles to position and configure popup.
+
+ let top = this.state.top;
+ let left = this.state.left;
+
+ const popupHeight = 200;
+ const popupWidth = 320;
+
+ let auxCssVertical = 'top';
+ let auxCssHorizontal = 'left';
+
+ if (this.state.top > (window.innerHeight - popupHeight)) {
+ top -= (popupHeight + 50);
+ auxCssVertical = 'bottom';
+ }
+
+ if (this.state.left > (window.innerWidth - popupWidth)) {
+ left -= (popupWidth - 80);
+ auxCssHorizontal = 'right';
+ }
+
+ const auxCss = `asdcs-diagram-popup-${auxCssVertical}${auxCssHorizontal}`;
+ const styles = {
+ top,
+ left,
+ display: (this.state.visible ? 'block' : 'none'),
+ };
+
+ // Render element.
+
+ let notes = this.state.notes || '';
+ if (notes.length > 255) {
+ notes = notes.substring(0, 255);
+ notes = `${notes} ...`;
+ }
+
+ return (
+ <div className={`asdcs-diagram-popup ${auxCss}`} style={styles}>
+ <div className="asdcs-diagram-popup-header">Notes</div>
+ <div className="asdcs-diagram-popup-body">
+ <div className="asdcs-icon-popup">
+ <Icon glyph={iconEdit} />
+ </div>
+ <div className="asdcs-diagram-notes">
+ <div className="asdcs-diagram-note">
+ {notes}
+ </div>
+ </div>
+ </div>
+ <div className="asdcs-diagram-popup-footer"></div>
+ </div>
+ );
+ }
+}
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/diagram.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/diagram.html
new file mode 100644
index 0000000000..22893ce864
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/diagram.html
@@ -0,0 +1,56 @@
+<svg height="100%" width="100%" viewBox="<%-(x-25)%> <%-(y-25)%> <%-(width+50)%> <%-(height+50)%>" preserveAspectRatio="xMinYMin">
+ <defs>
+
+ <filter id="asdcsSvgHighlight" height="50" width="50" x="-25" y="-25">
+ <morphology in="SourceAlpha" operator="dilate" radius="25"></morphology>
+ <feGaussianBlur result="blur" stdDeviation="20"></feGaussianBlur>
+ <feComposite in2="SourceAlpha" k2="1" k3="-1" operator="arithmetic" result="hlDiff"></feComposite>
+ <feFlood flood-color="<%-floodColor%>" flood-opacity="1"></feFlood>
+ <feComposite in2="hlDiff" operator="in"></feComposite>
+ <feComposite in2="SourceGraphic" operator="over" result="withGlow"></feComposite>
+ </filter>
+
+ <marker id="asdcsDiagramArrowOpen"
+ viewBox="0 0 15 15"
+ refX="12"
+ refY="4"
+ markerWidth="15"
+ markerHeight="20"
+ orient="auto">
+ <path d="M0,0 L12,4 L0,8" class="asdcs-diagram-arrow asdcs-diagram-arrow-open" />
+ </marker>
+
+ <marker id="asdcsDiagramArrowClosed"
+ viewBox="0 0 15 15"
+ refX="12"
+ refY="4"
+ markerWidth="15"
+ markerHeight="20"
+ orient="auto">
+ <path d="M0,0 L12,4 L0,8 Z" class="asdcs-diagram-arrow asdcs-diagram-arrow-open" />
+ </marker>
+
+ <marker id="asdcsDiagramArrowSolid"
+ viewBox="0 0 15 15"
+ refX="12"
+ refY="4"
+ markerWidth="15"
+ markerHeight="20"
+ orient="auto">
+ <path d="M0,0 L12,4 L0,8 Z" class="asdcs-diagram-arrow asdcs-diagram-arrow-solid" />
+ </marker>
+
+ <!--
+ <marker id="asdcsDiagramArrowSolid"
+ viewBox="0 0 20 20"
+ refX="20"
+ refY="6"
+ markerWidth="20"
+ markerHeight="20"
+ orient="auto">
+ <path d="M0,0 L18,6 L0,12 Z" class="asdcs-diagram-arrow asdcs-diagram-arrow-solid" />
+ </marker>
+ -->
+
+ </defs>
+</svg>
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/fragment.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/fragment.html
new file mode 100644
index 0000000000..812f5fcfb8
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/fragment.html
@@ -0,0 +1,18 @@
+<g class="asdcs-diagram-fragment" data-id="<%-id%>" data-type="fragment">
+
+ <rect x="<%-x%>" y="<%-y%>" height="<%-height%>" width="<%-width%>"></rect>
+
+ <path d="M<%-x%>,<%-(y+80)%> L<%-(x+100)%>,<%-(y+80)%> L<%-(x+120)%>,<%-(y+60)%> L<%-(x+120)%>,<%-y%>"/>
+
+ <text x="<%-(x+20)%>" y="<%-(y+50)%>" class="asdcs-diagram-fragment-operation"><%-operator%></text>
+
+ <rect class="asdcs-diagram-fragment-guard-bg"
+ x="0" y="0" height="0" width="0"
+ rx="5" ry="5" ></rect>
+
+ <text class="asdcs-diagram-fragment-guard" x="<%-(x+160)%>" y="<%-(y+10)%>"><%
+ for (var lineIndex = 0; lineIndex < lines.length ; lineIndex++) {
+ %><tspan x="<%-(x+160)%>" dy="40px"><%- lines[lineIndex] %></tspan><%
+ }%></text>
+
+</g>
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/lifeline.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/lifeline.html
new file mode 100644
index 0000000000..cd01d42c5a
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/lifeline.html
@@ -0,0 +1,19 @@
+<g class="asdcs-diagram-lifeline-container" data-id="<%-id%>" data-type="lifeline">
+
+ <rect x="<%-(x-(headerWidth/2))%>" y="<%-(y0)%>" width="<%-headerWidth%>" height="<%-headerHeight%>" class="asdcs-diagram-lifeline-heading-box"></rect>
+
+ <text x="<%-x%>" y="<%-(headerHeight * ((rows-lines.length)/10))%>" class="asdcs-diagram-lifeline-heading-label"><%
+ for (var linesIndex = 0; linesIndex < lines.length && linesIndex < rows ; linesIndex++) {
+ %><tspan x="<%-x%>" dy="<%-(headerHeight/rows)-5%>px"><%- lines[linesIndex] %></tspan><%
+ }
+ %></text>
+
+ <rect x="<%-(x-5)%>" y="<%-(y0+headerHeight)%>" width="10" height="<%-(y1-(y0+headerHeight))%>" class="asdcs-diagram-lifeline-bg"></rect>
+
+ <path d="M<%-x%>,<%-(y0+headerHeight)%> L<%-x%>,<%-y1%>"
+ class="asdcs-diagram-lifeline-selectable"></path>
+
+ <path d="M<%-x%>,<%-(y0+headerHeight)%> L<%-x%>,<%-y1%>"
+ class="asdcs-diagram-lifeline"></path>
+
+</g>
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/message.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/message.html
new file mode 100644
index 0000000000..bd4c33a016
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/message.html
@@ -0,0 +1,29 @@
+<g class="asdcs-diagram-message-container" data-id="<%-id%>" data-type="lifeline">
+
+ <%
+ var delta = 40;
+ var x = (x0 + x1) / 2;
+ var y0 = y - ((labels.length + 1) * delta);
+ %>
+
+ <rect class="asdcs-diagram-message-label-bg"
+ x="<%-x%>"
+ y="<%-y0%>"
+ height="10"
+ width="10"
+ rx="10" ry="10" ></rect>
+
+ <text class="asdcs-diagram-message-label" x="<%-x%>" y="<%-y0%>"><%
+ for (var labelIndex = 0; labelIndex < labels.length && labelIndex < lines ; labelIndex++) {
+ %><tspan x="<%-x%>" dy="<%-delta%>px"><%- labels[labelIndex] %></tspan><%
+ }%></text>
+
+
+ <rect x="<%-Math.min(x0,x1)%>" y="<%-(y-5)%>" width="<%-Math.abs(x1-x0)%>" height="10" class="asdcs-diagram-message-bg"></rect>
+
+ <path class="asdcs-diagram-message-selectable" d="<%-path%>"></path>
+
+ <path class="<%-classes%>" marker-end="url(#<%-marker%>)" stroke-dasharray="<%-dasharray%>"
+ data-id="<%-id%>" data-type="message" d="<%-path%>"></path>
+
+</g>
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/occurrence.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/occurrence.html
new file mode 100644
index 0000000000..0af9ff3d68
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/occurrence.html
@@ -0,0 +1,7 @@
+<g>
+ <rect class="asdcs-diagram-occurrence"
+ x="<%-x%>"
+ y="<%-y%>"
+ width="<%-width%>"
+ height="<%-height%>" />
+</g>
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/title.html b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/title.html
new file mode 100644
index 0000000000..b7a5d68a6d
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/diagram/templates/title.html
@@ -0,0 +1,3 @@
+<g class="asdcs-diagram-title">
+ <text x="<%-x%>" y="<%-y%>"><%-title%></text>
+</g>
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/dialog/Dialog.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/dialog/Dialog.jsx
new file mode 100644
index 0000000000..4429d80bc6
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/dialog/Dialog.jsx
@@ -0,0 +1,222 @@
+
+
+import React from 'react';
+
+import Icon from '../icons/Icon';
+import iconQuestion from '../../../../../../res/ecomp/asdc/sequencer/sprites/icon/question.svg';
+import iconExclaim from '../../../../../../res/ecomp/asdc/sequencer/sprites/icon/exclaim.svg';
+import iconInfo from '../../../../../../res/ecomp/asdc/sequencer/sprites/icon/info.svg';
+import iconEdit from '../../../../../../res/ecomp/asdc/sequencer/sprites/icon/edit.svg';
+import iconClose from '../../../../../../res/ecomp/asdc/sequencer/sprites/icon/close.svg';
+
+/**
+ * Multi-purpose dialog. Rendered into the page on initialization, and then
+ * configured, shown and hidden as required.
+ */
+export default class Dialog extends React.Component {
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Construct view.
+ */
+ constructor(props, context) {
+
+ super(props, context);
+
+ this.MODE = {
+ INFO: {
+ icon: 'asdcs-icon-info',
+ heading: 'Information',
+ },
+ ERROR: {
+ icon: 'asdcs-icon-exclaim',
+ heading: 'Error',
+ },
+ EDIT: {
+ icon: 'asdcs-icon-edit',
+ heading: 'Edit',
+ edit: true,
+ confirm: true,
+ },
+ CONFIRM: {
+ icon: 'asdcs-icon-question',
+ heading: 'Confirm',
+ confirm: true,
+ },
+ };
+
+ this.state = {
+ mode: this.MODE.INFO,
+ message: '',
+ text: '',
+ visible: false,
+ };
+
+ // Bindings.
+
+ this.onClickOK = this.onClickOK.bind(this);
+ this.onClickCancel = this.onClickCancel.bind(this);
+ this.onChangeText = this.onChangeText.bind(this);
+ this.showConfirmDialog = this.showConfirmDialog.bind(this);
+ this.showInfoDialog = this.showInfoDialog.bind(this);
+ this.showEditDialog = this.showEditDialog.bind(this);
+ this.showErrorDialog = this.showErrorDialog.bind(this);
+ this.showDialog = this.showDialog.bind(this);
+
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Show info dialog.
+ * @param message info message.
+ */
+ showInfoDialog(message) {
+ this.showDialog(this.MODE.INFO, { message });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Show error dialog.
+ * @param message error message.
+ */
+ showErrorDialog(message) {
+ this.showDialog(this.MODE.ERROR, { message });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Show edit dialog.
+ * @param message dialog message.
+ * @param text current edit text.
+ * @param callback callback function to be invoked on OK.
+ */
+ showEditDialog(message, text, callback) {
+ this.showDialog(this.MODE.EDIT, { message, text, callback });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Show confirmation dialog.
+ * @param message dialog message.
+ * @param callback callback function to be invoked on OK.
+ */
+ showConfirmDialog(message, callback) {
+ this.showDialog(this.MODE.CONFIRM, { message, callback });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle buttonclick.
+ */
+ onClickOK() {
+ this.props.application.hideOverlay();
+ this.setState({ visible: false });
+ if (this.callback) {
+
+ // So far the only thing we can return is edit text, but send it back
+ // as properties to allow for future return values.
+
+ this.callback({ text: this.state.text });
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle buttonclick.
+ */
+ onClickCancel() {
+ this.props.application.hideOverlay();
+ this.setState({ visible: false });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle text changes.
+ * @param event update event.
+ */
+ onChangeText(event) {
+ this.setState({ text: event.target.value });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Show dialog in specified configuration.
+ * @param mode dialog mode.
+ * @param args dialog parameters, varying slightly by dialog type.
+ * @private
+ */
+ showDialog(mode, args) {
+ this.props.application.showOverlay();
+ this.callback = args.callback;
+ this.setState({
+ mode,
+ visible: true,
+ message: args.message || '',
+ text: args.text || '',
+ });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Render dialog into the page, initially hidden.
+ */
+ render() {
+
+ const dialogClass = (this.state.visible) ? '' : 'asdcs-hidden';
+ const cancelClass = (this.callback) ? '' : 'asdcs-hidden';
+ const textClass = (this.state.mode === this.MODE.EDIT) ? '' : 'asdcs-hidden';
+
+ return (
+ <div className={`asdcs-dialog ${dialogClass}`}>
+ <div className="asdcs-dialog-header">{this.state.mode.heading}</div>
+ <div className="asdcs-dialog-close" onClick={this.onClickCancel} >
+ <Icon glyph={iconClose} className={this.MODE.CONFIRM.icon} />
+ </div>
+ <div className={`asdcs-dialog-icon ${this.state.mode.icon}`}>
+ <Icon glyph={iconQuestion} className={this.MODE.CONFIRM.icon} />
+ <Icon glyph={iconExclaim} className={this.MODE.ERROR.icon} />
+ <Icon glyph={iconInfo} className={this.MODE.INFO.icon} />
+ <Icon glyph={iconEdit} className={this.MODE.EDIT.icon} />
+ </div>
+ <div className="asdcs-dialog-message">
+ {this.state.message}
+ </div>
+ <div className={`asdcs-dialog-text ${textClass}`}>
+ <textarea
+ maxLength="255"
+ value={this.state.text}
+ onChange={this.onChangeText}
+ />
+ </div>
+ <div className="asdcs-dialog-buttonbar">
+ <button
+ className={`asdcs-dialog-button-cancel ${cancelClass}`}
+ onClick={this.onClickCancel}
+ >
+ Cancel
+ </button>
+ <button
+ className="asdcs-dialog-button-ok"
+ onClick={this.onClickOK}
+ >
+ OK
+ </button>
+ </div>
+ </div>
+ );
+ }
+}
+
+Dialog.propTypes = {
+ application: React.PropTypes.object.isRequired,
+};
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/Editor.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/Editor.jsx
new file mode 100644
index 0000000000..09703b84bf
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/Editor.jsx
@@ -0,0 +1,171 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+
+import Logger from '../../common/Logger';
+import Common from '../../common/Common';
+import Designer from './components/designer/Designer';
+import Toolbar from './components/toolbar/Toolbar';
+import Source from './components/source/Source';
+
+/**
+ * Editor view, aggregating the designer, the code editor, the toolbar.
+ */
+export default class Editor extends React.Component {
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Construct React view.
+ * @param props properties.
+ * @param context context.
+ */
+ constructor(props, context) {
+ super(props, context);
+
+ this.application = Common.assertNotNull(props.application);
+ this.demo = this.application.getOptions().demo;
+
+ // Bindings.
+
+ this.selectMessage = this.selectMessage.bind(this);
+ this.selectLifeline = this.selectLifeline.bind(this);
+
+ this.onMouseDown = this.onMouseDown.bind(this);
+ this.onMouseUp = this.onMouseUp.bind(this);
+ this.onMouseMove = this.onMouseMove.bind(this);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Select message by ID.
+ * @param id message ID.
+ */
+ selectMessage(id) {
+ if (this.designer) {
+ this.designer.selectMessage(id);
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Select lifeline by ID.
+ * @param id lifeline ID.
+ */
+ selectLifeline(id) {
+ if (this.designer) {
+ this.designer.selectLifeline(id);
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Record that we're dragging.
+ */
+ onMouseDown() {
+ if (this.editor) {
+ this.resize = {
+ initialWidth: this.editor.offsetWidth,
+ initialPageX: undefined,
+ };
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Record that we're not dragging.
+ */
+ onMouseUp() {
+ this.resize = undefined;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Record mouse movement.
+ */
+ onMouseMove(event) {
+ if (this.resize) {
+ if (this.editor) {
+ if (this.resize.initialPageX) {
+ const deltaX = event.pageX - this.resize.initialPageX;
+ const newWidth = this.resize.initialWidth + deltaX;
+ const newWidthBounded = Math.min(800, Math.max(400, newWidth));
+ this.editor.style.width = `${newWidthBounded}px`;
+ } else {
+ this.resize.initialPageX = event.pageX;
+ }
+ }
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Render editor.
+ */
+ render() {
+
+ Logger.info('Editor.jsx - render()');
+
+ return (
+
+ <div
+ className="asdcs-editor"
+ ref={(r) => { this.editor = r; }}
+ >
+
+ <Toolbar application={this.props.application} editor={this} />
+
+ <div className="asdcs-editor-content">
+ <Source application={this.props.application} />
+ <Designer
+ application={this.props.application}
+ ref={(r) => {
+ if (r) {
+ this.designer = r.getDecoratedComponentInstance();
+ } else {
+ this.designer = null;
+ }
+ }}
+ />
+ </div>
+
+ <div className="asdcs-editor-statusbar">
+ <div className="asdcs-editor-status"></div>
+ <div className="asdcs-editor-validation"></div>
+ </div>
+
+ <div
+ className="asdcs-editor-resize-handle"
+ onMouseDown={this.onMouseDown}
+ onMouseUp={this.onMouseUp}
+ >
+ </div>
+ </div>
+ );
+ }
+}
+
+/** Element properties. */
+Editor.propTypes = {
+ application: React.PropTypes.object.isRequired,
+};
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/Designer.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/Designer.jsx
new file mode 100644
index 0000000000..69cdd17ed5
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/Designer.jsx
@@ -0,0 +1,403 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+
+import HTML5Backend from 'react-dnd-html5-backend';
+import { DragDropContext } from 'react-dnd';
+
+import Common from '../../../../common/Common';
+import Logger from '../../../../common/Logger';
+
+import Actions from './components/actions/Actions';
+import Lifelines from './components/lifeline/Lifelines';
+import Messages from './components/message/Messages';
+import Metadata from './components/metadata/Metadata';
+
+import Icon from '../../../icons/Icon';
+import iconExpanded from '../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/expanded.svg';
+import iconCollapsed from '../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/collapsed.svg';
+
+/**
+ * LHS design wid` view.
+ */
+class Designer extends React.Component {
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Construct view.
+ * @param props element properties.
+ * @param context react context.
+ */
+ constructor(props, context) {
+ super(props, context);
+
+ Logger.noop();
+
+ this.application = Common.assertNotNull(props.application);
+
+ this.state = {
+ lifelinesExpanded: false,
+ messagesExpanded: true,
+ activeLifelineId: undefined,
+ activeMessageId: undefined,
+ };
+
+ // Bind this.
+
+ this.onToggle = this.onToggle.bind(this);
+ this.onMouseEnterLifeline = this.onMouseEnterLifeline.bind(this);
+ this.onMouseLeaveLifeline = this.onMouseLeaveLifeline.bind(this);
+ this.onMouseEnterMessage = this.onMouseEnterMessage.bind(this);
+ this.onMouseLeaveMessage = this.onMouseLeaveMessage.bind(this);
+
+ this.addMessage = this.addMessage.bind(this);
+ this.updateMessage = this.updateMessage.bind(this);
+ this.deleteMessage = this.deleteMessage.bind(this);
+ this.addLifeline = this.addLifeline.bind(this);
+ this.updateLifeline = this.updateLifeline.bind(this);
+ this.deleteLifeline = this.deleteLifeline.bind(this);
+
+ this.selectMessage = this.selectMessage.bind(this);
+ this.selectLifeline = this.selectLifeline.bind(this);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Select message by ID.
+ * @param id message ID.
+ */
+ selectMessage(id) {
+
+ // TODO: scroll into view.
+
+ this.setState({ activeMessageId: id });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Select lifeline by ID.
+ * @param id lifeline ID.
+ */
+ selectLifeline(id) {
+
+ // TODO: scroll into view.
+
+ this.setState({ activeLifelineId: id });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Show/hide lifelines section.
+ */
+ onToggle() {
+ const lifelinesExpanded = !this.state.lifelinesExpanded;
+ const messagesExpanded = !lifelinesExpanded;
+ this.setState({ lifelinesExpanded, messagesExpanded });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle mouse event.
+ * @param id lifeline identifier.
+ */
+ onMouseEnterLifeline(id) {
+ this.application.selectLifeline(id);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle mouse event.
+ */
+ onMouseLeaveLifeline() {
+ this.application.selectLifeline();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle mouse event.
+ * @param id message identifier.
+ */
+ onMouseEnterMessage(id) {
+ this.application.selectMessage(id);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle mouse event.
+ */
+ onMouseLeaveMessage() {
+ // Only on next selection.
+ // this.application.selectMessage();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Add new message.
+ */
+ addMessage() {
+
+ if (this.application.getModel().unwrap().diagram.lifelines.length < 2) {
+ self.application.showErrorDialog('You need at least two lifelines.');
+ return;
+ }
+
+ this.application.getModel().addMessage();
+ this.forceUpdate();
+ this.application.renderDiagram();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Apply property changes to the message identified by props.id.
+ * @param props properties to be updated (excluding 'id').
+ */
+ updateMessage(props) {
+ Common.assertPlainObject(props);
+ const model = this.application.getModel();
+ const message = model.getMessageById(props.id);
+ if (message) {
+ for (const k of Object.keys(props)) {
+ if (k !== 'id') {
+ message[k] = props[k];
+ }
+ }
+ }
+ this.forceUpdate();
+ this.application.renderDiagram();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Delete message after confirmation.
+ * @param id ID of message to be deleted.
+ */
+ deleteMessage(id) {
+
+ const self = this;
+ const model = this.application.getModel();
+
+ const confirmComplete = function f() {
+ model.deleteMessageById(id);
+ self.render();
+ self.application.renderDiagram();
+ };
+
+ this.application.showConfirmDialog('Delete this message?',
+ confirmComplete);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Add new lifeline.
+ */
+ addLifeline() {
+ this.application.getModel().addLifeline();
+ this.forceUpdate();
+ this.application.renderDiagram();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Apply property changes to the lifeline identified by props.id.
+ * @param props properties to be updated (excluding 'id').
+ */
+ updateLifeline(props) {
+ Common.assertPlainObject(props);
+ const model = this.application.getModel();
+ const lifeline = model.getLifelineById(props.id);
+ if (lifeline) {
+ for (const k of Object.keys(props)) {
+ if (k !== 'id') {
+ lifeline[k] = props[k];
+ }
+ }
+ }
+ this.forceUpdate();
+ this.application.renderDiagram();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Delete lifeline after confirmation.
+ * @param id candidate for deletion.
+ */
+ deleteLifeline(id) {
+
+ const self = this;
+ const model = this.application.getModel();
+
+ const confirmComplete = function f() {
+ model.deleteLifelineById(id);
+ self.forceUpdate();
+ self.application.renderDiagram();
+ };
+ this.application.showConfirmDialog('Delete this lifeline and all its steps?',
+ confirmComplete);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Render designer.
+ */
+ render() {
+
+ const application = this.props.application;
+ const model = application.getModel();
+ const diagram = model.unwrap().diagram;
+ const metadata = diagram.metadata;
+
+ const lifelinesIcon = this.state.lifelinesExpanded ? iconExpanded : iconCollapsed;
+ const lifelinesClass = this.state.lifelinesExpanded ? '' : 'asdcs-hidden';
+ const messagesIcon = this.state.messagesExpanded ? iconExpanded : iconCollapsed;
+ const messagesClass = this.state.messagesExpanded ? '' : 'asdcs-hidden';
+
+ return (
+
+ <div className="asdcs-editor-designer">
+ <div className="asdcs-designer-accordion">
+
+ <div className="asdcs-designer-metadata-container">
+ <Metadata metadata={metadata} />
+ </div>
+
+ <h3 onClick={this.onToggle}>Lifelines
+ <div className="asdcs-designer-icon" onClick={this.onToggle}>
+ <Icon glyph={lifelinesIcon} />
+ </div>
+ </h3>
+
+ <div className={`asdcs-designer-lifelines-container ${lifelinesClass}`}>
+ <Lifelines
+ application={this.application}
+ designer={this}
+ activeLifelineId={this.state.activeLifelineId}
+ />
+ </div>
+
+ <h3 onClick={this.onToggle}>Steps
+ <div className="asdcs-designer-icon" onClick={this.onToggle}>
+ <Icon glyph={messagesIcon} />
+ </div>
+ </h3>
+
+ <div className={`asdcs-designer-steps-container ${messagesClass}`} >
+ <Messages
+ application={this.application}
+ designer={this}
+ activeMessageId={this.state.activeMessageId}
+ />
+ </div>
+
+ </div>
+
+ <Actions
+ application={this.props.application}
+ model={model}
+ ref={(r) => { this.actions = r; }}
+ />
+
+ </div>
+ );
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Scroll accordion pane to make
+ * @param $element focused element.
+ * @private
+ */
+ static _scrollIntoView($element) {
+ const $pane = $element.closest('.ui-accordion-content');
+ const paneScrollTop = $pane.scrollTop();
+ const paneHeight = $pane.height();
+ const paneBottom = paneScrollTop + paneHeight;
+ const elementTop = $element[0].offsetTop - $pane[0].offsetTop;
+ const elementHeight = $element.height();
+ const elementBottom = elementTop + elementHeight;
+ if (elementBottom > paneBottom) {
+ $pane.scrollTop(elementTop);
+ } else if (elementTop < paneScrollTop) {
+ $pane.scrollTop(elementTop);
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Show actions menu.
+ * @param id selected message ID.
+ * @param position page coordinates.
+ */
+ showActions(id, position) {
+ if (this.actions) {
+ this.actions.show(id, position);
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Show notes popup.
+ * @param id selected message identifier.
+ */
+ showNotes(id) {
+ const model = this.application.getModel();
+ const options = this.application.getOptions();
+ const message = model.getMessageById(id);
+ const notes = (message.notes && (message.notes.length > 0)) ? message.notes[0] : '';
+ const editComplete = function f(p) {
+ message.notes = [];
+ if (p && p.text) {
+ const sanitized = Common.sanitizeText(p.text, options, 'notes');
+ message.notes.push(sanitized);
+ }
+ };
+ this.application.showEditDialog('Notes:', notes, editComplete);
+ }
+}
+
+/** Element properties. */
+Designer.propTypes = {
+ application: React.PropTypes.object.isRequired,
+};
+
+export default DragDropContext(HTML5Backend)(Designer);
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/actions/Actions.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/actions/Actions.jsx
new file mode 100644
index 0000000000..851da78870
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/actions/Actions.jsx
@@ -0,0 +1,471 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+import Select from 'react-select';
+
+import Common from '../../../../../../common/Common';
+import Logger from '../../../../../../common/Logger';
+
+import Icon from '../../../../../icons/Icon';
+import iconSettings from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/settings.svg';
+import iconExpanded from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/expanded.svg';
+import iconCollapsed from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/collapsed.svg';
+import iconOccurrenceDefault from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/occurrence-default.svg';
+import iconOccurrenceStart from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/occurrence-start.svg';
+import iconOccurrenceStop from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/occurrence-stop.svg';
+import iconFragmentDefault from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/fragment-default.svg';
+import iconFragmentStart from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/fragment-start.svg';
+import iconFragmentStop from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/fragment-stop.svg';
+
+/**
+ * Action menu view.
+ */
+export default class Actions extends React.Component {
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Construct view.
+ * @param props element properties.
+ * @param context react context.
+ */
+ constructor(props, context) {
+ super(props, context);
+
+ Logger.noop();
+
+ this.state = {
+ id: undefined,
+ visible: false,
+ };
+
+ // Bindings.
+
+ this.show = this.show.bind(this);
+
+ this.onClickOccurrenceToggle = this.onClickOccurrenceToggle.bind(this);
+ this.onClickOccurrenceFrom = this.onClickOccurrenceFrom.bind(this);
+ this.onClickOccurrenceTo = this.onClickOccurrenceTo.bind(this);
+
+ this.onClickFragmentToggle = this.onClickFragmentToggle.bind(this);
+ this.onChangeFragmentGuard = this.onChangeFragmentGuard.bind(this);
+ this.onChangeFragmentOperator = this.onChangeFragmentOperator.bind(this);
+
+ this.onMouseOut = this.onMouseOut.bind(this);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Show for message.
+ * @param id message ID.
+ * @param position xy coordinates.
+ */
+ show(id, position) {
+ const message = this.props.model.getMessageById(id);
+
+ let occurrencesToggle = false;
+ let fragmentToggle = false;
+ if (message) {
+
+ message.occurrences = message.occurrences || { start: [], stop: [] };
+ message.occurrences.start = message.occurrences.start || [];
+ message.occurrences.stop = message.occurrences.stop || [];
+ message.fragment = message.fragment || {};
+ message.fragment.start = message.fragment.start || false;
+ message.fragment.stop = message.fragment.stop || false;
+ message.fragment.guard = message.fragment.guard || '';
+ message.fragment.operator = message.fragment.operator || '';
+
+ const mo = message.occurrences;
+ occurrencesToggle = (mo.start.length > 0 || mo.stop.length > 0);
+
+ const mf = message.fragment;
+ fragmentToggle = (mf.start || mf.stop);
+ }
+
+ this.setState({
+ id,
+ message,
+ occurrencesToggle,
+ fragmentToggle,
+ visible: true,
+ x: position.x,
+ y: position.y,
+ });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Toggle occurrence state.
+ */
+ onClickOccurrenceToggle() {
+ const message = this.state.message;
+ if (message) {
+ const oFromState = Actions.getOccurrenceState(message.occurrences, message.from);
+ const oToState = Actions.getOccurrenceState(message.occurrences, message.to);
+ const oExpanded = oFromState > 0 || oToState > 0;
+ if (oExpanded) {
+ this.setState({ occurrencesExpanded: true });
+ } else {
+ const occurrencesExpanded = !this.state.occurrencesExpanded;
+ this.setState({ occurrencesExpanded });
+ }
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle menu click.
+ */
+ onClickOccurrenceFrom() {
+ const message = this.state.message;
+ if (message) {
+ Actions._toggleOccurrence(message.occurrences, message.from);
+ }
+ this.setState({ message });
+ this.props.application.renderDiagram();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle menu click.
+ */
+ onClickOccurrenceTo() {
+ const message = this.state.message;
+ if (message) {
+ Actions._toggleOccurrence(message.occurrences, message.to);
+ }
+ this.setState({ message });
+ this.props.application.renderDiagram();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Toggle fragment.
+ */
+ onClickFragmentToggle() {
+ const message = this.state.message;
+ if (message) {
+ Actions._toggleFragment(message.fragment);
+ }
+ this.setState({ message });
+ this.props.application.renderDiagram();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle menu click.
+ * @param event update event.
+ */
+ onChangeFragmentGuard(event) {
+ const message = this.state.message;
+ if (message) {
+ const options = this.props.application.getOptions();
+ message.fragment.guard = Common.sanitizeText(event.target.value, options, 'guard');
+ }
+ this.setState({ message });
+ this.props.application.renderDiagram();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle menu click.
+ * @param value updated value.
+ */
+ onChangeFragmentOperator(value) {
+ const message = this.state.message;
+ if (message) {
+ message.fragment.operator = value.value;
+ }
+ this.setState({ message });
+ this.props.application.renderDiagram();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle mouse movement.
+ */
+ onMouseOut() {
+ this.setState({ id: -1, visible: false, x: 0, y: 0 });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Render view.
+ * @returns {XML}
+ */
+ render() {
+
+ const actionsStyles = { };
+ const message = this.state.message;
+ if (!message || !this.state.visible) {
+
+ // Invisible.
+
+ return (<div className="asdcs-actions" ></div>);
+ }
+
+ // Position and display.
+
+ actionsStyles.display = 'block';
+ actionsStyles.left = this.state.x - 10;
+ actionsStyles.top = this.state.y - 10;
+
+ const oFromState = Actions.getOccurrenceState(message.occurrences, message.from);
+ const oToState = Actions.getOccurrenceState(message.occurrences, message.to);
+ const fState = Actions.getFragmentState(message.fragment);
+
+ const oExpanded = this.state.occurrencesExpanded || (oFromState > 0) || (oToState > 0);
+ const oAuxClassName = oExpanded ? '' : 'asdcs-hidden';
+
+ const fExpanded = fState !== 0;
+ const fAuxClassName = fExpanded ? '' : 'asdcs-hidden';
+
+ const fragmentOperatorOptions = [{
+ value: 'alt',
+ label: 'Alternate',
+ }, {
+ value: 'opt',
+ label: 'Optional',
+ }, {
+ value: 'loop',
+ label: 'Loop',
+ }];
+
+ const operator = message.fragment.operator || 'alt';
+
+ return (
+ <div
+ className="asdcs-actions"
+ style={actionsStyles}
+ onMouseLeave={this.onMouseOut}
+ >
+ <div className="asdcs-actions-header">
+ <div className="asdcs-actions-icon">
+ <Icon glyph={iconSettings} />
+ </div>
+ </div>
+
+ <div className="asdcs-actions-options">
+
+ <div className="asdcs-actions-optiongroup asdcs-actions-optiongroup-occurrence">
+ <div
+ className="asdcs-actions-option asdcs-actions-option-occurrence-toggle"
+ onClick={this.onClickOccurrenceToggle}
+ >
+ <span className="asdcs-label">Occurrence</span>
+ <div className="asdcs-actions-state">
+ <Icon glyph={iconCollapsed} className={oExpanded ? 'asdcs-hidden' : ''} />
+ <Icon glyph={iconExpanded} className={oExpanded ? '' : 'asdcs-hidden'} />
+ </div>
+ </div>
+ </div>
+
+ <div
+ className={`asdcs-actions-option asdcs-actions-option-occurrence-from ${oAuxClassName}`}
+ onClick={this.onClickOccurrenceFrom}
+ >
+ <span className="asdcs-label">From</span>
+ <div className="asdcs-actions-state">
+ <span className="asdcs-annotation"></span>
+ <Icon glyph={iconOccurrenceDefault} className={oFromState === 0 ? '' : 'asdcs-hidden'} />
+ <Icon glyph={iconOccurrenceStart} className={oFromState === 1 ? '' : 'asdcs-hidden'} />
+ <Icon glyph={iconOccurrenceStop} className={oFromState === 2 ? '' : 'asdcs-hidden'} />
+ </div>
+ </div>
+
+ <div
+ className={`asdcs-actions-option asdcs-actions-option-occurrence-to ${oAuxClassName}`}
+ onClick={this.onClickOccurrenceTo}
+ >
+ <span className="asdcs-label">To</span>
+ <div className="asdcs-actions-state">
+ <span className="asdcs-annotation"></span>
+ <Icon glyph={iconOccurrenceDefault} className={oToState === 0 ? '' : 'asdcs-hidden'} />
+ <Icon glyph={iconOccurrenceStart} className={oToState === 1 ? '' : 'asdcs-hidden'} />
+ <Icon glyph={iconOccurrenceStop} className={oToState === 2 ? '' : 'asdcs-hidden'} />
+ </div>
+ </div>
+
+ <div className="asdcs-actions-optiongroup asdcs-actions-optiongroup-fragment">
+ <div
+ className="asdcs-actions-option asdcs-actions-fragment-toggle"
+ onClick={this.onClickFragmentToggle}
+ >
+ <span className="asdcs-label">Fragment</span>
+ <div className="asdcs-actions-state">
+ <span className="asdcs-annotation"></span>
+ <Icon glyph={iconFragmentDefault} className={fState === 0 ? '' : 'asdcs-hidden'} />
+ <Icon glyph={iconFragmentStart} className={fState === 1 ? '' : 'asdcs-hidden'} />
+ <Icon glyph={iconFragmentStop} className={fState === 2 ? '' : 'asdcs-hidden'} />
+ </div>
+ </div>
+ </div>
+
+ <div className={`asdcs-actions-option asdcs-actions-fragment-operator ${fAuxClassName}`}>
+ <div className="asdcs-label">Operator</div>
+ <div className="asdcs-value">
+ <Select
+ className="asdcs-editable-select"
+ openOnFocus
+ clearable={false}
+ searchable={false}
+ value={operator}
+ onChange={this.onChangeFragmentOperator}
+ options={fragmentOperatorOptions}
+ />
+ </div>
+ </div>
+
+ <div className={`asdcs-actions-option asdcs-actions-fragment-guard ${fAuxClassName}`}>
+ <div className="asdcs-label">Guard</div>
+ <div className="asdcs-value">
+ <input
+ className="asdcs-editable"
+ type="text"
+ size="20"
+ maxLength="80"
+ value={message.fragment.guard}
+ placeholder="Condition"
+ onChange={this.onChangeFragmentGuard}
+ />
+ </div>
+ </div>
+
+ </div>
+
+ <div className="asdcs-actions-footer"></div>
+
+ </div>
+
+ );
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Toggle through three occurrence states on click.
+ * @param occurrence occurrences state, updated as side-effect.
+ * @param lifelineId message end that's being toggled.
+ * @private
+ */
+ static _toggleOccurrence(occurrence, lifelineId) {
+ const o = occurrence;
+
+ const rm = function rm(array, value) {
+ const index = array.indexOf(value);
+ if (index !== -1) {
+ array.splice(index, 1);
+ }
+ };
+
+ const add = function add(array, value) {
+ if (array.indexOf(value) === -1) {
+ array.push(value);
+ }
+ };
+
+ if (o.start && o.start.indexOf(lifelineId) !== -1) {
+ // Start -> stop.
+ rm(o.start, lifelineId);
+ add(o.stop, lifelineId);
+ } else if (o.stop && o.stop.indexOf(lifelineId) !== -1) {
+ // Stop -> default.
+ rm(o.start, lifelineId);
+ rm(o.stop, lifelineId);
+ } else {
+ // Default -> start.
+ add(o.start, lifelineId);
+ rm(o.stop, lifelineId);
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Toggle fragment setting on click.
+ * @param fragment
+ * @private
+ **/
+ static _toggleFragment(fragment) {
+ const f = fragment;
+ if (f.start === true) {
+ f.start = false;
+ f.stop = true;
+ } else if (f.stop === true) {
+ f.stop = false;
+ f.start = false;
+ } else {
+ f.start = true;
+ f.stop = false;
+ }
+ f.guard = '';
+ f.operator = '';
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Get ternary occurrences state.
+ * @param o occurrences.
+ * @param lifelineId from/to lifeline ID.
+ * @returns {number}
+ * @private
+ */
+ static getOccurrenceState(o, lifelineId) {
+ if (o.start.indexOf(lifelineId) !== -1) {
+ return 1;
+ }
+ if (o.stop.indexOf(lifelineId) !== -1) {
+ return 2;
+ }
+ return 0;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Get ternary fragment state.
+ * @param f fragment.
+ * @returns {number}
+ * @private
+ */
+ static getFragmentState(f) {
+ if (f.start) {
+ return 1;
+ }
+ if (f.stop) {
+ return 2;
+ }
+ return 0;
+ }
+}
+
+/** Element properties. */
+Actions.propTypes = {
+ application: React.PropTypes.object.isRequired,
+ model: React.PropTypes.object.isRequired,
+};
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/Lifeline.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/Lifeline.jsx
new file mode 100644
index 0000000000..e8d8cffb7a
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/Lifeline.jsx
@@ -0,0 +1,264 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+import { DragSource, DropTarget } from 'react-dnd';
+
+import Common from '../../../../../../common/Common';
+
+import Icon from '../../../../../icons/Icon';
+import iconHandle from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/handle.svg';
+import iconDelete from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/delete.svg';
+
+/**
+ * LHS lifeline row view.
+ */
+class Lifeline extends React.Component {
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Construct editor view.
+ * @param props element properties.
+ * @param context react context.
+ */
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ active: false,
+ name: props.lifeline.name,
+ };
+
+ const metamodel = Common.assertNotNull(this.props.metamodel).unwrap();
+ this.canReorder = metamodel.diagram.lifelines.constraints.reorder;
+ this.canDelete = metamodel.diagram.lifelines.constraints.delete;
+
+ // Bindings.
+
+ this.onChangeName = this.onChangeName.bind(this);
+ this.onBlurName = this.onBlurName.bind(this);
+ this.onClickDelete = this.onClickDelete.bind(this);
+ this.onMouseEnter = this.onMouseEnter.bind(this);
+ this.onMouseLeave = this.onMouseLeave.bind(this);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle name change.
+ * @param event change event.
+ */
+ onChangeName(event) {
+ this.setState({ name: event.target.value });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle name change.
+ * @param event change event.
+ */
+ onBlurName(event) {
+ const options = this.props.application.getOptions();
+ const sanitized = Common.sanitizeText(event.target.value, options, 'lifeline');
+ const props = {
+ id: this.props.lifeline.id,
+ name: sanitized,
+ };
+ this.props.designer.updateLifeline(props);
+ this.setState({ name: sanitized });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle lifeline delete.
+ */
+ onClickDelete() {
+ this.props.designer.deleteLifeline(this.props.lifeline.id);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle mouseover event.
+ */
+ onMouseEnter() {
+ this.setState({ active: true });
+ this.props.designer.onMouseEnterLifeline(this.props.lifeline.id);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle mouseleave event.
+ */
+ onMouseLeave() {
+ this.setState({ active: false });
+ this.props.designer.onMouseLeaveLifeline(this.props.lifeline.id);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Get whether metadata permits reorder.
+ * @returns true if reorderable.
+ */
+ isCanReorder() {
+ return this.canReorder;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Get whether metadata permits delete.
+ * @returns true if lifeline can be deleted.
+ */
+ isCanDelete() {
+ return this.canDelete;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * React render.
+ * @returns {*}
+ */
+ render() {
+
+ const id = this.props.lifeline.id;
+ const activeClass = (this.props.active === true) ? 'asdcs-active' : '';
+ const { connectDragSource, connectDropTarget } = this.props;
+ return connectDragSource(connectDropTarget(
+
+ <div
+ className={`asdcs-designer-lifeline ${activeClass}`}
+ data-id={id}
+ onMouseEnter={this.onMouseEnter}
+ onMouseLeave={this.onMouseLeave}
+ >
+ <table className="asdcs-designer-layout asdcs-designer-lifeline-row1">
+ <tbody>
+ <tr>
+ <td>
+ <div className="asdcs-designer-sort asdcs-designer-icon">
+ <Icon glyph={iconHandle} />
+ </div>
+ </td>
+ <td>
+ <div className="asdcs-designer-lifeline-index">{this.props.lifeline.index}.</div>
+ </td>
+ <td>
+ <div className="asdcs-designer-lifeline-name">
+ <input
+ type="text"
+ className="asdcs-editable"
+ placeholder="Unnamed"
+ value={this.state.name}
+ onChange={this.onChangeName}
+ onBlur={this.onBlurName}
+ />
+ </div>
+ </td>
+ <td>
+ <div className="asdcs-designer-delete asdcs-designer-icon" onClick={this.onClickDelete}>
+ <Icon glyph={iconDelete} />
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ ));
+ }
+}
+
+/**
+ * Declare properties.
+ */
+Lifeline.propTypes = {
+ application: React.PropTypes.object.isRequired,
+ designer: React.PropTypes.object.isRequired,
+ lifeline: React.PropTypes.object.isRequired,
+ active: React.PropTypes.bool.isRequired,
+ metamodel: React.PropTypes.object.isRequired,
+ id: React.PropTypes.any.isRequired,
+ index: React.PropTypes.number.isRequired,
+ lifelines: React.PropTypes.object.isRequired,
+ isDragging: React.PropTypes.bool.isRequired,
+ connectDragSource: React.PropTypes.func.isRequired,
+ connectDropTarget: React.PropTypes.func.isRequired,
+};
+
+/** DND. */
+const source = {
+ beginDrag(props) {
+ return {
+ id: props.id,
+ index: props.index,
+ };
+ },
+};
+
+/** DND. */
+const sourceCollect = function collection(connect, monitor) {
+ return {
+ connectDragSource: connect.dragSource(),
+ isDragging: monitor.isDragging(),
+ };
+};
+
+/** DND. */
+const target = {
+ drop(props, monitor, component) {
+ Common.assertNotNull(props);
+ Common.assertNotNull(monitor);
+ const decorated = component.getDecoratedComponentInstance();
+ if (decorated) {
+ const lifelines = decorated.props.lifelines;
+ if (lifelines) {
+ const dragIndex = monitor.getItem().index;
+ const hoverIndex = lifelines.getHoverIndex();
+ lifelines.onDrop(dragIndex, hoverIndex);
+ }
+ }
+ },
+ hover(props, monitor, component) {
+ Common.assertNotNull(props);
+ Common.assertNotNull(monitor);
+ if (component) {
+ const decorated = component.getDecoratedComponentInstance();
+ if (decorated) {
+ const lifelines = decorated.props.lifelines;
+ if (lifelines) {
+ lifelines.setHoverIndex(decorated.props.index);
+ }
+ }
+ }
+ },
+};
+
+/** DND. */
+function targetCollect(connect, monitor) {
+ return {
+ connectDropTarget: connect.dropTarget(),
+ isOver: monitor.isOver(),
+ };
+}
+
+const wrapper1 = DragSource('lifeline', source, sourceCollect)(Lifeline);
+export default DropTarget(['lifeline', 'lifeline-new'], target, targetCollect)(wrapper1);
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/LifelineNew.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/LifelineNew.jsx
new file mode 100644
index 0000000000..a6e9a70703
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/LifelineNew.jsx
@@ -0,0 +1,112 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+import { DragSource } from 'react-dnd';
+
+import Icon from '../../../../../icons/Icon';
+import iconPlus from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/plus.svg';
+import iconHandle from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/handle.svg';
+
+/**
+ * LHS lifeline row view.
+ */
+class LifelineNew extends React.Component {
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Construct view.
+ * @param props element properties.
+ * @param context react context.
+ */
+ constructor(props, context) {
+ super(props, context);
+
+ // Bindings.
+
+ this.onClickAdd = this.onClickAdd.bind(this);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle click event.
+ */
+ onClickAdd() {
+ this.props.designer.addLifeline();
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Render view.
+ * @returns {*}
+ */
+ render() {
+ const { connectDragSource } = this.props;
+ return connectDragSource(
+ <div className="asdcs-designer-lifeline asdcs-designer-lifeline-new">
+ <table className="asdcs-designer-layout asdcs-designer-lifeline-new">
+ <tbody>
+ <tr>
+ <td>
+ <div className="asdcs-designer-sort asdcs-designer-icon">
+ <Icon glyph={iconHandle} />
+ </div>
+ </td>
+ <td>
+ <div className="asdcs-designer-label" onClick={this.onClickAdd}>
+ Add Lifeline
+ </div>
+ </td>
+ <td>
+ <div className="asdcs-designer-icon" onClick={this.onClickAdd}>
+ <Icon glyph={iconPlus} />
+ </div>
+ </td>
+ <td>&nbsp;</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ );
+ }
+}
+
+/** Element properties. */
+LifelineNew.propTypes = {
+ designer: React.PropTypes.object.isRequired,
+ lifelines: React.PropTypes.object.isRequired,
+ connectDragSource: React.PropTypes.func.isRequired,
+};
+
+/** DND. */
+const source = {
+ beginDrag(props) {
+ return { id: props.id };
+ },
+};
+
+/** DND. */
+const collect = function collection(connect, monitor) {
+ return {
+ connectDragSource: connect.dragSource(),
+ isDragging: monitor.isDragging(),
+ };
+};
+
+export default DragSource('lifeline-new', source, collect)(LifelineNew);
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/Lifelines.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/Lifelines.jsx
new file mode 100644
index 0000000000..2e2f2ee7fd
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/lifeline/Lifelines.jsx
@@ -0,0 +1,136 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+
+import Common from '../../../../../../common/Common';
+
+import Lifeline from './Lifeline';
+import LifelineNew from './LifelineNew';
+
+/**
+ * Lifeline container, facilitating DND.
+ * @param props lifeline element properties.
+ * @returns {*}
+ * @constructor
+ */
+export default class Lifelines extends React.Component {
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Construct view.
+ * @param props element properties.
+ * @param context react context.
+ */
+ constructor(props, context) {
+ super(props, context);
+ this.setHoverIndex = this.setHoverIndex.bind(this);
+ this.getHoverIndex = this.getHoverIndex.bind(this);
+ this.onDrop = this.onDrop.bind(this);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Record last hover index as non-state.
+ * @param index index.
+ */
+ setHoverIndex(index) {
+ this.hoverIndex = index;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Get last recorded hover index.
+ * @returns {*}
+ */
+ getHoverIndex() {
+ return this.hoverIndex;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle drop.
+ * @param dragIndex dragged item index; undefined if new.
+ * @param hoverIndex drop index.
+ */
+ onDrop(dragIndex, hoverIndex) {
+ if (hoverIndex >= 0) {
+ const application = this.props.application;
+ const model = application.getModel();
+ if (Common.isNumber(dragIndex)) {
+ if (dragIndex !== hoverIndex) {
+ model.reorderLifelines(dragIndex, hoverIndex);
+ }
+ } else {
+ model.addLifeline(hoverIndex);
+ }
+ this.forceUpdate();
+ application.renderDiagram();
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Render view.
+ * @returns {XML}
+ */
+ render() {
+ const model = this.props.application.getModel();
+ const metamodel = model.getMetamodel();
+ const diagram = model.unwrap().diagram;
+
+ const lifelines = [];
+ for (const lifeline of diagram.lifelines) {
+ lifelines.push(<Lifeline
+ key={`l${lifeline.id}`}
+ application={this.props.application}
+ designer={this.props.designer}
+ lifeline={lifeline}
+ active={this.props.activeLifelineId === lifeline.id}
+ id={lifeline.id}
+ metamodel={metamodel}
+ lifelines={this}
+ index={lifelines.length}
+ />);
+ }
+
+ lifelines.push(<LifelineNew
+ key="_l"
+ designer={this.props.designer}
+ lifelines={this}
+ />);
+
+ return (
+ <div className="asdcs-designer-lifelines">
+ {lifelines}
+ </div>
+ );
+ }
+}
+
+/**
+ * Declare properties.
+ */
+Lifelines.propTypes = {
+ application: React.PropTypes.object.isRequired,
+ designer: React.PropTypes.object.isRequired,
+ activeLifelineId: React.PropTypes.string,
+};
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/Message.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/Message.jsx
new file mode 100644
index 0000000000..95bff702da
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/Message.jsx
@@ -0,0 +1,587 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+import Select from 'react-select';
+import { DragSource, DropTarget } from 'react-dnd';
+
+import Common from '../../../../../../common/Common';
+
+import Icon from '../../../../../icons/Icon';
+import iconDelete from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/delete.svg';
+import iconHandle from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/handle.svg';
+import iconNotes from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/notes.svg';
+import iconSettings from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/settings.svg';
+import iconRequestSync from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/arrow/request-sync.svg';
+import iconRequestAsync from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/arrow/request-async.svg';
+import iconResponse from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/arrow/response.svg';
+
+/**
+ * LHS message row view.
+ */
+class Message extends React.Component {
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Construct view.
+ * @param props element properties.
+ * @param context react context.
+ */
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ active: false,
+ name: props.message.name || '',
+ };
+
+ this.combinedOptions = [{
+ value: 'REQUEST_SYNC',
+ }, {
+ value: 'REQUEST_ASYNC',
+ }, {
+ value: 'RESPONSE',
+ }];
+
+ // Bindings.
+
+ this.onChangeName = this.onChangeName.bind(this);
+ this.onBlurName = this.onBlurName.bind(this);
+ this.onChangeType = this.onChangeType.bind(this);
+ this.onChangeFrom = this.onChangeFrom.bind(this);
+ this.onChangeTo = this.onChangeTo.bind(this);
+ this.onClickDelete = this.onClickDelete.bind(this);
+ this.onClickActions = this.onClickActions.bind(this);
+ this.onClickNotes = this.onClickNotes.bind(this);
+ this.onMouseEnter = this.onMouseEnter.bind(this);
+ this.onMouseLeave = this.onMouseLeave.bind(this);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle name change.
+ * @param event change event.
+ */
+ onChangeName(event) {
+ this.setState({ name: event.target.value });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle name change.
+ * @param event change event.
+ */
+ onBlurName(event) {
+ const options = this.props.application.getOptions();
+ const sanitized = Common.sanitizeText(event.target.value, options, 'message');
+ const props = {
+ id: this.props.message.id,
+ name: sanitized,
+ };
+ this.props.designer.updateMessage(props);
+ this.setState({ name: sanitized });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle delete.
+ */
+ onClickDelete() {
+ this.props.designer.deleteMessage(this.props.message.id);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle menu click.
+ */
+ onClickActions(event) {
+ this.props.designer.showActions(this.props.message.id, { x: event.pageX, y: event.pageY });
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle menu click.
+ */
+ onClickNotes() {
+ this.props.designer.showNotes(this.props.message.id);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle selection.
+ * @param value selection.
+ */
+ onChangeFrom(value) {
+ if (value.target) {
+ this.updateMessage({ from: value.target.value });
+ } else {
+ this.updateMessage({ from: value.value });
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle selection.
+ * @param value selection.
+ */
+ onChangeTo(value) {
+ if (value.target) {
+ this.updateMessage({ to: value.target.value });
+ } else {
+ this.updateMessage({ to: value.value });
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle selection.
+ * @param selected selection.
+ */
+ onChangeType(selected) {
+
+ const value = selected.target ? selected.target.value : selected.value;
+ const props = {};
+ if (value.indexOf('RESPONSE') !== -1) {
+ props.type = 'response';
+ props.asynchronous = false;
+ } else {
+ props.type = 'request';
+ props.asynchronous = (value.indexOf('ASYNC') !== -1);
+ }
+
+ this.updateMessage(props);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle mouse event.
+ */
+ onMouseEnter() {
+ this.setState({ active: true });
+ this.props.designer.onMouseEnterMessage(this.props.message.id);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle mouse event.
+ */
+ onMouseLeave() {
+ this.setState({ active: false });
+ this.props.designer.onMouseLeaveMessage(this.props.message.id);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Update message properties.
+ * @param props properties updates.
+ */
+ updateMessage(props) {
+ const update = {
+ id: this.props.message.id,
+ };
+ for (const k of Object.keys(props)) {
+ update[k] = props[k];
+ }
+ this.props.designer.updateMessage(update);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Render icon.
+ * @param option selection.
+ * @returns {XML}
+ */
+ renderOption(option) {
+ if (option.value === 'RESPONSE') {
+ return <Icon glyph={iconResponse} />;
+ }
+ if (option.value === 'REQUEST_ASYNC') {
+ return <Icon glyph={iconRequestAsync} />;
+ }
+ return <Icon glyph={iconRequestSync} />;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Get request/response and asynchronous combined constant.
+ * @param message message whose properties define spec.
+ * @returns {*}
+ */
+ getMessageSpec(message) {
+ if (message.type === 'response') {
+ return 'RESPONSE';
+ }
+ if (message.asynchronous) {
+ return 'REQUEST_ASYNC';
+ }
+ return 'REQUEST_SYNC';
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * @returns {*}
+ * @private
+ */
+ renderHTMLSelect() {
+
+ const message = this.props.message;
+ const from = this.props.from;
+ const to = Common.assertNotNull(this.props.to);
+ const messageNotesActiveClass = message.notes && message.notes.length > 0 ? 'asdcs-active' : '';
+ const combinedValue = this.getMessageSpec(message);
+
+ const lifelineOptions = [];
+ for (const lifeline of this.props.model.unwrap().diagram.lifelines) {
+ lifelineOptions.push(<option
+ key={lifeline.id}
+ value={lifeline.id}
+ >
+ {lifeline.name}
+ </option>);
+ }
+
+ const activeClass = (this.state.active || this.props.active) ? 'asdcs-active' : '';
+ const { connectDragSource, connectDropTarget } = this.props;
+ return connectDragSource(connectDropTarget(
+ <div
+ className={`asdcs-designer-message ${activeClass}`}
+ data-id={message.id}
+ onMouseEnter={this.onMouseEnter}
+ onMouseLeave={this.onMouseLeave}
+ >
+
+ <table className="asdcs-designer-layout asdcs-designer-message-row1">
+ <tbody>
+ <tr>
+ <td>
+ <div className="asdcs-designer-sort asdcs-designer-icon">
+ <Icon glyph={iconHandle} />
+ </div>
+ </td>
+ <td>
+ <div className="asdcs-designer-message-index">{message.index}.</div>
+ </td>
+ <td>
+ <div className="asdcs-designer-message-name">
+ <input
+ type="text"
+ className="asdcs-editable"
+ value={this.state.name}
+ placeholder="Unnamed"
+ onBlur={this.onBlurName}
+ onChange={this.onChangeName}
+ />
+ </div>
+ </td>
+ <td>
+ <div className="asdcs-designer-actions">
+ <div
+ className="asdcs-designer-settings asdcs-designer-icon"
+ onClick={this.onClickActions}
+ >
+ <Icon glyph={iconSettings} />
+ </div>
+ <div
+ className={`asdcs-designer-notes asdcs-designer-icon ${messageNotesActiveClass}`}
+ onClick={this.onClickNotes}
+ >
+ <Icon glyph={iconNotes} />
+ </div>
+ <div
+ className="asdcs-designer-delete asdcs-designer-icon"
+ onClick={this.onClickDelete}
+ >
+ <Icon glyph={iconDelete} />
+ </div>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table className="asdcs-designer-layout asdcs-designer-message-row2">
+ <tbody>
+ <tr>
+ <td>
+ <select
+ onChange={this.onChangeFrom}
+ className="asdcs-designer-select-message-from"
+ value={from.id}
+ onChange={this.onChangeFrom}
+ >
+ options={lifelineOptions}
+ </select>
+ </td>
+ <td>
+ <select
+ onChange={this.onChangeFrom}
+ className="asdcs-designer-select-message-type"
+ value={combinedValue}
+ onChange={this.onChangeType}
+ >
+ <option value="REQUEST_SYNC">⇾</option>
+ <option value="REQUEST_ASYNC">→</option>
+ <option value="RESPONSE">⇠</option>
+ </select>
+ </td>
+ <td>
+ <select
+ onChange={this.onChangeFrom}
+ className="asdcs-designer-select-message-to"
+ value={to.id}
+ onChange={this.onChangeTo}
+ >
+ options={lifelineOptions}
+ </select>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ </div>
+ ));
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Render view.
+ * @returns {*}
+ * @private
+ */
+ renderReactSelect() {
+
+ const message = this.props.message;
+ const from = this.props.from;
+ const to = Common.assertNotNull(this.props.to);
+ const messageNotesActiveClass = message.notes && message.notes.length > 0 ? 'asdcs-active' : '';
+ const combinedValue = this.getMessageSpec(message);
+
+ const lifelineOptions = [];
+ for (const lifeline of this.props.model.unwrap().diagram.lifelines) {
+ lifelineOptions.push({
+ value: lifeline.id,
+ label: lifeline.name,
+ });
+ }
+
+ const activeClass = (this.state.active || this.props.active) ? 'asdcs-active' : '';
+ const { connectDragSource, connectDropTarget } = this.props;
+ return connectDragSource(connectDropTarget(
+
+ <div
+ className={`asdcs-designer-message ${activeClass}`}
+ data-id={message.id}
+ onMouseEnter={this.onMouseEnter}
+ onMouseLeave={this.onMouseLeave}
+ >
+
+ <table className="asdcs-designer-layout asdcs-designer-message-row1">
+ <tbody>
+ <tr>
+ <td>
+ <div className="asdcs-designer-sort asdcs-designer-icon">
+ <Icon glyph={iconHandle} />
+ </div>
+ </td>
+ <td>
+ <div className="asdcs-designer-message-index">{message.index}.</div>
+ </td>
+ <td>
+ <div className="asdcs-designer-message-name">
+ <input
+ type="text"
+ className="asdcs-editable"
+ value={this.state.name}
+ placeholder="Unnamed"
+ onBlur={this.onBlurName}
+ onChange={this.onChangeName}
+ />
+ </div>
+ </td>
+ <td>
+ <div className="asdcs-designer-actions">
+ <div
+ className="asdcs-designer-settings asdcs-designer-icon"
+ onClick={this.onClickActions}
+ >
+ <Icon glyph={iconSettings} />
+ </div>
+ <div
+ className={`asdcs-designer-notes asdcs-designer-icon ${messageNotesActiveClass}`}
+ onClick={this.onClickNotes}
+ >
+ <Icon glyph={iconNotes} />
+ </div>
+ <div
+ className="asdcs-designer-delete asdcs-designer-icon"
+ onClick={this.onClickDelete}
+ >
+ <Icon glyph={iconDelete} />
+ </div>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <table className="asdcs-designer-layout asdcs-designer-message-row2">
+ <tbody>
+ <tr>
+ <td>
+ <Select
+ className="asdcs-editable-select asdcs-designer-editable-message-from"
+ openOnFocus
+ clearable={false}
+ searchable={false}
+ value={from.id}
+ onChange={this.onChangeFrom}
+ options={lifelineOptions}
+ />
+ </td>
+ <td>
+ <Select
+ className="asdcs-editable-select asdcs-designer-editable-message-type"
+ openOnFocus
+ clearable={false}
+ searchable={false}
+ value={combinedValue}
+ onChange={this.onChangeType}
+ options={this.combinedOptions}
+ optionRenderer={this.renderOption}
+ valueRenderer={this.renderOption}
+ />
+ </td>
+ <td>
+ <Select
+ className="asdcs-editable-select asdcs-designer-editable-message-to"
+ openOnFocus
+ clearable={false}
+ searchable={false}
+ value={to.id}
+ onChange={this.onChangeTo}
+ options={lifelineOptions}
+ />
+ </td>
+
+ </tr>
+ </tbody>
+ </table>
+
+ </div>
+ ));
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ render() {
+ const options = this.props.application.getOptions();
+ if (options.useHtmlSelect) {
+ return this.renderHTMLSelect();
+ }
+ return this.renderReactSelect();
+ }
+}
+
+/**
+ * Declare properties.
+ * @type {{designer: *, message: *, from: *, to: *, model: *, connectDragSource: *}}
+ */
+Message.propTypes = {
+ application: React.PropTypes.object.isRequired,
+ designer: React.PropTypes.object.isRequired,
+ message: React.PropTypes.object.isRequired,
+ active: React.PropTypes.bool.isRequired,
+ from: React.PropTypes.object.isRequired,
+ to: React.PropTypes.object.isRequired,
+ model: React.PropTypes.object.isRequired,
+ index: React.PropTypes.number.isRequired,
+ messages: React.PropTypes.object.isRequired,
+ connectDragSource: React.PropTypes.func.isRequired,
+ connectDropTarget: React.PropTypes.func.isRequired,
+};
+
+/** DND. */
+const source = {
+ beginDrag(props) {
+ return {
+ id: props.id,
+ index: props.index,
+ };
+ },
+};
+
+/** DND. */
+const sourceCollect = function collection(connect, monitor) {
+ return {
+ connectDragSource: connect.dragSource(),
+ isDragging: monitor.isDragging(),
+ };
+};
+
+
+/** DND. */
+const target = {
+ drop(props, monitor, component) {
+ Common.assertNotNull(props);
+ Common.assertNotNull(monitor);
+ const decorated = component.getDecoratedComponentInstance();
+ if (decorated) {
+ const messages = decorated.props.messages;
+ if (messages) {
+ const dragIndex = monitor.getItem().index;
+ const hoverIndex = messages.getHoverIndex();
+ messages.onDrop(dragIndex, hoverIndex);
+ }
+ }
+ },
+ hover(props, monitor, component) {
+ Common.assertNotNull(props);
+ Common.assertNotNull(monitor);
+ if (component) {
+ const decorated = component.getDecoratedComponentInstance();
+ if (decorated) {
+ decorated.props.messages.setHoverIndex(decorated.props.index);
+ }
+ }
+ },
+};
+
+/** DND. */
+function targetCollect(connect, monitor) {
+ return {
+ connectDropTarget: connect.dropTarget(),
+ isOver: monitor.isOver(),
+ };
+}
+
+const wrapper = DragSource('message', source, sourceCollect)(Message);
+export default DropTarget(['message', 'message-new'], target, targetCollect)(wrapper);
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/MessageNew.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/MessageNew.jsx
new file mode 100644
index 0000000000..230cb9fa60
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/MessageNew.jsx
@@ -0,0 +1,106 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+import { DragSource } from 'react-dnd';
+
+import Icon from '../../../../../icons/Icon';
+import iconPlus from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/plus.svg';
+import iconHandle from '../../../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/handle.svg';
+
+/**
+ * LHS lifeline row view.
+ */
+class MessageNew extends React.Component {
+
+ /**
+ * Construct view.
+ * @param props element properties.
+ * @param context react context.
+ */
+ constructor(props, context) {
+ super(props, context);
+ this.onClickAdd = this.onClickAdd.bind(this);
+ }
+
+ /**
+ * Handle add.
+ */
+ onClickAdd() {
+ this.props.designer.addMessage();
+ }
+
+ /**
+ * Render view.
+ * @returns {*}
+ */
+ render() {
+ const { connectDragSource } = this.props;
+ return connectDragSource(
+ <div className="asdcs-designer-message asdcs-designer-message-new">
+ <table className="asdcs-designer-layout asdcs-designer-message-new">
+ <tbody>
+ <tr>
+ <td>
+ <div className="asdcs-designer-sort asdcs-designer-icon">
+ <Icon glyph={iconHandle} />
+ </div>
+ </td>
+ <td>
+ <div className="asdcs-designer-label" onClick={this.onClickAdd}>
+ Add Message
+ </div>
+ </td>
+ <td>
+ <div className="asdcs-designer-icon" onClick={this.onClickAdd}>
+ <Icon glyph={iconPlus} />
+ </div>
+ </td>
+ <td>
+ &nbsp;
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ );
+ }
+}
+
+/** Element properties. */
+MessageNew.propTypes = {
+ designer: React.PropTypes.object.isRequired,
+ messages: React.PropTypes.object.isRequired,
+ connectDragSource: React.PropTypes.func.isRequired,
+};
+
+/** DND. */
+const source = {
+ beginDrag(props) {
+ return { id: props.id };
+ },
+};
+
+/** DND. */
+const collect = function collection(connect, monitor) {
+ return {
+ connectDragSource: connect.dragSource(),
+ isDragging: monitor.isDragging(),
+ };
+};
+
+export default DragSource('message-new', source, collect)(MessageNew);
+
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/Messages.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/Messages.jsx
new file mode 100644
index 0000000000..a305a6bd86
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/message/Messages.jsx
@@ -0,0 +1,143 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+
+import Common from '../../../../../../common/Common';
+
+import Message from './Message';
+import MessageNew from './MessageNew';
+
+/**
+ * Messages container, facilitating DND.
+ * @param props lifeline element properties.
+ * @returns {*}
+ * @constructor
+ */
+export default class Messages extends React.Component {
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Construct view.
+ * @param props element properties.
+ * @param context react context.
+ */
+ constructor(props, context) {
+ super(props, context);
+ this.state = {
+ };
+ this.setHoverIndex = this.setHoverIndex.bind(this);
+ this.getHoverIndex = this.getHoverIndex.bind(this);
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Record last hover index as non-state.
+ * @param index index.
+ */
+ setHoverIndex(index) {
+ this.hoverIndex = index;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Get last recorded hover index.
+ * @returns {*}
+ */
+ getHoverIndex() {
+ return this.hoverIndex;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Handle drop.
+ * @param dragIndex dragged item index; undefined if new.
+ * @param hoverIndex drop index.
+ */
+ onDrop(dragIndex, hoverIndex) {
+ if (hoverIndex >= 0) {
+ const application = this.props.application;
+ const model = application.getModel();
+ if (Common.isNumber(dragIndex)) {
+ if (dragIndex !== hoverIndex) {
+ model.reorderMessages(dragIndex, hoverIndex);
+ }
+ } else {
+ model.addMessage(hoverIndex);
+ }
+ this.forceUpdate();
+ application.renderDiagram();
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Render view.
+ * @returns {*}
+ */
+ render() {
+
+ const model = this.props.application.getModel();
+ const diagram = model.unwrap().diagram;
+
+ // Render existing messages.
+
+ const messages = [];
+ for (const step of diagram.steps) {
+ const message = step.message;
+ const from = model.getLifelineById(message.from);
+ const to = model.getLifelineById(message.to);
+ messages.push(<Message
+ key={`m${message.id}`}
+ application={this.props.application}
+ designer={this.props.designer}
+ message={message}
+ active={this.props.activeMessageId === message.id}
+ from={from}
+ to={to}
+ model={model}
+ index={messages.length}
+ messages={this}
+ />);
+ }
+
+ // Render add.
+
+ messages.push(<MessageNew
+ key="_m"
+ designer={this.props.designer}
+ messages={this}
+ />);
+
+ return (
+ <div className="asdcs-designer-steps">
+ {messages}
+ </div>
+ );
+ }
+}
+
+/** Element properties. */
+Messages.propTypes = {
+ application: React.PropTypes.object.isRequired,
+ designer: React.PropTypes.object.isRequired,
+ activeMessageId: React.PropTypes.string,
+};
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/metadata/Metadata.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/metadata/Metadata.jsx
new file mode 100644
index 0000000000..a17a197ca0
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/designer/components/metadata/Metadata.jsx
@@ -0,0 +1,34 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+
+/**
+ * Metadata view.
+ */
+const Metadata = function Metadata(props) {
+ return (
+ <div className="asdcs-designer-metadata">
+ {props.metadata.name}
+ </div>
+ );
+};
+
+Metadata.propTypes = {
+ metadata: React.PropTypes.object.isRequired,
+};
+
+export default Metadata;
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/source/Source.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/source/Source.jsx
new file mode 100644
index 0000000000..3d13d830da
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/source/Source.jsx
@@ -0,0 +1,86 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+
+/**
+ * Editor view, aggregating the designer, the code editor, the toolbar.
+ */
+export default class Source extends React.Component {
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Construct view.
+ */
+ constructor(props, context) {
+ super(props, context);
+ this.demo = this.props.application.getOptions().demo;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Set JSON mode.
+ * @param json JSON (stringified) code.
+ */
+ setJSON(json = '') {
+ if (this.textarea) {
+ this.textarea.value = json;
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Set YAML mode.
+ * @param yaml YAML code.
+ */
+ setYAML(yaml = '') {
+ if (this.textarea) {
+ this.textarea.value = yaml;
+ }
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ componentDidMount() {
+ /*
+ this.cm = CodeMirror.fromTextArea(this.textarea, {
+ lineNumbers: true,
+ readOnly: true,
+ });
+ */
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Render to DOM.
+ */
+ render() {
+ return (
+ <div className="asdcs-editor-code">
+ <textarea ref={(r) => { this.textarea = r; }}></textarea>
+ </div>
+ );
+ }
+}
+
+Source.propTypes = {
+ application: React.PropTypes.object.isRequired,
+};
+
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/toolbar/Toolbar.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/toolbar/Toolbar.jsx
new file mode 100644
index 0000000000..dd75180b2a
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/editor/components/toolbar/Toolbar.jsx
@@ -0,0 +1,275 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+
+import Common from '../../../../common/Common';
+
+import iconPlus from '../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/plus.svg';
+import iconOpen from '../../../../../../../../res/ecomp/asdc/sequencer/sprites/icon/open.svg';
+
+/**
+ * Toolbar view. Buttons offered in the toolbar depend on the mode. Unless in demo mode,
+ * all you get are the buttons for toggling between JSON/YAML/Designer.
+ */
+export default class Toolbar extends React.Component {
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Construct view.
+ */
+ constructor(props, context) {
+ super(props, context);
+ this.application = Common.assertType(this.props.application, 'Object');
+ this.editor = Common.assertType(this.props.editor, 'Object');
+ this.mode = 'design';
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Set editor mode, one of {design, json, yaml}.
+ * @param mode
+ */
+ setMode(mode = 'design') {
+ this.mode = mode;
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Render into the DOM.
+ */
+ render() {
+
+ const demo = this.application.getOptions().demo;
+ const demoCss = demo ? '' : 'asdc-hide';
+
+ return (
+ <div className={`asdcs-editor-toolbar ${demoCss}`}>
+ <div className="asdcs-editor-toolbar-demo">
+ <button className="asdcs-button-new" data-title="New sequence">
+ <svg>
+ <use xlinkHref={iconPlus} className="asdcs-icon" />
+ </svg>
+ </button>
+ <button className="asdcs-button-open" data-title="Open sequence">
+ <svg>
+ <use xlinkHref={iconOpen} className="asdcs-icon" />
+ </svg>
+ </button>
+ <button className="asdcs-button-save" data-title="Save checkpoint">
+ <svg>
+ <use xlinkHref="#icon--save" className="asdcs-icon" />
+ </svg>
+ </button>
+ <button className="asdcs-button-validate" data-title="Validate">
+ <svg>
+ <use xlinkHref="#icon--validate" className="asdcs-icon" />
+ </svg>
+ </button>
+ <button className="asdcs-button-download" data-title="Download">
+ <svg>
+ <use xlinkHref="#icon--download" className="asdcs-icon" />
+ </svg>
+ </button>
+ <button className="asdcs-button-upload" data-title="Upload">
+ <svg>
+ <use xlinkHref="#icon--upload" className="asdcs-icon" />
+ </svg>
+ </button>
+ </div>
+ <div className="asdcs-editor-toolbar-toggle">
+ <button className="asdcs-button-design asdcs-button-mode asdcs-button-toggle-left">
+ Design
+ </button>
+ <button className="asdcs-button-json asdcs-button-mode asdcs-button-toggle-center">
+ JSON
+ </button>
+ <button className="asdcs-button-yaml asdcs-button-mode asdcs-button-toggle-right">
+ YAML
+ </button>
+ </div>
+ </div>
+ );
+ }
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Initialize eventhandlers.
+ * @private
+ *
+ _initEvents() {
+
+ $('button.asdcs-button-open', this.$el).click(() => {
+ this._doDemoOpen();
+ });
+
+ $('button.asdcs-button-new', this.$el).click(() => {
+ this._doDemoNew();
+ });
+
+ $('button.asdcs-button-save', this.$el).click(() => {
+ this._doDemoSave();
+ });
+
+ $('button.asdcs-button-upload', this.$el).click(() => {
+ this._doDemoUpload();
+ });
+
+ $('button.asdcs-button-download', this.$el).click(() => {
+ this._doDemoDownload();
+ });
+
+ $('button.asdcs-button-validate', this.$el).click(() => {
+ this._doDemoValidate();
+ });
+
+ $('button.asdcs-button-json', this.$el).click((e) => {
+ if ($(e.target).hasClass('asdcs-active')) {
+ return;
+ }
+ this.editor.toggleToJSON();
+ });
+
+ $('button.asdcs-button-yaml', this.$el).click((e) => {
+ if ($(e.target).hasClass('asdcs-active')) {
+ return;
+ }
+ this.editor.toggleToYAML();
+ });
+
+ $('button.asdcs-button-design', this.$el).click((e) => {
+ if ($(e.target).hasClass('asdcs-active')) {
+ return;
+ }
+ this.editor.toggleToDesign();
+ });
+ }
+ */
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Demo action.
+ *
+ _doDemoOpen() {
+ const complete = function complete() {
+ const sequencer = this.application.getSequencer();
+ const scenarios = sequencer.getDemoScenarios();
+ sequencer.setModel(scenarios.getECOMP());
+ };
+ this.application.showConfirmDialog('[DEMO MODE] Open a canned DEMO sequence ' +
+ 'via the public #setModel() API?', complete);
+
+ }
+ */
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Demo action.
+ *
+ _doDemoNew() {
+ const complete = function complete() {
+ const sequencer = this.application.getSequencer();
+ sequencer.newModel();
+ };
+ this.application.showConfirmDialog('[DEMO MODE] Create an empty sequence via the ' +
+ 'public #newModel() API?', complete);
+ }
+ */
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Demo action.
+ *
+ _doDemoSave() {
+ const sequencer = this.application.getSequencer();
+ Logger.info(`[DEMO MODE] model:\n${JSON.stringify(sequencer.getModel(), null, 4)}`);
+ this.application.showInfoDialog('[DEMO MODE] Retrieved model via the public #getModel ' +
+ 'API and logged its JSON to the console.');
+ }
+ */
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Demo action.
+ *
+ _doDemoUpload() {
+ const sequencer = this.application.getSequencer();
+ const svg = sequencer.getSVG();
+ // console.log(`[DEMO MODE] SVG:\n${svg}`);
+ const $control = this.$el.closest('.asdcs-control');
+ Logger.info(`parent: ${$control.length}`);
+ const $form = $('form.asdcs-export', $control);
+ Logger.info(`form: ${$form.length}`);
+ $('input[name=svg]', $form).val(svg);
+ try {
+ $form.submit();
+ } catch (e) {
+ Logger.error(e);
+ this.application.showErrorDialog('[DEMO MODE] Export service not available. Retrieved ' +
+ 'SVG via the public #getSVG API and dumped it to the console.');
+ }
+ }
+ */
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Demo action.
+ *
+ _doDemoDownload() {
+ const json = JSON.stringify(this.application.getSequencer().getModel());
+ const $control = this.$el.closest('.asdcs-control');
+ const $a = $('<a download="model.json" style="display:none">').appendTo($control);
+ $a.attr('href', `data:application/json;charset=utf-8,${encodeURIComponent(json)}`);
+ $a[0].click();
+ $a.remove();
+ }
+ */
+
+ // ///////////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ * Demo action.
+ *
+ _doDemoValidate() {
+ this.application.showInfoDialog('[DEMO MODE] Dumping validation result to the console.');
+ const errors = this.application.getModel().validate();
+ Logger.info(`[DEMO MODE] Validation: ${JSON.stringify(errors, null, 4)}`);
+ }
+ */
+}
+
+Toolbar.propTypes = {
+ application: React.PropTypes.object.isRequired,
+ editor: React.PropTypes.object.isRequired,
+};
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/export/Export.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/export/Export.jsx
new file mode 100644
index 0000000000..529ae92ded
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/export/Export.jsx
@@ -0,0 +1,31 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+
+const Export = function Export() {
+ return (
+ <form className="asdcs-export" action="/ossui-svg/services/ossui/svg/export" method="post">
+ <input name="svg" type="hidden" value="" />
+ <input name="css" type="hidden" value="sdc/sequencer/default" />
+ <input name="type" type="hidden" value="PDF" />
+ <input name="height" type="hidden" value="1920" />
+ <input name="width" type="hidden" value="1080" />
+ </form>
+ );
+};
+
+export default Export;
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/icons/Icon.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/icons/Icon.jsx
new file mode 100644
index 0000000000..6bc04f997f
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/icons/Icon.jsx
@@ -0,0 +1,41 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+
+/**
+ * Simple icon view.
+ * @param glyph glyph definition, from import.
+ * @param className optional classname, for svg element.
+ * @returns {XML}
+ * @constructor
+ */
+const Icon = function Icon({ glyph, className }) {
+ return (
+ <svg viewBox="0 0 1000 1000" className={className} >
+ <use xlinkHref={glyph} className="asdcs-icon" />
+ </svg>
+ );
+};
+
+/** Declare properties. */
+Icon.propTypes = {
+ className: React.PropTypes.string,
+ glyph: React.PropTypes.string.isRequired,
+};
+
+export default Icon;
+
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/overlay/Overlay.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/overlay/Overlay.jsx
new file mode 100644
index 0000000000..817f4f1697
--- /dev/null
+++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/components/overlay/Overlay.jsx
@@ -0,0 +1,61 @@
+/*!
+ * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import React from 'react';
+
+/**
+ * Overlay view.
+ */
+export default class Overlay extends React.Component {
+
+ /**
+ * Construct view.
+ * @param props element properties.
+ * @param context react context.
+ */
+ constructor(props, context) {
+ super(props, context);
+ this.state = {
+ visible: false,
+ };
+ this.setVisible = this.setVisible.bind(this);
+ }
+
+ /**
+ * Set visibility.
+ * @param visible true if visible.
+ */
+ setVisible(visible) {
+ this.setState({
+ visible,
+ });
+ }
+
+ /**
+ * Render view.
+ * @returns {XML}
+ */
+ render() {
+ const display = this.state.visible ? 'block' : 'none';
+ return (
+ <div
+ className="asdcs-overlay"
+ style={{ display }}
+ >
+ </div>
+ );
+ }
+}