2 * Copyright © 2016-2017 European Support Limited
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13 * or implied. See the License for the specific language governing
14 * permissions and limitations under the License.
17 import React from 'react';
18 import PropTypes from 'prop-types';
19 import _template from 'lodash/template';
20 import _merge from 'lodash/merge';
21 import { select, event as d3event } from 'd3-selection';
22 import { zoom as d3zoom } from 'd3-zoom';
24 import Common from '../../common/Common';
25 import Logger from '../../common/Logger';
26 import Popup from './components/popup/Popup';
31 class Diagram extends React.Component {
32 // ///////////////////////////////////////////////////////////////////////////////////////////////
35 * Construct React view.
36 * @param props properties.
37 * @param context context.
39 constructor(props, context) {
40 super(props, context);
42 this.application = Common.assertNotNull(props.application);
43 this.options = this.application.getOptions().diagram;
52 diagram: _template(require('./templates/diagram.html')),
53 lifeline: _template(require('./templates/lifeline.html')),
54 message: _template(require('./templates/message.html')),
55 occurrence: _template(require('./templates/occurrence.html')),
56 fragment: _template(require('./templates/fragment.html')),
57 title: _template(require('./templates/title.html'))
60 this.handleResize = this.handleResize.bind(this);
61 this.initialTransformX = 0;
62 this.initialTransformY = 0;
65 // ///////////////////////////////////////////////////////////////////////////////////////////////
72 this.svg.select('').text(n);
75 // ///////////////////////////////////////////////////////////////////////////////////////////////
78 * Get SVG from diagram.
82 const svg = this.svg.node().outerHTML;
83 return svg.replace('<svg ', '<svg xmlns="http://www.w3.org/2000/svg" ');
86 // ///////////////////////////////////////////////////////////////////////////////////////////////
89 * Select message by ID.
90 * @param id message ID.
93 const sel = this.svg.selectAll('g.asdcs-diagram-message-container');
94 sel.classed('asdcs-active', false);
95 sel.selectAll('rect.asdcs-diagram-message-bg').attr('filter', null);
97 const parent = this.svg.select(
98 `g.asdcs-diagram-message-container[data-id="${id}"]`
100 parent.classed('asdcs-active', true);
102 .selectAll('rect.asdcs-diagram-message-bg')
103 .attr('filter', 'url(#asdcsSvgHighlight)');
105 this._showNotesPopup(id);
108 // ///////////////////////////////////////////////////////////////////////////////////////////////
111 * Select lifeline by ID.
112 * @param id lifeline ID.
115 const sel = this.svg.selectAll('g.asdcs-diagram-lifeline-container');
116 sel.classed('asdcs-active', false);
117 sel.selectAll('rect').attr('filter', null);
119 const parent = this.svg.select(
120 `g.asdcs-diagram-lifeline-container[data-id="${id}"]`
122 parent.selectAll('rect').attr('filter', 'url(#asdcsSvgHighlight)');
123 parent.classed('asdcs-active', true);
127 // ///////////////////////////////////////////////////////////////////////////////////////////////
130 * Handle resize, including initial sizing.
134 const height = this.wrapper.offsetHeight;
135 const width = this.wrapper.offsetWidth;
136 if (this.state.height !== height || this.state.width !== width) {
137 this.setState({ height, width });
142 // ///////////////////////////////////////////////////////////////////////////////////////////////
145 * (Re)render diagram.
148 const model = this.application.getModel();
149 const modelJSON = model.unwrap();
150 const name = modelJSON.diagram.metadata.name;
151 const options = this.application.getOptions();
152 const titleHeight = options.diagram.title.height;
154 titleHeight && titleHeight > 0
155 ? `height:${titleHeight}`
159 <div className="asdcs-diagram">
160 <div className={`asdcs-diagram-name ${titleClass}`}>{name}</div>
162 className="asdcs-diagram-svg"
177 // ///////////////////////////////////////////////////////////////////////////////////////////////
183 // ///////////////////////////////////////////////////////////////////////////////////////////////
188 componentDidMount() {
189 window.addEventListener('resize', this.handleResize);
199 // ///////////////////////////////////////////////////////////////////////////////////////////////
201 componentWillUnmount() {
202 window.removeEventListener('resize', this.handleResize);
205 // ///////////////////////////////////////////////////////////////////////////////////////////////
210 componentDidUpdate() {
214 // ///////////////////////////////////////////////////////////////////////////////////////////////
217 * Redraw SVG diagram. So far it's fast enough that it doesn't seem to matter whether
218 * it's completely redrawn.
222 const svgparams = _merge({}, this.options.svg);
223 this.wrapper.innerHTML = this.templates.diagram(svgparams);
224 this.svg = select(this.wrapper).select('svg');
227 if (this.state.height === 0) {
228 // We'll get a resize event, and the height will be non-zero when it's actually time.
233 if (this.state.height && this.state.width) {
234 const margin = this.options.svg.margin;
237 const height = this.state.height + margin * 2;
238 const width = this.state.width + margin * 2;
239 const viewBox = `${x} ${y} ${width} ${height}`;
240 this.svg.attr('viewBox', viewBox);
243 // If we've already rendered, then save the current scale/translate so that we
244 // can reapply it after rendering.
246 const gContentSelection = this.svg.selectAll('g.asdcs-diagram-content');
247 if (gContentSelection.size() === 1) {
248 const transform = gContentSelection.attr('transform');
250 this.savedTransform = transform;
254 // Empty the document. We're starting again.
256 this.svg.selectAll('.asdcs-diagram-content').remove();
258 // Extract the model.
260 const model = this.application.getModel();
264 const modelJSON = model.unwrap();
266 // Extract dimension options.
268 const header = this.options.lifelines.header;
269 const spacing = this.options.lifelines.spacing;
271 // Make separate container elements so that we can control Z order.
273 const gContent = this.svg
275 .attr('class', 'asdcs-diagram-content');
276 const gLifelines = gContent
278 .attr('class', 'asdcs-diagram-lifelines');
279 const gCanvas = gContent
281 .attr('class', 'asdcs-diagram-canvas');
282 gCanvas.append('g').attr('class', 'asdcs-diagram-occurrences');
283 gCanvas.append('g').attr('class', 'asdcs-diagram-fragments');
284 gCanvas.append('g').attr('class', 'asdcs-diagram-messages');
286 // Lifelines -----------------------------------------------------------------------------------
288 const actorsById = {};
289 const positionsByMessageId = {};
290 const lifelines = [];
291 for (const actor of modelJSON.diagram.lifelines) {
292 const x = header.width / 2 + lifelines.length * spacing.horizontal;
293 Diagram._processLifeline(actor, x);
294 lifelines.push({ x, actor });
295 actorsById[actor.id] = actor;
298 // Messages ------------------------------------------------------------------------------------
300 // Analyze occurrence information.
302 const occurrences = model.analyzeOccurrences();
303 const fragments = model.analyzeFragments();
304 let y = this.options.lifelines.header.height + spacing.vertical;
305 let messageIndex = 0;
306 for (const step of modelJSON.diagram.steps) {
308 positionsByMessageId[step.message.id] =
309 positionsByMessageId[step.message.id] || {};
310 positionsByMessageId[step.message.id].y = y;
316 positionsByMessageId,
322 y += spacing.vertical;
325 // ---------------------------------------------------------------------------------------------
327 // Draw the actual (dashed) lifelines in a background <g>.
329 this._drawLifelines(gLifelines, lifelines, y);
331 // Initialize mouse event handlers.
333 this._initMouseEvents(gLifelines, gCanvas);
337 const bb = gContent.node().getBBox();
338 this._initZoom(gContent, bb.width, bb.height);
341 // ///////////////////////////////////////////////////////////////////////////////////////////////
342 // ///////////////////////////////////////////////////////////////////////////////////////////////
343 // ///////////////////////////////////////////////////////////////////////////////////////////////
344 // ///////////////////////////////////////////////////////////////////////////////////////////////
345 // ///////////////////////////////////////////////////////////////////////////////////////////////
346 // ///////////////////////////////////////////////////////////////////////////////////////////////
347 // ///////////////////////////////////////////////////////////////////////////////////////////////
348 // ///////////////////////////////////////////////////////////////////////////////////////////////
349 // ///////////////////////////////////////////////////////////////////////////////////////////////
350 // ///////////////////////////////////////////////////////////////////////////////////////////////
353 * Draw message into SVG canvas.
354 * @param gCanvas container.
355 * @param message message to be rendered.
356 * @param y current y position.
357 * @param actorsById actor lookup.
358 * @param positionsByMessageId x- and y-position of each message.
359 * @param messageIndex where we are in the set of messages to be rendered.
360 * @param oData occurrences info.
361 * @param fData fragments info.
369 positionsByMessageId,
374 Common.assertNotNull(oData);
376 const request = message.type === 'request';
377 const fromActor = request
378 ? actorsById[message.from]
379 : actorsById[message.to];
380 const toActor = request
381 ? actorsById[message.to]
382 : actorsById[message.from];
386 `Cannot draw message ${JSON.stringify(
388 )}: 'from' not found.`
395 `Cannot draw message ${JSON.stringify(
402 // Occurrences. --------------------------------------------------------------------------------
404 if (message.occurrence) {
406 `Found occurrence for ${message.name}: ${JSON.stringify(
411 const activeTo = Diagram._calcActive(oData, toActor.id);
412 this._drawOccurrence(
415 positionsByMessageId,
419 this._drawOccurrence(
422 positionsByMessageId,
426 const activeFrom = Diagram._calcActive(oData, fromActor.id);
428 // Messages. -----------------------------------------------------------------------------------
430 const gMessages = gCanvas.select('g.asdcs-diagram-messages');
432 // Save positions for later.
434 const positions = positionsByMessageId[message.id];
435 positions.x0 = fromActor.x;
436 positions.x1 = toActor.x;
440 const leftToRight = fromActor.x < toActor.x;
441 const loopback = message.to === message.from;
442 const x1 = this._calcMessageX(activeTo, toActor.x, true, leftToRight);
445 : this._calcMessageX(activeFrom, fromActor.x, false, leftToRight);
451 messagePath = `M${x1},${y}`;
452 messagePath = `${messagePath} L${x1 + 200},${y}`;
453 messagePath = `${messagePath} L${x1 + 200},${y + 50}`;
454 messagePath = `${messagePath} L${x1},${y + 50}`;
456 // Between lifelines.
458 messagePath = `M${x0},${y}`;
459 messagePath = `${messagePath} L${x1},${y}`;
462 const styles = Diagram._getMessageStyles(message);
464 // Split message over lines.
466 const messageWithPrefix = `${messageIndex}. ${message.name}`;
467 const maxLines = this.options.messages.label.maxLines;
468 const wrapWords = this.options.messages.label.wrapWords;
469 const wrapLines = this.options.messages.label.wrapLines;
470 const messageLines = Common.tokenize(
477 const messageTxt = this.templates.message({
480 marker: styles.marker,
481 dasharray: styles.dasharray,
482 labels: messageLines,
491 const messageEl = Common.txt2dom(messageTxt);
492 const gMessage = gMessages.append('g');
493 Common.dom2svg(messageEl, gMessage);
495 // Set the background's bounding box to that of the text,
496 // so that they fit snugly.
498 const labelBB = gMessage
499 .select('.asdcs-diagram-message-label')
503 .select('.asdcs-diagram-message-label-bg')
504 .attr('x', labelBB.x)
505 .attr('y', labelBB.y)
506 .attr('height', labelBB.height)
507 .attr('width', labelBB.width);
509 // Fragments. ----------------------------------------------------------------------------------
511 const fragment = fData[message.id];
513 // It ends on this message.
515 this._drawFragment(gCanvas, fragment, positionsByMessageId);
519 // ///////////////////////////////////////////////////////////////////////////////////////////////
522 * Draw a single occurrence.
523 * @param gCanvas container.
524 * @param oData occurrence data.
525 * @param positionsByMessageId map of y positions by message ID.
526 * @param actor wrapper containing lifeline ID (.id), position (.x) and name (.name).
527 * @param messageId message identifier.
530 _drawOccurrence(gCanvas, oData, positionsByMessageId, actor, messageId) {
531 Common.assertType(oData, 'Object');
532 Common.assertType(positionsByMessageId, 'Object');
533 Common.assertType(actor, 'Object');
534 Common.assertType(messageId, 'String');
536 const gOccurrences = gCanvas.select('g.asdcs-diagram-occurrences');
538 const oOptions = this.options.lifelines.occurrences;
539 const oWidth = oOptions.width;
540 const oHalfWidth = oWidth / 2;
541 const oForeshortening = oOptions.foreshortening;
542 const oMarginTop = oOptions.marginTop;
543 const oMarginBottom = oOptions.marginBottom;
544 const o = oData[actor.id];
546 const active = Diagram._calcActive(oData, actor.id);
548 const x = actor.x - oHalfWidth + active * oWidth;
549 const positions = positionsByMessageId[messageId];
550 const y = positions.y;
554 if (o.start[messageId]) {
555 // Starting, but drawing nothing until we find the end.
557 o.active.push(messageId);
559 } else if (active > 0) {
560 const startMessageId = o.stop[messageId];
561 if (startMessageId) {
562 // OK, it ends here. Draw the occurrence box.
565 const foreshorteningY = active * oForeshortening;
566 const startY = positionsByMessageId[startMessageId].y;
573 x: actor.x - oHalfWidth + (active - 1) * oWidth,
574 y: startY - oMarginTop + foreshorteningY,
579 const occurrenceTxt = this.templates.occurrence(oProps);
580 const occurrenceEl = Common.txt2dom(occurrenceTxt);
581 Common.dom2svg(occurrenceEl, gOccurrences.append('g'));
588 // Seems this is a singleton occurrence. We just draw a wee box around it.
590 const height = oMarginTop + oMarginBottom;
591 const occurrenceProperties = {
597 const defaultTxt = this.templates.occurrence(occurrenceProperties);
598 const defaultEl = Common.txt2dom(defaultTxt);
599 Common.dom2svg(defaultEl, gOccurrences.append('g'));
603 // ///////////////////////////////////////////////////////////////////////////////////////////////
606 * Draw box(es) around fragment(s).
607 * @param gCanvas container.
608 * @param fragment fragment definition, corresponding to its final (stop) message.
609 * @param positionsByMessageId message dimensions.
612 _drawFragment(gCanvas, fragment, positionsByMessageId) {
613 const optFragments = this.options.fragments;
614 const gFragments = gCanvas.select('g.asdcs-diagram-fragments');
615 const p1 = positionsByMessageId[fragment.stop];
616 if (p1 && fragment.start && fragment.start.length > 0) {
617 for (const start of fragment.start) {
618 const message = this.application
620 .getMessageById(start);
621 const bounds = this._calcFragmentBounds(
627 const maxLines = this.options.fragments.label.maxLines;
628 const wrapWords = this.options.fragments.label.wrapWords;
629 const wrapLines = this.options.fragments.label.wrapLines;
630 const lines = Common.tokenize(
631 message.fragment.guard,
639 x: bounds.x0 - optFragments.leftMargin,
640 y: bounds.y0 - optFragments.topMargin,
642 bounds.y1 - bounds.y0 + optFragments.heightMargin,
643 width: bounds.x1 - bounds.x0 + optFragments.widthMargin,
644 operator: message.fragment.operator || 'alt',
648 const fragmentTxt = this.templates.fragment(params);
649 const fragmentEl = Common.txt2dom(fragmentTxt);
650 const gFragment = gFragments.append('g');
651 Common.dom2svg(fragmentEl, gFragment);
653 const labelBB = gFragment
654 .select('.asdcs-diagram-fragment-guard')
658 .select('.asdcs-diagram-fragment-guard-bg')
659 .attr('x', labelBB.x)
660 .attr('y', labelBB.y)
661 .attr('height', labelBB.height)
662 .attr('width', labelBB.width);
664 Logger.warn(`Bad fragment: ${JSON.stringify(fragment)}`);
670 // ///////////////////////////////////////////////////////////////////////////////////////////////
672 _calcFragmentBounds(startMessage, fragment, positionsByMessageId) {
674 const steps = this.application.getModel().unwrap().diagram.steps;
675 const bounds = { x0: 99999, x1: 0, y0: 99999, y1: 0 };
676 let foundStart = false;
677 let foundStop = false;
678 for (const step of steps) {
679 const message = step.message;
681 if (message.id === startMessage.id) {
684 if (foundStart && !foundStop) {
685 const positions = positionsByMessageId[message.id];
687 bounds.x0 = Math.min(
689 Math.min(positions.x0, positions.x1)
691 bounds.y0 = Math.min(bounds.y0, positions.y);
692 bounds.x1 = Math.max(
694 Math.max(positions.x0, positions.x1)
696 bounds.y1 = Math.max(bounds.y1, positions.y);
698 // This probably means it hasn't been recorded yet, which is fine, because
699 // we draw fragments from where they END.
704 if (message.id === fragment.stop) {
714 // ///////////////////////////////////////////////////////////////////////////////////////////////
717 * Draw all lifelines.
718 * @param gLifelines lifelines container.
719 * @param lifelines lifelines definitions.
723 _drawLifelines(gLifelines, lifelines, y) {
724 const maxLines = this.options.lifelines.header.maxLines;
725 const wrapWords = this.options.lifelines.header.wrapWords;
726 const wrapLines = this.options.lifelines.header.wrapLines;
728 for (const lifeline of lifelines) {
729 const lines = Common.tokenize(
735 const lifelineTxt = this.templates.lifeline({
741 headerHeight: this.options.lifelines.header.height,
742 headerWidth: this.options.lifelines.header.width,
743 id: lifeline.actor.id
746 const lifelineEl = Common.txt2dom(lifelineTxt);
747 Common.dom2svg(lifelineEl, gLifelines.append('g'));
751 // ///////////////////////////////////////////////////////////////////////////////////////////////
754 * Initialize all mouse events.
755 * @param gLifelines lifelines container.
756 * @param gCanvas top-level canvas container.
759 _initMouseEvents(gLifelines, gCanvas) {
761 const source = 'asdcs';
762 const origin = `${window.location.protocol}//${window.location.host}`;
766 .selectAll('.asdcs-diagram-lifeline-selectable')
767 .on('mouseenter', function f() {
768 timer = setTimeout(() => {
769 self.application.selectLifeline(
770 select(this.parentNode).attr('data-id')
774 .on('mouseleave', () => {
776 self.application.selectLifeline();
778 .on('click', function f() {
779 const id = select(this.parentNode).attr('data-id');
780 window.postMessage({ source, id, type: 'lifeline' }, origin);
784 .selectAll('.asdcs-diagram-lifeline-heading-box')
785 .on('mouseenter', function f() {
786 timer = setTimeout(() => {
787 self.application.selectLifeline(
788 select(this.parentNode).attr('data-id')
792 .on('mouseleave', () => {
794 self.application.selectLifeline();
796 .on('click', function f() {
797 const id = select(this.parentNode).attr('data-id');
799 { source, id, type: 'lifelineHeader' },
805 .selectAll('.asdcs-diagram-message-selectable')
806 .on('mouseenter', function f() {
807 self.events.message = { x: d3event.pageX, y: d3event.pageY };
808 timer = setTimeout(() => {
809 self.application.selectMessage(
810 select(this.parentNode).attr('data-id')
814 .on('mouseleave', () => {
815 delete self.events.message;
817 self.application.selectMessage();
819 .on('click', function f() {
820 const id = select(this.parentNode).attr('data-id');
821 window.postMessage({ source, id, type: 'message' }, origin);
825 // ///////////////////////////////////////////////////////////////////////////////////////////////
828 * Get CSS classes to be applied to a message, according to whether request/response
829 * or synchronous/asynchronous.
830 * @param message message being rendered.
831 * @returns CSS class name(s).
834 static _getMessageStyles(message) {
835 let marker = 'asdcsDiagramArrowSolid';
837 let css = 'asdcs-diagram-message';
838 if (message.type === 'request') {
839 css = `${css} asdcs-diagram-message-request`;
841 css = `${css} asdcs-diagram-message-response`;
842 marker = 'asdcsDiagramArrowOpen';
843 dasharray = '30, 10';
846 if (message.asynchronous) {
847 css = `${css} asdcs-diagram-message-asynchronous`;
848 marker = 'asdcsDiagramArrowOpen';
850 css = `${css} asdcs-diagram-message-synchronous`;
853 return { css, marker, dasharray };
856 // ///////////////////////////////////////////////////////////////////////////////////////////////
859 * Initialize or reinitialize zoom. This sets the initial zoom in the case of
860 * a re-rendering, and initializes the eventhandling in all cases.
862 * It does some fairly risky parsing of the 'transform' attribute, assuming that it
863 * can contain scale() and translate(). But only the zoom handler and us are writing
864 * the transform values, so that's probably OK.
866 * @param gContent container.
867 * @param width diagram width.
868 * @param height diagram height.
871 _initZoom(gContent, width, height) {
872 const zoomed = function zoomed() {
873 if (!this.initialTransformX && !this.initialTransformY) {
874 this.initialTransformX = d3event.transform.x;
875 this.initialTransformY = d3event.transform.y;
880 `translate(${d3event.transform.x -
881 this.initialTransformX}, ${d3event.transform.y -
882 this.initialTransformY})scale(${d3event.transform.k}, ${
888 const viewWidth = this.state.width || this.options.svg.width;
889 const viewHeight = this.state.height || this.options.svg.height;
890 const scaleMinimum = this.options.svg.scale.minimum;
891 const scaleWidth = viewWidth / width;
892 const scaleHeight = viewHeight / height;
894 let scale = scaleMinimum;
895 if (this.options.svg.scale.width) {
896 scale = Math.max(scale, scaleWidth);
898 if (this.options.svg.scale.height) {
899 scale = Math.min(scale, scaleHeight);
902 scale = Math.max(scale, scaleMinimum);
904 let translate = [0, 0];
905 if (this.savedTransform) {
906 const s = this.savedTransform;
907 const scaleStart = s.indexOf('scale(');
908 if (scaleStart !== -1) {
909 scale = parseFloat(s.substring(scaleStart + 6, s.length - 1));
911 const translateStart = s.indexOf('translate(');
912 if (translateStart !== -1) {
913 const spec = s.substring(
915 s.indexOf(')', translateStart)
917 const tokens = spec.split(',');
918 translate = [parseFloat(tokens[0]), parseFloat(tokens[1])];
921 gContent.attr('transform', this.savedTransform);
923 gContent.attr('transform', `scale(${scale})`);
926 const zoom = d3zoom().on('zoom', zoomed);
929 this.svg.call(zoom.scaleBy, scale);
933 `translate(${translate[0]}, ${translate[1]})`
935 gContent.attr('transform', `scale(${scale})`);
938 // ///////////////////////////////////////////////////////////////////////////////////////////////
941 * Hide from the linter the fact that we're modifying the lifeline.
942 * @param lifeline to be updated with X position.
943 * @param x X position.
946 static _processLifeline(lifeline, x) {
947 const actor = lifeline;
948 actor.id = actor.id || actor.name;
952 // ///////////////////////////////////////////////////////////////////////////////////////////////
955 * Derive active occurrences for lifeline.
956 * @param oData occurrences data.
957 * @param lifelineId lifeline to be analyzed.
961 static _calcActive(oData, lifelineId) {
962 const o = oData[lifelineId];
965 active = o.active.length;
970 // ///////////////////////////////////////////////////////////////////////////////////////////////
973 * Derive the X position of an occurrence on a lifeline, taking into account how
974 * many occurrences are active.
975 * @param active active count.
976 * @param x lifeline X position; basis for offset.
977 * @param arrow whether this is the arrow (to) end.
978 * @param leftToRight whether this message goes left-to-right.
979 * @returns {*} calculated X position for occurrence left-hand side.
982 _calcMessageX(active, x, arrow, leftToRight) {
983 const width = this.options.lifelines.occurrences.width;
984 const halfWidth = width / 2;
985 const active0 = Math.max(0, active - 1);
986 let calculated = x + active0 * width;
990 calculated -= halfWidth;
992 calculated += halfWidth;
995 // Start (NOT ARROW).
997 calculated += halfWidth;
999 calculated -= halfWidth;
1006 // ///////////////////////////////////////////////////////////////////////////////////////////////
1009 * Show popup upon hovering over a messages that has associated notes.
1013 _showNotesPopup(id) {
1016 const message = this.application.getModel().getMessageById(id);
1020 message.notes.length > 0 &&
1023 this.popup.setState({
1025 left: this.events.message.x - 50,
1026 top: this.events.message.y + 20,
1027 notes: message.notes[0]
1031 this.popup.setState({ visible: false, notes: '' });
1037 Diagram.propTypes = {
1038 application: PropTypes.object.isRequired
1041 export default Diagram;