diff options
Diffstat (limited to 'dox-sequence-diagram-ui/src/main/webapp/lib/ecomp')
45 files changed, 8282 insertions, 0 deletions
diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/Sequencer.jsx b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/Sequencer.jsx new file mode 100644 index 0000000000..ff8e9a22ca --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/Sequencer.jsx @@ -0,0 +1,199 @@ +/*! + * 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 Application from './components/application/Application'; +import Common from './common/Common'; +import Options from './common/Options'; +import Model from './model/Model'; +import Metamodel from './model/Metamodel'; +import Metamodels from './model/Metamodels'; +import Scenarios from './model/demo/scenarios/Scenarios'; +import '../../../../res/sdc-sequencer.scss'; +/** + * ASDC Sequencer entry point. + */ +export default class Sequencer extends React.Component { + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + constructor(props, context) { + super(props, context); + + + this.setMetamodel.bind(this); + this.setModel.bind(this); + this.getModel.bind(this); + this.getMetamodel.bind(this); + this.getSVG.bind(this); + this.getDemoScenarios.bind(this); + this.newModel.bind(this); + + // Parse options. + + this.options = new Options(props.options); + + // Default scenarios. + + const scenarios = this.getDemoScenarios(); + this.setMetamodel(scenarios.getMetamodels()); + + // this.setModel(scenarios.getBlank()); + this.setModel(scenarios.getDimensions()); + // this.setModel(scenarios.getECOMP()); + + } + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Optionally save known metamodels so that subsequent loading and unloading + * of models needn't include the corresponding metamodel. + * @param metamodels array of conformant metamodel JSON definitions. + * @return this. + */ + setMetamodel(metamodels) { + Common.assertType(metamodels, 'Array'); + this.metamodels = new Metamodels(metamodels); + return this; + } + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set current diagram. + * @param modelJSON JSON diagram spec. + * @param metamodelIdOrDefinition optional metamodel definition or reference. Defaults to + * the model's metadata @ref, or the default (permissive) metamodel. + * @return this. + */ + setModel(modelJSON, metamodelIdOrDefinition) { + Common.assertType(modelJSON, 'Object'); + const ref = (modelJSON.metadata) ? modelJSON.metadata.ref : undefined; + const metamodel = this.getMetamodel(metamodelIdOrDefinition || ref); + Common.assertInstanceOf(metamodel, Metamodel); + this.model = new Model(modelJSON, metamodel); + if (this.application) { + this.application.setModel(this.model); + } + return this; + } + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get current diagram state. At any given instant the diagram might not make *sense* + * but it should always be syntactically valid. + * @return current Model. + */ + getModel() { + + if (this.application) { + const model = this.application.getModel(); + if (model) { + return model.unwrap(); + } + } + + return this.model; + } + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Extract SVG element. + * @return stringified SVG element. + */ + getSVG() { + return this.application.getSVG(); + } + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get demo scenarios, allowing initialization in demo mode from the outside. + * @returns {Scenarios} + */ + getDemoScenarios() { + return new Scenarios(); + } + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Create new model. + * @param metamodelIdOrDefinition + * @return newly-created model. + */ + newModel(metamodelIdOrDefinition) { + const metamodel = this.getMetamodel(metamodelIdOrDefinition); + Common.assertInstanceOf(metamodel, Metamodel); + const model = new Model({}, metamodel); + if (this.application) { + this.application.setModel(model); + } + return model; + } + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get Metamodel instance corresponding to an ID or JSON definition. + * @param metamodelIdOrDefinition String ID or JSON definition. + * @returns Metamodel instance. + * @private + */ + getMetamodel(metamodelIdOrDefinition) { + const metamodelType = Common.getType(metamodelIdOrDefinition); + if (metamodelType === 'Object') { + return new Metamodel(metamodelIdOrDefinition); + } + return this.metamodels.getMetamodelOrDefault(metamodelIdOrDefinition); + } + + // ////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Render current diagram state. + */ + render() { + + if (this.props.model) { + + // If a model was specified as a property, apply it. Otherwise + // fall back to the demo model. + + const scenarios = this.getDemoScenarios(); + const metamodel = [scenarios.getBlankMetamodel(), scenarios.getECOMPMetamodel()]; + if (this.props.metamodel) { + metamodel.push(this.props.metamodel); + } + this.setMetamodel(metamodel); + this.setModel(this.props.model); + } + + return ( + <Application options={this.options} sequencer={this} ref={(a) => { this.application = a; }} /> + ); + } + +} + +Sequencer.propTypes = { + options: React.PropTypes.object.isRequired, + model: React.PropTypes.object, + metamodel: React.PropTypes.object, +}; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Common.js b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Common.js new file mode 100644 index 0000000000..7337367dca --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Common.js @@ -0,0 +1,356 @@ +/*! + * 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. + */ + +/** + * Common operations. + */ +export default class Common { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Retrieve and start a simple timer. Retrieve elapsed time by calling #ms(). + * @returns {*} + */ + static timer() { + const start = new Date().getTime(); + return { + ms() { + return (new Date().getTime() - start); + }, + }; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get datatype, stripping '[object Boolean]' to just 'Boolean'. + * @param o JS object. + * @return String like String, Number, Date, Null, Undefined, stuff like that. + */ + static getType(o) { + const str = Object.prototype.toString.call(o); + const prefix = '[object '; + if (str.substr(str, prefix.length) === prefix) { + return str.substr(prefix.length, str.length - (prefix.length + 1)); + } + return str; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Assert that an argument was provided. + * @param value to be checked. + * @param message message on assertion failure. + * @return value. + */ + static assertNotNull(value, message = 'Unexpected null value') { + if (!value) { + throw new Error(message); + } + return value; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Assert argument type. + * @param value to be checked. + * @param expected expected type string, e,g. Number from [object Number]. + * @return value. + */ + static assertType(value, expected) { + const type = this.getType(value); + if (type !== expected) { + throw new Error(`Expected type ${expected}, got ${type}`); + } + return value; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Assert argument type. + * @param value to be checked. + * @param unexpected unexpected type string, e,g. Number from [object Number]. + * @return value. + */ + static assertNotType(value, unexpected) { + const type = this.getType(value); + if (type === unexpected) { + throw new Error(`Forbidden type "${unexpected}"`); + } + return value; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Assert argument is a simple JSON object, and specifically not (something like an) ES6 class. + * @param value to be checked. + * @return value. + */ + static assertPlainObject(value) { + Common.assertType(value, 'Object'); + // TODO + /* + if (!($.isPlainObject(value))) { + throw new Error(`Expected plain object: ${value}`); + } + */ + return value; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Assert argument type. + * @param value to be checked. + * @param c expected class. + * @return value. + */ + static assertInstanceOf(value, c) { + Common.assertNotNull(value); + if (!(value instanceof c)) { + throw new Error(`Expected instanceof ${c}: ${value}`); + } + return value; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Assert that a string matches a regex. + * @param value value to be tested. + * @param re pattern to be applied. + * @return value. + */ + static assertMatches(value, re) { + this.assertType(value, 'String'); + this.assertType(re, 'RegExp'); + if (!re.test(value)) { + throw new Error(`Value ${value} doesn't match pattern ${re}`); + } + return value; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Assert the value of a boolean. + * + * @param bool to be checked. + * @param message optional message on assertion failure. + * @return value. + */ + static assertThat(bool, message) { + if (!bool) { + throw new Error(message || `Unexpected: ${bool}`); + } + return bool; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Verify that a value, generally a function arg, is a DOM element. + * @param value to be checked. + * @return value. + */ + static assertHTMLElement(value) { + if (!Common.isHTMLElement(value)) { + throw new Error(`Expected HTMLElement: ${value}`); + } + return value; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Check whether a value, generally a function arg, is an HTML DOM element. + * @param o to be checked. + * @return true if DOM element. + */ + static isHTMLElement(o) { + if (typeof HTMLElement === 'object') { + return o instanceof HTMLElement; + } + return o && typeof o === 'object' && o !== null + && o.nodeType === 1 && typeof o.nodeName === 'string'; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Check if a string is non-empty. + * @param s string to be checked. + * @returns false if non-blank string, true otherwise. + */ + static isBlank(s) { + if (Common.getType(s) === 'String') { + return (s.trim().length === 0); + } + return true; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Detect dates that are numbers, milli/seconds since epoch.. + * + * @param n candidate number. + * @returns {boolean} + */ + static isNumber(n) { + return !isNaN(parseFloat(n)) && isFinite(n); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Parse the text output from a template to a DOM element. + * @param txt input text. + * @returns {Element} + */ + static txt2dom(txt) { + return new DOMParser().parseFromString(txt, 'image/svg+xml').documentElement; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Recursively convert a DOM element to an SVG (namespaced) element. Otherwise + * you get HTML elements that *happen* to have SVG names, but which aren't actually SVG. + * + * @param node DOM node to be converted. + * @param svg to be updated. + * @returns {*} for chaining. + */ + static dom2svg(node, svg) { + + Common.assertNotType(node, 'String'); + + if (node.childNodes && node.childNodes.length > 0) { + + for (const c of node.childNodes) { + switch (c.nodeType) { + case document.TEXT_NODE: + svg.text(c.nodeValue); + break; + default: + break; + } + } + + for (const c of node.childNodes) { + switch (c.nodeType) { + case document.ELEMENT_NODE: + Common.dom2svg(c, svg.append(`svg:${c.nodeName.toLowerCase()}`)); + break; + default: + break; + } + } + } + + if (node.hasAttributes()) { + for (let i = 0; i < node.attributes.length; i++) { + const a = node.attributes.item(i); + svg.attr(a.name, a.value); + } + } + + return svg; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get the lines to be shown in the label. + * + * @param labelText original label text. + * @param wordWrapAt chars at which to break words. + * @param lineWrapAt chars at which to wrap. + * @param maximumLines lines at which to truncate. + * @returns {Array} + */ + static tokenize(labelText = '', wordWrapAt, lineWrapAt, maximumLines) { + + let l = labelText; + + // Hyphenate and break long words. + + const regex = new RegExp(`(\\w{${wordWrapAt - 1}})(?=\\w)`, 'g'); + l = l.replace(regex, '$1- '); + + const labelTokens = l.split(/\s+/); + const lines = []; + let label = ''; + for (const labelToken of labelTokens) { + if (label.length > 0) { + const length = label.length + labelToken.length + 1; + if (length > lineWrapAt) { + lines.push(label.trim()); + label = labelToken; + continue; + } + } + label = `${label} ${labelToken}`; + } + + if (label) { + lines.push(label.trim()); + } + + const truncated = lines.slice(0, maximumLines); + if (truncated.length < lines.length) { + let finalLine = truncated[maximumLines - 1]; + if (finalLine.length > (lineWrapAt - 4)) { + finalLine = finalLine.substring(0, lineWrapAt - 4); + } + finalLine = `${finalLine} ...`; + truncated[maximumLines - 1] = finalLine; + } + + return truncated; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Brutally sanitize an input string. We have no syntax rules, and hence no specific + * rules to apply, but we have very few unconstrained fields, so we can implement a + * crude default and devolve the rest to options. + * @param value value to be sanitized. + * @param options control options including validation rules. + * @param type validation type. + * @returns {*} sanitized string. + * @private + */ + static sanitizeText(value, options, type) { + const rules = Common.assertNotNull(options.validation[type]); + let v = value || rules.defaultValue || ''; + if (rules.replace) { + v = v.replace(rules.replace, ''); + } + if (v.length > rules.maxLength) { + v = `${v.substring(0, rules.maxLength)}...`; + } + return v; + } + +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Logger.js b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Logger.js new file mode 100644 index 0000000000..187f49bb08 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Logger.js @@ -0,0 +1,137 @@ +/*! + * 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. + */ + +/* eslint-disable no-console */ + +import Common from './Common'; + +/** + * Logger, to allow calls to console.log during development, but + * disable them for production. + */ +export default class Logger { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * No-op call so that we can leave imports in place, + * even when there's no debugging. + */ + static noop() { + // Nothing. + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Set debug level. + * @param level threshold. + */ + static setLevel(level) { + this.level = Logger.OFF; + if (Common.getType(level) === 'Number') { + this.level = level; + } else { + this.level = Logger[level]; + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get debug level. + * @returns {number|*} + */ + static getLevel() { + return this.level; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Write DEBUG-level log. + * @param msg message or tokens. + */ + static debug(...msg) { + if (this.level >= Logger.DEBUG) { + const out = this.serialize(msg); + console.info(`ASDCS [DEBUG] ${out}`); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Write INFO-level log. + * @param msg message or tokens. + */ + static info(...msg) { + if (this.level >= Logger.INFO) { + const out = this.serialize(msg); + console.info(`ASDCS [INFO] ${out}`); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Write debug. + * @param msg message or tokens. + */ + static warn(msg) { + if (this.level >= Logger.WARN) { + const out = this.serialize(msg); + console.warn(`ASDCS [WARN] ${out}`); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Write error. + * @param msg message or tokens. + */ + static error(...msg) { + if (this.level >= Logger.ERROR) { + const out = this.serialize(msg); + console.error(`ASDCS [ERROR] ${out}`); + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Serialize msg. + * @param msg message or tokens. + * @returns {string} + */ + static serialize(...msg) { + let out = ''; + msg.forEach((token) => { + out = `${out}${token}`; + }); + return out; + } +} + +// ///////////////////////////////////////////////////////////////////////////////////////////////// + +Logger.OFF = 0; +Logger.ERROR = 1; +Logger.WARN = 2; +Logger.INFO = 3; +Logger.DEBUG = 4; +Logger.level = Logger.OFF; diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Options.js b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Options.js new file mode 100644 index 0000000000..15897d7ee3 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/common/Options.js @@ -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 _merge from 'lodash/merge'; + +import Logger from './Logger'; + +/** + * A wrapper for an options object. User-supplied options are merged with defaults, + * and the result -- runtime options -- are available by calling #getOptions(). + */ +export default class Options { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct options, applying defaults. + * @param options optional override options. + */ + constructor(options = {}) { + this.options = _merge({}, Options.DEFAULTS, options); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Unwrap options. + * @returns {*} + */ + unwrap() { + return this.options; + } +} + +// ///////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Default options, overridden by anything of the same name. + */ +Options.DEFAULTS = { + log: { + level: Logger.WARN, + }, + demo: false, + useHtmlSelect: true, + diagram: { + svg: { + x: 0, + y: 0, + width: 1600, + height: 1200, + margin: 50, + floodColor: '#009fdb', + scale: { + height: true, + width: true, + minimum: 0.25, + }, + }, + title: { + height: 0, + }, + metadata: false, + lifelines: { + header: { + height: 225, + width: 350, + wrapWords: 14, + wrapLines: 18, + maxLines: 5, + }, + occurrences: { + marginTop: 50, + marginBottom: 75, + foreshortening: 5, + width: 50, + }, + spacing: { + horizontal: 400, + vertical: 400, + }, + }, + messages: { + label: { + wrapWords: 14, + wrapLines: 18, + maxLines: 4, + }, + }, + fragments: { + leftMargin: 150, + topMargin: 200, + widthMargin: 300, + heightMargin: 350, + label: { + wrapWords: 50, + wrapLines: 50, + maxLines: 2, + }, + }, + }, + validation: { + lifeline: { + maxLength: 100, + defaultValue: '', + replace: /[^\-\.\+ &%#@\?\(\)\[\]<>\w\d]/g, + }, + message: { + maxLength: 100, + defaultValue: '', + replace: /[^\-\.\+ &%#@\?\(\)\[\]<>\w\d]/g, + }, + notes: { + maxLength: 255, + defaultValue: '', + }, + guard: { + maxLength: 80, + defaultValue: '', + replace: /[^\-\.\+ &%#@\?\(\)\[\]<>\w\d]/g, + }, + }, +}; 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> </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> + + </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> + ); + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Metamodel.js b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Metamodel.js new file mode 100644 index 0000000000..82e8ada588 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Metamodel.js @@ -0,0 +1,94 @@ +/*! + * 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 _merge from 'lodash/merge'; + +import Common from '../common/Common'; + +/** + * Rules governing what a definition can contain. + */ +export default class Metamodel { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct from JSON definition. + * @param json schema definition. + */ + constructor(json) { + Common.assertType(json, 'Object'); + const dfault = require('./templates/default.metamodel.json'); + this.json = _merge({}, dfault, json); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get schema identifier. + * @returns ID. + */ + getId() { + return this.json.diagram.metadata.id; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get lifeline constraints. + * @returns {*} + */ + getConstraints() { + return this.json.diagram.lifelines.constraints; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get lifeline metadata by lifeline ID. + * @param id sought lifeline. + * @returns lifeline if found. + */ + getLifelineById(id) { + for (const lifeline of this.json.diagram.lifelines.lifelines) { + if (lifeline.id === id) { + return lifeline; + } + } + return undefined; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get original JSON. + * @returns JSON. + */ + unwrap() { + return this.json; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get default schema. + * @returns Metamodel default (permissive) Metamodel. + */ + static getDefault() { + return new Metamodel({}); + } + +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Metamodels.js b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Metamodels.js new file mode 100644 index 0000000000..4ecfc0b5f7 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Metamodels.js @@ -0,0 +1,87 @@ +/*! + * 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 Common from '../common/Common'; +import Metamodel from './Metamodel'; + +/** + * A simple lookup for schemas by ID. + */ +export default class Metamodels { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct metamodels from provided JSON definitions. + * @param metamodels JSON metamodel definitions. + */ + constructor(metamodels) { + + Common.assertType(metamodels, 'Array'); + + this.lookup = {}; + + // Save each metamodel. It's up to the Metamodel class to make sense of + // potentially nonsense metamodel definitions. + + for (const json of metamodels) { + const metamodel = new Metamodel(json); + this.lookup[metamodel.getId()] = metamodel; + } + + // Set (or override) the default metamodel with the inlined one. + + this.lookup.$ = Metamodel.getDefault(); + Common.assertInstanceOf(this.lookup.$, Metamodel); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get Metamodel by its @id. + * @param id identifier. + * @returns Metamodel, or undefined if no matching metamodel found. + */ + getMetamodel(id) { + return this.lookup[id]; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get the default (permissive) metamodel. + * @returns default Metamodel. + */ + getDefault() { + return this.lookup.$; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get metamodel by its @id, falling back to the default. + * @param id identifier. + * @returns matching metamodel, or default. + */ + getMetamodelOrDefault(id) { + const metamodel = this.getMetamodel(id); + if (metamodel) { + return metamodel; + } + return this.getDefault(); + } + +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Model.js b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Model.js new file mode 100644 index 0000000000..1e68cd6034 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/Model.js @@ -0,0 +1,512 @@ +/*! + * 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 _merge from 'lodash/merge'; +// import jsonschema from 'jsonschema'; + +import Common from '../common/Common'; +import Metamodel from './Metamodel'; + +/** + * A wrapper for a model instance. + */ +export default class Model { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct model from model JSON. JSON is assumed to be in more or less + * the correct structure, but it's OK if it's missing IDs. + * + * @param json initial JSON; will be updated in situ. + * @param metamodel Metaobject definition. + */ + constructor(json, metamodel) { + + if (metamodel) { + Common.assertInstanceOf(metamodel, Metamodel); + } + + this.metamodel = metamodel || Metamodel.getDefault(); + Common.assertInstanceOf(this.metamodel, Metamodel); + + this.jsonschema = require('./schema/asdc_sequencer_schema.json'); + this.templates = { + defaultModel: require('./templates/default.model.json'), + defaultMetamodel: require('./templates/default.metamodel.json'), + }; + + this.model = this._preprocess(Common.assertType(json, 'Object')); + Common.assertPlainObject(this.model); + + this.renumber(); + + this.addLifeline = this.addLifeline.bind(this); + this.addMessage = this.addMessage.bind(this); + this.renumber = this.renumber.bind(this); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Unwrap to get model object. + * @returns {*} + */ + unwrap() { + return Common.assertPlainObject(this.model); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get the metamodel which defines valid states for this model. + * @returns Metamodel definition. + */ + getMetamodel() { + return Common.assertInstanceOf(this.metamodel, Metamodel); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Find lifeline by its ID. + * @param id lifeline ID. + * @returns lifeline object, if found. + */ + getLifelineById(id) { + for (const lifeline of this.model.diagram.lifelines) { + if (lifeline.id === id) { + return lifeline; + } + } + return undefined; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get message by ID. + * @param id message ID. + * @returns message if matched. + */ + getMessageById(id) { + Common.assertNotNull(id); + const step = this.getStepByMessageId(id); + if (step) { + return step.message; + } + return undefined; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get step by message ID. + * @param id step ID. + * @returns step if matched. + */ + getStepByMessageId(id) { + Common.assertNotNull(id); + for (const step of this.model.diagram.steps) { + if (step.message && step.message.id === id) { + return step; + } + } + return undefined; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Add message to steps. + * @returns {{}} + */ + addMessage(index) { + const d = this.model.diagram; + const step = {}; + step.message = {}; + step.message.id = Model._guid(); + step.message.name = '[Unnamed Message]'; + step.message.type = 'request'; + step.message.from = d.lifelines.length > 0 ? d.lifelines[0].id : -1; + step.message.to = d.lifelines.length > 1 ? d.lifelines[1].id : -1; + if (index >= 0) { + d.steps.splice(index, 0, step); + } else { + d.steps.push(step); + } + this.renumber(); + return step; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Delete message with ID. + * @param id to be deleted. + */ + deleteMessageById(id) { + Common.assertNotNull(id); + const step = this.getStepByMessageId(id); + if (step) { + const index = this.model.diagram.steps.indexOf(step); + if (index !== -1) { + this.model.diagram.steps.splice(index, 1); + } + } + this.renumber(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Add lifeline to lifelines. + * @param index optional index. + * @returns {{}} + */ + addLifeline(index) { + const lifeline = {}; + lifeline.id = Model._guid(); + lifeline.name = '[Unnamed Lifeline]'; + if (index >= 0) { + this.model.diagram.lifelines.splice(index, 0, lifeline); + } else { + this.model.diagram.lifelines.push(lifeline); + } + this.renumber(); + return lifeline; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Delete lifeline with ID. + * @param id to be deleted. + */ + deleteLifelineById(id) { + Common.assertNotNull(id); + this.deleteStepsByLifelineId(id); + const lifeline = this.getLifelineById(id); + if (lifeline) { + const index = this.model.diagram.lifelines.indexOf(lifeline); + if (index !== -1) { + this.model.diagram.lifelines.splice(index, 1); + } + } + this.renumber(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Delete all steps corresponding to lifeline. + * @param id lifeline ID. + */ + deleteStepsByLifelineId(id) { + Common.assertNotNull(id); + const steps = this.getStepsByLifelineId(id); + for (const step of steps) { + this.deleteMessageById(step.message.id); + } + this.renumber(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get all steps corresponding to lifeline. + * @param id lifeline ID. + * @return steps from/to lifeline. + */ + getStepsByLifelineId(id) { + Common.assertNotNull(id); + const steps = []; + for (const step of this.model.diagram.steps) { + if (step.message) { + if (step.message.from === id || step.message.to === id) { + steps.push(step); + } + } + } + return steps; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Validate model. Disabled, because we removed the jsonschema dependency. + * @returns {Array} of validation errors, if any. + */ + validate() { + const errors = []; + return errors; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Reorder messages. + * @param index message index. + * @param afterIndex new (after) index. + */ + reorderMessages(index, afterIndex) { + Common.assertType(index, 'Number'); + Common.assertType(afterIndex, 'Number'); + const steps = this.model.diagram.steps; + const element = steps[index]; + steps.splice(index, 1); + steps.splice(afterIndex, 0, element); + this.renumber(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Reorder lifelines. + * @param index lifeline index. + * @param afterIndex new (after) index. + */ + reorderLifelines(index, afterIndex) { + Common.assertType(index, 'Number'); + Common.assertType(afterIndex, 'Number'); + const lifelines = this.model.diagram.lifelines; + const element = lifelines[index]; + lifelines.splice(index, 1); + lifelines.splice(afterIndex, 0, element); + this.renumber(); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Renumber lifelines and messages. + */ + renumber() { + const modelJSON = this.unwrap(); + let stepIndex = 1; + let lifelineIndex = 1; + for (const step of modelJSON.diagram.steps) { + if (step.message) { + step.message.index = stepIndex++; + } + } + for (const lifeline of modelJSON.diagram.lifelines) { + lifeline.index = lifelineIndex++; + } + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Build a simple, navigable dataset describing fragments. + * @returns {{}}, indexed by (stop) message ID, describing fragments. + */ + analyzeFragments() { + + const fData = {}; + + let depth = 0; + const modelJSON = this.unwrap(); + const open = []; + + const getData = function g(stop, fragment) { + let data = fData[stop]; + if (!data) { + data = { stop, start: [], fragment }; + fData[stop] = data; + } + return data; + }; + + const fragmentsByStart = {}; + for (const step of modelJSON.diagram.steps) { + if (step.message && step.message.fragment) { + const message = step.message; + const fragment = message.fragment; + if (fragment.start) { + fragmentsByStart[fragment.start] = fragment; + open.push(message.id); + depth++; + } + if (fragment.stop) { + if (open.length > 0) { + getData(message.id).start.push(open.pop()); + } + depth = Math.max(depth - 1, 0); + } + } + } + + if (open.length > 0) { + for (const o of open) { + getData(o, fragmentsByStart[o]).start.push(o); + } + } + + return fData; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Build a simple, navigable dataset describing occurrences. + * @returns a map, indexed by lifeline ID, of objects containing {start:[],stop:[],active[]}. + * @private + */ + analyzeOccurrences() { + + const oData = {}; + + // A few inline functions. They make this method kinda lengthy, but they + // reduce clutter in the class and keep it coherent, so it's OK. + + const getDataByLifelineId = function get(lifelineId) { + if (!oData[lifelineId]) { + oData[lifelineId] = { active: [], start: {}, stop: {} }; + } + return oData[lifelineId]; + }; + + const contains = function contains(array, value) { + return (array && (array.indexOf(value) !== -1)); + }; + + const process = function process(message, lifelineId) { + const oRule = message.occurrences; + if (oRule) { + + const oDataLifeline = getDataByLifelineId(lifelineId); + if (oDataLifeline) { + + // Record all starts. + + if (contains(oRule.start, lifelineId)) { + oDataLifeline.active.push(message.id); + oDataLifeline.start[message.id] = undefined; + } + + // Reconcile with stops. + + if (contains(oRule.stop, lifelineId)) { + const startMessageId = oDataLifeline.active.pop(); + oDataLifeline.stop[message.id] = startMessageId; + if (startMessageId) { + oDataLifeline.start[startMessageId] = message.id; + } + } + } + } + }; + + // Analyze start and end. + + const modelJSON = this.unwrap(); + for (const step of modelJSON.diagram.steps) { + if (step.message) { + const message = step.message; + if (message.occurrences) { + process(message, message.from); + process(message, message.to); + } + } + } + + // Reset active. (We used it, but it's not actually for us; it's for keeping + // track of active occurrences when rendering the diagram.) + + for (const lifelineId of Object.keys(oData)) { + oData[lifelineId].active = []; + } + + // Reconcile the start and end (message ID) maps for each lifeline, + // finding a "stop" for every start. Default to starting and stopping + // on the same message, which is the same as no occurrence. + + for (const lifelineId of Object.keys(oData)) { + const lifelineData = oData[lifelineId]; + for (const startId of Object.keys(lifelineData.start)) { + const stopId = lifelineData.start[startId]; + if (!stopId) { + lifelineData.start[startId] = startId; + lifelineData.stop[startId] = startId; + } + } + } + + return oData; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Preprocess model, adding IDs and whatnot. + * @param original to be preprocessed. + * @returns preprocessed JSON. + * @private + */ + _preprocess(original) { + + const json = _merge({}, this.templates.defaultModel, original); + const metamodel = this.metamodel.unwrap(); + if (!json.diagram.metadata.ref) { + if (metamodel.diagram.metadata.id) { + json.diagram.metadata.ref = metamodel.diagram.metadata.id; + } else { + json.diagram.metadata.ref = '$'; + } + } + + for (const lifeline of json.diagram.lifelines) { + lifeline.id = lifeline.id || lifeline.name; + } + + for (const step of json.diagram.steps) { + if (step.message) { + step.message.id = step.message.id || Model._guid(); + const occurrences = step.message.occurrences; + if (occurrences) { + occurrences.start = occurrences.start || []; + occurrences.stop = occurrences.stop || []; + } + } + } + + if (!json.diagram.metadata.id || json.diagram.metadata.id === '$') { + json.diagram.metadata.id = Model._guid(); + } + + return json; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Create pseudo-UUID. + * @returns {string} + * @private + */ + static _guid() { + function s4() { + return Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); + } + return `${s4()}-${s4()}-${s4()}-${s4()}-${s4()}-${s4()}-${s4()}-${s4()}`; + } + +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/Scenarios.js b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/Scenarios.js new file mode 100644 index 0000000000..4130ec7ec3 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/Scenarios.js @@ -0,0 +1,110 @@ +/*! + * 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. + */ + +/** + * Example scenarios, for development, testing and demos. + */ +export default class Scenarios { + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Construct scenarios; read model and metamodel templates. + */ + constructor() { + this.templates = { + model: { + ecomp: require('./model/ECOMP.json'), + blank: require('./model/BLANK.json'), + dimensions: require('./model/DIMENSIONS.json'), + }, + metamodel: { + ecomp: require('./metamodel/ECOMP.json'), + blank: require('./metamodel/BLANK.json'), + }, + }; + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get ECOMP scenario. + * @return ECOMP scenario JSON. + */ + getECOMP() { + return JSON.parse(JSON.stringify(this.templates.model.ecomp)); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get ECOMP scenario metamodel. + * @return scenario metamodel JSON. + */ + getECOMPMetamodel() { + return JSON.parse(JSON.stringify(this.templates.metamodel.ecomp)); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get blank scenario. + * @return blank scenario JSON. + */ + getBlank() { + return JSON.parse(JSON.stringify(this.templates.model.blank)); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get empty scenario metamodel. + * @return empty metamodel JSON. + */ + getBlankMetamodel() { + return JSON.parse(JSON.stringify(this.templates.metamodel.blank)); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get scenario. + * @return scenario JSON. + */ + getDimensions() { + return JSON.parse(JSON.stringify(this.templates.model.dimensions)); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get scenario metamodel. + * @return metamodel JSON. + */ + getDimensionsMetamodel() { + return JSON.parse(JSON.stringify(this.templates.metamodel.blank)); + } + + // /////////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Get demo metamodels. + * @returns {*[]} + */ + getMetamodels() { + return [this.getBlankMetamodel(), this.getDimensionsMetamodel(), this.getECOMPMetamodel()]; + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/BLANK.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/BLANK.json new file mode 100644 index 0000000000..2853405883 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/BLANK.json @@ -0,0 +1,16 @@ +{ + "diagram": { + "metadata": { + "id": "BLANK", + "name": "Blank" + }, + "lifelines": { + "lifelines": [], + "constraints": { + "create": true, + "delete": true, + "reorder": true + } + } + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/DIMENSIONS.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/DIMENSIONS.json new file mode 100644 index 0000000000..f02111d0f3 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/DIMENSIONS.json @@ -0,0 +1,16 @@ +{ + "diagram": { + "metadata": { + "id": "DIMENSIONS", + "name": "Dimensions" + }, + "lifelines": { + "lifelines": [], + "constraints": { + "create": true, + "delete": true, + "reorder": true + } + } + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/ECOMP.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/ECOMP.json new file mode 100644 index 0000000000..939c1398b5 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/metamodel/ECOMP.json @@ -0,0 +1,62 @@ +{ + "diagram": { + "metadata": { + "id": "ECOMP", + "name": "ECOMP" + + }, + "lifelines": { + "lifelines": [{ + "id": "1", + "name": "Customer" + }, { + "id": "2", + "name": "MSO" + }, { + "id": "3", + "name": "SDN" + }, { + "id": "4", + "name": "A&AI" + }, { + "id": "5", + "name": "IPE TOR" + }, { + "id": "6", + "name": "ORM" + }, { + "id": "7", + "name": "ORD" + }, { + "id": "8", + "name": "Heat" + }, { + "id": "9", + "name": "NovaAPI" + }, { + "id": "10", + "name": "Ntrn Contrl" + }, { + "id": "11", + "name": "RO" + }, { + "id": "12", + "name": "Nova Agent" + }, { + "id": "13", + "name": "VF Agent" + }, { + "id": "14", + "name": "Hypervisor" + }, { + "id": "15", + "name": "VF" + }], + "constraints": { + "create": true, + "delete": true, + "reorder": true + } + } + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/BLANK.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/BLANK.json new file mode 100644 index 0000000000..784a80e820 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/BLANK.json @@ -0,0 +1,37 @@ +{ + "diagram": { + "metadata": { + "id": "$", + "ref": "BLANK", + "name": "New Sequence" + }, + "lifelines": [ + { + "id": "Alice", + "name": "Alice" + }, + { + "id": "Bob", + "name": "Bob" + } + ], + "steps": [ + { + "message": { + "from": "Alice", + "to": "Bob", + "label": "Sup Bob", + "type": "request" + } + }, + { + "message": { + "from": "Bob", + "to": "Alice", + "label": "Yo Alice", + "type": "response" + } + } + ] + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/DIMENSIONS.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/DIMENSIONS.json new file mode 100644 index 0000000000..642e34a785 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/DIMENSIONS.json @@ -0,0 +1,91 @@ +{ + "diagram": { + "metadata": { + "id": "DIMENSIONS1", + "name": "Dimensions Test", + "ref": "DIMENSIONS" + }, + "lifelines": [ + { + "id": "L01", + "name": "Lorum Ipsum" + }, + { + "id": "L02", + "name": "Donec nisi urna, porttitor efficitur felis vel, efficitur consequat nunc" + }, + { + "id": "L03", + "name": "Mauris dignissim SphymomanometerSphymomanometer enim non sapien tristique lacinia" + } + ], + "steps": [ + { + "message": { + "id": "M01", + "from": "L01", + "to": "L02", + "name": "Morbi", + "type": "request", + "notes": [ + "Proin non libero malesuada." + ], + "fragment": { + "operator": "alt", + "start": true, + "guard": "Curabitur sollicitudin nulla elit, et ultrices tortor faucibus quis" + }, + "occurrences": { + "start": ["L01", "L02"], + "stop": [] + } + } + }, + { + "message": { + "id": "M02", + "from": "L02", + "to": "L03", + "name": "Quisque pretium tellus sit amet congue dictum. Mauris ac rutrum arcu, et fringilla orci", + "type": "request", + "notes": [ + "Nam quis felis hendrerit, lacinia ipsum vitae, faucibus elit. Morbi sit amet nunc eget massa vehicula rhoncus sit amet vel tellus. Aliquam accumsan eros elit, et sollicitudin lacus vehicula eu. Aenean rhoncus justo ut felis tincidunt, sit amet vulputate metus aliquet. Phasellus tellus est, consequat nec ex mollis, lacinia vestibulum justo. Nam quis felis hendrerit, lacinia ipsum vitae, faucibus elit. Morbi sit amet nunc eget massa vehicula rhoncus sit amet vel tellus. Aliquam accumsan eros elit, et sollicitudin lacus vehicula eu. Aenean rhoncus justo ut felis tincidunt, sit amet vulputate metus aliquet. Phasellus tellus est, consequat nec ex mollis, lacinia vestibulum justo." + ], + "occurrences": { + "start": [], + "stop": ["L02"] + } + } + }, + { + "message": { + "id": "M03", + "from": "L01", + "to": "L03", + "name": "Nullam", + "type": "response", + "fragment": { + "stop": true + }, + "occurrences": { + "start": [], + "stop": ["L01"] + } + } + }, + { + "message": { + "id": "M04", + "from": "L01", + "to": "L03", + "name": "Etiam convallis augue est. ", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + } + ] + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/ECOMP.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/ECOMP.json new file mode 100644 index 0000000000..dd9bfc5eb0 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/demo/scenarios/model/ECOMP.json @@ -0,0 +1,514 @@ +{ + "diagram": { + "metadata": { + "id": "ECOMP1", + "name": "Detailed flow in Ecomp1", + "ref": "ECOMP" + }, + "lifelines": [ + { + "id": "L01", + "name": "Customer" + }, + { + "id": "L02", + "name": "MSO" + }, + { + "id": "L03", + "name": "SDN" + }, + { + "id": "L04", + "name": "A&AI" + }, + { + "id": "L05", + "name": "IPE TOR" + }, + { + "id": "L06", + "name": "ORM" + }, + { + "id": "L07", + "name": "ORD" + }, + { + "id": "L08", + "name": "Heat" + }, + { + "id": "L09", + "name": "NovaAPI" + }, + { + "id": "L10", + "name": "Ntrn Contrl" + }, + { + "id": "L11", + "name": "RO" + }, + { + "id": "L12", + "name": "Nova Agent" + }, + { + "id": "L13", + "name": "VF Agent" + }, + { + "id": "L14", + "name": "Hypervisor" + }, + { + "id": "L15", + "name": "VF" + } + ], + "steps": [ + { + "message": { + "id": "M01", + "from": "L01", + "to": "L02", + "name": "Create", + "type": "request", + "notes": [ + "This note is short." + ], + "occurrences": { + "start": ["L01", "L02"], + "stop": [] + } + } + }, + { + "message": { + "id": "M02", + "from": "L02", + "to": "L04", + "name": "Check Tenant", + "type": "request", + "occurrences": { + "start": ["L02"], + "stop": [] + } + } + }, + { + "message": { + "id": "M03", + "from": "L02", + "to": "L06", + "name": "Create Tenant", + "type": "request", + "fragment": { + "operator": "alt", + "start": true, + "guard": "Does not exist" + }, + "occurrences": { + "start": ["L06"], + "stop": [] + } + } + }, + { + "message": { + "id": "M04", + "from": "L06", + "to": "L07", + "name": "Distribute", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M05", + "from": "L06", + "to": "L02", + "name": "Async Response", + "type": "response", + "asynchronous": true, + "fragment": { + "stop": true + }, + "occurrences": { + "start": [], + "stop": ["L02", "L06"] + } + } + }, + { + "message": { + "id": "M06", + "from": "L07", + "to": "L08", + "name": "Push", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M07", + "from": "L08", + "to": "L02", + "name": "Tenant Complete", + "type": "response", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M08", + "from": "L02", + "to": "L03", + "name": "Service Topology", + "type": "request", + "occurrences": { + "start": ["L03"], + "stop": [] + } + } + }, + { + "message": { + "id": "M09", + "from": "L03", + "to": "L05", + "name": "Pre-configs", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M10", + "from": "L03", + "to": "L04", + "name": "Retrieve and populate", + "type": "request", + "occurrences": { + "start": [], + "stop": ["L03"] + } + } + }, + { + "message": { + "id": "M11", + "from": "L02", + "to": "L08", + "name": "VNF PreRequisite Heat Template", + "type": "request", + "notes": [ + "I got up and made coffee and read my emails and answered them until I got frustrated and made a mental note to answer the others later and then looked out of the window for a while and then made more coffee." + ], + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M12", + "from": "L08", + "to": "L10", + "name": "Provider and OAM nw", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M13", + "from": "L02", + "to": "L08", + "name": "Get Stack Status", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M14", + "from": "L08", + "to": "L02", + "name": "Status complete", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M15", + "from": "L11", + "to": "L04", + "name": "Provider and OAM Inventory", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M16", + "from": "L02", + "to": "L08", + "name": "VNF Server Heat Template", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M17", + "from": "L08", + "to": "L10", + "name": "Show Port", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M18", + "from": "L11", + "to": "L02", + "name": "Async Response with Stack ID", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M19", + "from": "L10", + "to": "L08", + "name": "Response", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M20", + "from": "L08", + "to": "L09", + "name": "Nova VM", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M21", + "from": "L09", + "to": "L12", + "name": "Scheduler Picks Nova Agent", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M22", + "from": "L12", + "to": "L14", + "name": "Picks VF", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M23", + "from": "L12", + "to": "L10", + "name": "Retrieves Port Info", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M24", + "from": "L12", + "to": "L13", + "name": "Calls CF Agent", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M25", + "from": "L13", + "to": "L15", + "name": "Configure VF", + "type": "response", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M26", + "from": "L15", + "to": "L13", + "name": "Response", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M27", + "from": "L13", + "to": "L12", + "name": "Complete", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M28", + "from": "L12", + "to": "L08", + "name": "Response Complete", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M29", + "from": "L11", + "to": "L04", + "name": "VServer and Show Port Inventory", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M30", + "from": "L02", + "to": "L08", + "name": "Get Stack Status", + "type": "request", + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M31", + "from": "L08", + "to": "L02", + "name": "Stack Status Complete", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": [] + } + } + }, + { + "message": { + "id": "M32", + "from": "L02", + "to": "L01", + "name": "Done", + "type": "response", + "asynchronous": true, + "occurrences": { + "start": [], + "stop": ["L01", "L02"] + } + } + } + ] + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc-sequencer-meta-schema.xsd b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc-sequencer-meta-schema.xsd new file mode 100644 index 0000000000..f75063bed5 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc-sequencer-meta-schema.xsd @@ -0,0 +1,166 @@ +<xs:schema + xmlns:xs="http://www.w3.org/2001/XMLSchema" + targetNamespace="http://ns.ecomp.com/asdc/sequencer" + xmlns:s="http://ns.ecomp.com/asdc/sequencer" + attributeFormDefault="unqualified" + elementFormDefault="unqualified"> + + <!-- + + https://github.com/highsource/jsonix-schema-compiler/wiki/JSON-Schema-Generation + + npm install -x-save-dev json-schema-generation + + java -jar node_modules/jsonix-schema-compiler/lib/jsonix-schema-compiler-full.jar \ + -generateJsonSchema \ + -d ./src/main/webapp/lib/ecomp/asdc/sequencer/schema/ \ + -p asdc_sequencer_schema \ + ./src/main/webapp/lib/ecomp/asdc/sequencer/schema/asdc-sequencer-meta-schema.xsd + + --> + + <xs:element name="diagram"> + <xs:annotation> + <xs:documentation> + + Diagram meta-schema, defining what diagram documents may look like. + + The main difference between the metaschema (this) and the schema, is that + the metaschema describes what's *allowed* rather than what *is*. + + Specific differences: + + 1. The metaschema exists primarily to constrain lifelines; to declare any + that are predefined, to prescribe cardinality, order and whether or not + ad hoc lifelines may be created by the user. + 2. The metaschema doesn't constrain messages at all. This may come along later, + but for now they're freetext, and can be defined between any legal pair + of lifelines. + 3. The metaschema doesn't have @ref attributes; its @id attributes are the + target of @ref attributes in the instance schema.m + + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:sequence> + <xs:element name="metadata" type="s:metadataType"/> + <xs:element name="lifelines" type="s:lifelinesType"/> + </xs:sequence> + </xs:complexType> + </xs:element> + + <!-- /////////////////////////////////////////////////////////////////////////////////////// --> + + <xs:complexType name="entityType" abstract="true"> + <xs:annotation> + <xs:documentation> + Common attributes, most importantly @id, which every entity must have. + </xs:documentation> + </xs:annotation> + <xs:sequence> + <xs:element name="notes" minOccurs="0"> + <xs:complexType> + <xs:sequence> + <xs:element name="note" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + </xs:element> + </xs:sequence> + <xs:attribute name="id" use="required" type="xs:string"> + <xs:annotation> + <xs:documentation> + Schema definition identifier. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="name" use="required" type="xs:string"> + <xs:annotation> + <xs:documentation> + Human-readable name. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + + <!-- /////////////////////////////////////////////////////////////////////////////////////// --> + + <xs:complexType name="metadataType"> + <xs:annotation> + <xs:documentation> + Diagram metadata, including: + - Unique ID, referenced by @ref attributes in instance documents. + - Human-readable description, displayed on-screen. + </xs:documentation> + </xs:annotation> + <xs:complexContent> + <xs:extension base="s:entityType"/> + </xs:complexContent> + </xs:complexType> + + <!-- /////////////////////////////////////////////////////////////////////////////////////// --> + + <xs:complexType name="lifelineType"> + <xs:annotation> + <xs:documentation> + Metadata concerning a single lifeline. + </xs:documentation> + </xs:annotation> + <xs:complexContent> + <xs:extension base="s:entityType"> + <xs:attribute name="mandatory" type="xs:boolean" use="optional" default="false"> + <xs:annotation> + <xs:documentation> + Whether an instance may omit this lifeline. Only takes effect + where the lifelines setting is @delete=true. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:extension> + </xs:complexContent> + </xs:complexType> + + <!-- /////////////////////////////////////////////////////////////////////////////////////// --> + + <xs:complexType name="lifelinesType"> + <xs:annotation> + <xs:documentation> + Metadata concerning allowed lifelines. Somewhat more strict that + instance data. + </xs:documentation> + </xs:annotation> + <xs:complexContent> + <xs:extension base="s:entityType"> + <xs:sequence> + <xs:element name="lifeline" type="s:lifelineType" minOccurs="0" maxOccurs="unbounded"/> + <xs:element name="constraints"> + <xs:complexType> + <xs:attribute name="create" type="xs:boolean" use="required"> + <xs:annotation> + <xs:documentation> + Whether the user may create their own lifelines. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="delete" type="xs:boolean" use="required"> + <xs:annotation> + <xs:documentation> + Whether declared lifelines may be deleted. + See also @mandatory on lifeline. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="reorder" type="xs:boolean" use="required"> + <xs:annotation> + <xs:documentation> + Whether lifelines may be reordered. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + </xs:element> + </xs:sequence> + </xs:extension> + </xs:complexContent> + </xs:complexType> + +</xs:schema> diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc-sequencer-schema.xsd b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc-sequencer-schema.xsd new file mode 100644 index 0000000000..71a7d07cb1 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc-sequencer-schema.xsd @@ -0,0 +1,274 @@ +<xs:schema + xmlns:xs="http://www.w3.org/2001/XMLSchema" + targetNamespace="http://ns.ecomp.com/asdc/sequencer" + xmlns:s="http://ns.ecomp.com/asdc/sequencer" + attributeFormDefault="unqualified" + elementFormDefault="unqualified"> + + <!-- + + https://github.com/highsource/jsonix-schema-compiler/wiki/JSON-Schema-Generation + + npm install -x-save-dev json-schema-generation + + java -jar node_modules/jsonix-schema-compiler/lib/jsonix-schema-compiler-full.jar \ + -generateJsonSchema \ + -d ./src/main/webapp/lib/ecomp/asdc/sequencer/schema/ \ + -p asdc_sequencer_schema \ + ./src/main/webapp/lib/ecomp/asdc/sequencer/schema/asdc-sequencer-schema.xsd + + --> + + <!-- /////////////////////////////////////////////////////////////////////////////////////// --> + + <xs:element name="diagram"> + <xs:annotation> + <xs:documentation> + Diagram state. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:sequence> + <xs:element name="metadata" type="s:metadataType"/> + <xs:element name="lifelines" type="s:lifelinesType"/> + <xs:element name="steps" type="s:stepsType"/> + </xs:sequence> + </xs:complexType> + </xs:element> + + <!-- /////////////////////////////////////////////////////////////////////////////////////// --> + + <xs:complexType name="entityType" abstract="true"> + <xs:annotation> + <xs:documentation> + Stuff common to all entities; an identifier, a name, an optional + schema reference, and some optional notes. + </xs:documentation> + </xs:annotation> + <xs:sequence> + <xs:element name="notes" minOccurs="0"> + <xs:complexType> + <xs:sequence> + <xs:element name="note" type="xs:string" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + </xs:element> + <xs:element name="annotation" minOccurs="0"> + <xs:annotation> + <xs:documentation> + Optional annotations; non-structural information attached to any entity. + </xs:documentation> + </xs:annotation> + <xs:complexType> + <xs:sequence> + <xs:any minOccurs="0"/> + </xs:sequence> + </xs:complexType> + </xs:element> + </xs:sequence> + <xs:attribute name="id" use="required" type="xs:string"> + <xs:annotation> + <xs:documentation> + Entity identifier. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="ref" use="optional" type="xs:string"> + <xs:annotation> + <xs:documentation> + Optional reference to schema definition, where this entity + corresponds to (and is constrained by) a schema entity. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="name" use="required" type="xs:string"> + <xs:annotation> + <xs:documentation> + Human-readable name. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="externalId" use="optional" type="xs:string"> + <xs:annotation> + <xs:documentation> + ID of entity in originating system. For external use; not + used by the sequencer widget. + </xs:documentation> + </xs:annotation> + </xs:attribute> + + </xs:complexType> + + <!-- /////////////////////////////////////////////////////////////////////////////////////// --> + + <xs:complexType name="metadataType"> + <xs:annotation> + <xs:documentation> + Diagram metadata, including name, identifier and schema reference. + </xs:documentation> + </xs:annotation> + <xs:complexContent> + <xs:extension base="s:entityType"/> + </xs:complexContent> + </xs:complexType> + + <!-- /////////////////////////////////////////////////////////////////////////////////////// --> + + <xs:complexType name="lifelineType"> + <xs:annotation> + <xs:documentation> + Definition of a single lifeline. + </xs:documentation> + </xs:annotation> + <xs:complexContent> + <xs:extension base="s:entityType"/> + </xs:complexContent> + </xs:complexType> + + <!-- /////////////////////////////////////////////////////////////////////////////////////// --> + + <xs:complexType name="lifelinesType"> + <xs:annotation> + <xs:documentation> + A set of lifelines. May be top-level or in a fragment. + </xs:documentation> + </xs:annotation> + <xs:complexContent> + <xs:extension base="s:entityType"> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:element name="lifeline" type="s:lifelineType"/> + </xs:sequence> + </xs:extension> + </xs:complexContent> + </xs:complexType> + + <!-- /////////////////////////////////////////////////////////////////////////////////////// --> + + <xs:complexType name="occurrencesType"> + <xs:annotation> + <xs:documentation> + An occurrence at one or other end of a message. + </xs:documentation> + </xs:annotation> + <xs:attribute name="start" use="optional"> + <xs:simpleType> + <xs:list itemType="xs:token"/> + </xs:simpleType> + </xs:attribute> + <xs:attribute name="stop" use="optional"> + <xs:simpleType> + <xs:list itemType="xs:token"/> + </xs:simpleType> + </xs:attribute> + </xs:complexType> + + <!-- /////////////////////////////////////////////////////////////////////////////////////// --> + + <xs:complexType name="fragmentType"> + <xs:annotation> + <xs:documentation> + A fragment directive. + </xs:documentation> + </xs:annotation> + <xs:attribute name="start" type="xs:boolean" use="optional" default="false"> + <xs:annotation> + <xs:documentation> + Whether fragment starts; fragment activated when @start=true. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="stop" type="xs:string" use="optional"> + <xs:annotation> + <xs:documentation> + Indication of the last message in this fragment. + </xs:documentation> + </xs:annotation> + </xs:attribute> + <xs:attribute name="operation" use="optional" default="alt"> + <xs:annotation> + <xs:documentation> + Fragment operation. Start with the three everybody knows, but + there are others. + </xs:documentation> + </xs:annotation> + <xs:simpleType> + <xs:restriction base="xs:string"> + <xs:enumeration value="alt"/> + <xs:enumeration value="opt"/> + <xs:enumeration value="loop"/> + </xs:restriction> + </xs:simpleType> + </xs:attribute> + <xs:attribute name="guard" type="xs:string" use="optional"> + <xs:annotation> + <xs:documentation> + Guard condition. + </xs:documentation> + </xs:annotation> + </xs:attribute> + </xs:complexType> + + <!-- /////////////////////////////////////////////////////////////////////////////////////// --> + + <xs:complexType name="messageType"> + <xs:annotation> + <xs:documentation> + A message between lifelines. + </xs:documentation> + </xs:annotation> + <xs:complexContent> + <xs:extension base="s:entityType"> + <xs:sequence> + <xs:element name="occurrences" type="s:occurrencesType" minOccurs="0" maxOccurs="1"/> + <xs:element name="fragment" type="s:fragmentType" minOccurs="0" maxOccurs="1"/> + </xs:sequence> + <xs:attribute name="to" type="xs:string" use="required"/> + <xs:attribute name="from" type="xs:string" use="required"/> + <xs:attribute name="type"> + <xs:simpleType> + <xs:restriction base="xs:string"> + <xs:enumeration value="request"/> + <xs:enumeration value="response"/> + </xs:restriction> + </xs:simpleType> + </xs:attribute> + <xs:attribute name="asynchronous" type="xs:boolean" default="false"/> + </xs:extension> + </xs:complexContent> + </xs:complexType> + + <!-- /////////////////////////////////////////////////////////////////////////////////////// --> + + <xs:complexType name="guardType"> + <xs:annotation> + <xs:documentation> + Guard condition within a fragment. Some fragments have more than + one section, each with their own guard condition. + </xs:documentation> + </xs:annotation> + <xs:sequence> + <xs:element name="guard" type="xs:string"/> + <xs:element name="steps" type="s:stepsType"/> + </xs:sequence> + </xs:complexType> + + <!-- /////////////////////////////////////////////////////////////////////////////////////// --> + + <xs:complexType name="stepsType"> + <xs:annotation> + <xs:documentation> + An ordered set of messages and subsequences. + </xs:documentation> + </xs:annotation> + <xs:complexContent> + <xs:extension base="s:entityType"> + <xs:sequence maxOccurs="unbounded"> + <xs:choice> + <xs:element name="message" type="s:messageType"/> + </xs:choice> + </xs:sequence> + </xs:extension> + </xs:complexContent> + </xs:complexType> + +</xs:schema> diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc_sequencer_meta_schema.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc_sequencer_meta_schema.json new file mode 100644 index 0000000000..cf4174ed35 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc_sequencer_meta_schema.json @@ -0,0 +1,332 @@ + +{ + "id":"#", + "definitions":{ + "LifelinesType.Constraints":{ + "type":"object", + "title":"LifelinesType.Constraints", + "required":[ + "create", + "delete", + "reorder" + ], + "properties":{ + "create":{ + "title":"create", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/boolean" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"create", + "namespaceURI":"" + } + }, + "delete":{ + "title":"delete", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/boolean" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"delete", + "namespaceURI":"" + } + }, + "reorder":{ + "title":"reorder", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/boolean" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"reorder", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "propertiesOrder":[ + "create", + "delete", + "reorder" + ] + }, + "LifelinesType":{ + "required":[ + "constraints" + ], + "allOf":[ + { + "$ref":"#/definitions/EntityType" + }, + { + "type":"object", + "title":"LifelinesType", + "properties":{ + "lifeline":{ + "title":"lifeline", + "allOf":[ + { + "type":"array", + "items":{ + "$ref":"#/definitions/LifelineType" + }, + "minItems":0 + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"lifeline", + "namespaceURI":"" + } + }, + "constraints":{ + "title":"constraints", + "allOf":[ + { + "$ref":"#/definitions/LifelinesType.Constraints" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"constraints", + "namespaceURI":"" + } + } + } + } + ], + "typeType":"classInfo", + "typeName":{ + "localPart":"lifelinesType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "lifeline", + "constraints" + ] + }, + "EntityType.Notes":{ + "type":"object", + "title":"EntityType.Notes", + "properties":{ + "note":{ + "title":"note", + "allOf":[ + { + "type":"array", + "items":{ + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + }, + "minItems":0 + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"note", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "propertiesOrder":[ + "note" + ] + }, + "MetadataType":{ + "allOf":[ + { + "$ref":"#/definitions/EntityType" + }, + { + "type":"object", + "title":"MetadataType", + "properties":{ + } + } + ], + "typeType":"classInfo", + "typeName":{ + "localPart":"metadataType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + } + }, + "EntityType":{ + "type":"object", + "title":"EntityType", + "required":[ + "id", + "name" + ], + "properties":{ + "notes":{ + "title":"notes", + "allOf":[ + { + "$ref":"#/definitions/EntityType.Notes" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"notes", + "namespaceURI":"" + } + }, + "id":{ + "title":"id", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"id", + "namespaceURI":"" + } + }, + "name":{ + "title":"name", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"name", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "typeName":{ + "localPart":"entityType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "notes", + "id", + "name" + ] + }, + "Diagram":{ + "type":"object", + "title":"Diagram", + "required":[ + "metadata", + "lifelines" + ], + "properties":{ + "metadata":{ + "title":"metadata", + "allOf":[ + { + "$ref":"#/definitions/MetadataType" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"metadata", + "namespaceURI":"" + } + }, + "lifelines":{ + "title":"lifelines", + "allOf":[ + { + "$ref":"#/definitions/LifelinesType" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"lifelines", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "propertiesOrder":[ + "metadata", + "lifelines" + ] + }, + "LifelineType":{ + "allOf":[ + { + "$ref":"#/definitions/EntityType" + }, + { + "type":"object", + "title":"LifelineType", + "properties":{ + "mandatory":{ + "title":"mandatory", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/boolean" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"mandatory", + "namespaceURI":"" + } + } + } + } + ], + "typeType":"classInfo", + "typeName":{ + "localPart":"lifelineType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "mandatory" + ] + } + }, + "anyOf":[ + { + "type":"object", + "properties":{ + "name":{ + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/QName" + }, + { + "type":"object", + "properties":{ + "localPart":{ + "enum":[ + "diagram" + ] + }, + "namespaceURI":{ + "enum":[ + "http://ns.ecomp.com/asdc/sequencer" + ] + } + } + } + ] + }, + "value":{ + "$ref":"#/definitions/Diagram" + } + }, + "elementName":{ + "localPart":"diagram", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + } + } + ] +}
\ No newline at end of file diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc_sequencer_schema.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc_sequencer_schema.json new file mode 100644 index 0000000000..d655826290 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/schema/asdc_sequencer_schema.json @@ -0,0 +1,582 @@ + +{ + "id":"#", + "definitions":{ + "EntityType.Notes":{ + "type":"object", + "title":"EntityType.Notes", + "properties":{ + "note":{ + "title":"note", + "allOf":[ + { + "type":"array", + "items":{ + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + }, + "minItems":0 + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"note", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "propertiesOrder":[ + "note" + ] + }, + "GuardType":{ + "type":"object", + "title":"GuardType", + "required":[ + "guard", + "steps" + ], + "properties":{ + "guard":{ + "title":"guard", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"guard", + "namespaceURI":"" + } + }, + "steps":{ + "title":"steps", + "allOf":[ + { + "$ref":"#/definitions/StepsType" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"steps", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "typeName":{ + "localPart":"guardType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "guard", + "steps" + ] + }, + "MetadataType":{ + "allOf":[ + { + "$ref":"#/definitions/EntityType" + }, + { + "type":"object", + "title":"MetadataType", + "properties":{ + } + } + ], + "typeType":"classInfo", + "typeName":{ + "localPart":"metadataType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + } + }, + "OccurrencesType":{ + "type":"object", + "title":"OccurrencesType", + "properties":{ + "start":{ + "title":"start", + "allOf":[ + { + "type":"array", + "items":{ + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"start", + "namespaceURI":"" + } + }, + "stop":{ + "title":"stop", + "allOf":[ + { + "type":"array", + "items":{ + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"stop", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "typeName":{ + "localPart":"occurrencesType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "start", + "stop" + ] + }, + "Diagram":{ + "type":"object", + "title":"Diagram", + "required":[ + "metadata", + "lifelines", + "steps" + ], + "properties":{ + "metadata":{ + "title":"metadata", + "allOf":[ + { + "$ref":"#/definitions/MetadataType" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"metadata", + "namespaceURI":"" + } + }, + "lifelines":{ + "title":"lifelines", + "allOf":[ + { + "$ref":"#/definitions/LifelinesType" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"lifelines", + "namespaceURI":"" + } + }, + "steps":{ + "title":"steps", + "allOf":[ + { + "$ref":"#/definitions/StepsType" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"steps", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "propertiesOrder":[ + "metadata", + "lifelines", + "steps" + ] + }, + "LifelineType":{ + "allOf":[ + { + "$ref":"#/definitions/EntityType" + }, + { + "type":"object", + "title":"LifelineType", + "properties":{ + } + } + ], + "typeType":"classInfo", + "typeName":{ + "localPart":"lifelineType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + } + }, + "LifelinesType":{ + "allOf":[ + { + "$ref":"#/definitions/EntityType" + }, + { + "type":"object", + "title":"LifelinesType", + "properties":{ + "lifeline":{ + "title":"lifeline", + "allOf":[ + { + "type":"array", + "items":{ + "$ref":"#/definitions/LifelineType" + }, + "minItems":0 + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"lifeline", + "namespaceURI":"" + } + } + } + } + ], + "typeType":"classInfo", + "typeName":{ + "localPart":"lifelinesType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "lifeline" + ] + }, + "FragmentType":{ + "type":"object", + "title":"FragmentType", + "properties":{ + "start":{ + "title":"start", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/boolean" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"start", + "namespaceURI":"" + } + }, + "stop":{ + "title":"stop", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"stop", + "namespaceURI":"" + } + }, + "operation":{ + "title":"operation", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"operation", + "namespaceURI":"" + } + }, + "guard":{ + "title":"guard", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"guard", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "typeName":{ + "localPart":"fragmentType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "start", + "stop", + "operation", + "guard" + ] + }, + "StepsType":{ + "required":[ + "message" + ], + "allOf":[ + { + "$ref":"#/definitions/EntityType" + }, + { + "type":"object", + "title":"StepsType", + "properties":{ + "message":{ + "title":"message", + "allOf":[ + { + "type":"array", + "items":{ + "$ref":"#/definitions/MessageType" + }, + "minItems":1 + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"message", + "namespaceURI":"" + } + } + } + } + ], + "typeType":"classInfo", + "typeName":{ + "localPart":"stepsType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "message" + ] + }, + "EntityType":{ + "type":"object", + "title":"EntityType", + "required":[ + "id", + "name" + ], + "properties":{ + "notes":{ + "title":"notes", + "allOf":[ + { + "$ref":"#/definitions/EntityType.Notes" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"notes", + "namespaceURI":"" + } + }, + "id":{ + "title":"id", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"id", + "namespaceURI":"" + } + }, + "ref":{ + "title":"ref", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"ref", + "namespaceURI":"" + } + }, + "name":{ + "title":"name", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"name", + "namespaceURI":"" + } + } + }, + "typeType":"classInfo", + "typeName":{ + "localPart":"entityType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "notes", + "id", + "ref", + "name" + ] + }, + "MessageType":{ + "required":[ + "to", + "from" + ], + "allOf":[ + { + "$ref":"#/definitions/EntityType" + }, + { + "type":"object", + "title":"MessageType", + "properties":{ + "occurrences":{ + "title":"occurrences", + "allOf":[ + { + "$ref":"#/definitions/OccurrencesType" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"occurrences", + "namespaceURI":"" + } + }, + "fragment":{ + "title":"fragment", + "allOf":[ + { + "$ref":"#/definitions/FragmentType" + } + ], + "propertyType":"element", + "elementName":{ + "localPart":"fragment", + "namespaceURI":"" + } + }, + "to":{ + "title":"to", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"to", + "namespaceURI":"" + } + }, + "from":{ + "title":"from", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"from", + "namespaceURI":"" + } + }, + "type":{ + "title":"type", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/string" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"type", + "namespaceURI":"" + } + }, + "asynchronous":{ + "title":"asynchronous", + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/boolean" + } + ], + "propertyType":"attribute", + "attributeName":{ + "localPart":"asynchronous", + "namespaceURI":"" + } + } + } + } + ], + "typeType":"classInfo", + "typeName":{ + "localPart":"messageType", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + }, + "propertiesOrder":[ + "occurrences", + "fragment", + "to", + "from", + "type", + "asynchronous" + ] + } + }, + "anyOf":[ + { + "type":"object", + "properties":{ + "name":{ + "allOf":[ + { + "$ref":"http://www.jsonix.org/jsonschemas/w3c/2001/XMLSchema.jsonschema#/definitions/QName" + }, + { + "type":"object", + "properties":{ + "localPart":{ + "enum":[ + "diagram" + ] + }, + "namespaceURI":{ + "enum":[ + "http://ns.ecomp.com/asdc/sequencer" + ] + } + } + } + ] + }, + "value":{ + "$ref":"#/definitions/Diagram" + } + }, + "elementName":{ + "localPart":"diagram", + "namespaceURI":"http://ns.ecomp.com/asdc/sequencer" + } + } + ] +}
\ No newline at end of file diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/templates/default.metamodel.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/templates/default.metamodel.json new file mode 100644 index 0000000000..f6a28a8723 --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/templates/default.metamodel.json @@ -0,0 +1,17 @@ +{ + "diagram": { + "metadata": { + "id": "$", + "name": "Blank Sequence" + + }, + "lifelines": { + "lifelines": [], + "constraints": { + "create": true, + "delete": true, + "reorder": true + } + } + } +} diff --git a/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/templates/default.model.json b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/templates/default.model.json new file mode 100644 index 0000000000..42edc5516b --- /dev/null +++ b/dox-sequence-diagram-ui/src/main/webapp/lib/ecomp/asdc/sequencer/model/templates/default.model.json @@ -0,0 +1,11 @@ +{ + "diagram": { + "metadata": { + "id": "$", + "name": "New Sequence" + + }, + "lifelines": [], + "steps": [] + } +} |