mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-28 01:31:17 +00:00
SVG export button
* applies CSS styles inline * exports SVG chart node * injects A tag to download * based on SVG-crowbar Fixes #555
This commit is contained in:
@@ -2,6 +2,7 @@ import debug from 'debug';
|
||||
|
||||
import AppDispatcher from '../dispatcher/app-dispatcher';
|
||||
import ActionTypes from '../constants/action-types';
|
||||
import { saveGraph } from '../utils/file-utils';
|
||||
import { updateRoute } from '../utils/router-utils';
|
||||
import { doControlRequest, getNodesDelta, getNodeDetails,
|
||||
getTopologies, deletePipe } from '../utils/web-api-utils';
|
||||
@@ -57,6 +58,10 @@ export function clickCloseTerminal(pipeId, closePipe) {
|
||||
updateRoute();
|
||||
}
|
||||
|
||||
export function clickDownloadGraph() {
|
||||
saveGraph();
|
||||
}
|
||||
|
||||
export function clickForceRelayout() {
|
||||
AppDispatcher.dispatch({
|
||||
type: ActionTypes.CLICK_FORCE_RELAYOUT
|
||||
|
||||
@@ -225,7 +225,7 @@ export default class NodesChart extends React.Component {
|
||||
<div className="nodes-chart">
|
||||
{errorEmpty}
|
||||
{errorMaxNodesExceeded}
|
||||
<svg width="100%" height="100%" className={svgClassNames} onClick={this.handleMouseClick}>
|
||||
<svg width="100%" height="100%" id="nodes-chart-canvas" className={svgClassNames} onClick={this.handleMouseClick}>
|
||||
<g className="canvas" transform={transform}>
|
||||
<g className="edges">
|
||||
{edgeElements}
|
||||
|
||||
@@ -7,7 +7,7 @@ import Status from './status.js';
|
||||
import Topologies from './topologies.js';
|
||||
import TopologyOptions from './topology-options.js';
|
||||
import { getApiDetails, getTopologies, basePathSlash } from '../utils/web-api-utils';
|
||||
import { clickForceRelayout, hitEsc } from '../actions/app-actions';
|
||||
import { clickDownloadGraph, clickForceRelayout, hitEsc } from '../actions/app-actions';
|
||||
import Details from './details';
|
||||
import Nodes from './nodes';
|
||||
import EmbeddedTerminal from './embedded-terminal';
|
||||
@@ -124,6 +124,9 @@ export default class App extends React.Component {
|
||||
<a className={forceRelayoutClassName} onClick={clickForceRelayout} title={forceRelayoutTitle}>
|
||||
<span className="fa fa-refresh" />
|
||||
</a>
|
||||
<a className="footer-label footer-label-icon" onClick={clickDownloadGraph} title="Save canvas as SVG">
|
||||
<span className="fa fa-download" />
|
||||
</a>
|
||||
<a className="footer-label footer-label-icon" href={otherContrastModeUrl} title={otherContrastModeTitle}>
|
||||
<span className="fa fa-adjust" />
|
||||
</a>
|
||||
|
||||
134
client/app/scripts/utils/file-utils.js
Normal file
134
client/app/scripts/utils/file-utils.js
Normal file
@@ -0,0 +1,134 @@
|
||||
// adapted from https://github.com/NYTimes/svg-crowbar
|
||||
import _ from 'lodash';
|
||||
|
||||
const doctype = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">';
|
||||
const prefix = {
|
||||
xmlns: 'http://www.w3.org/2000/xmlns/',
|
||||
xlink: 'http://www.w3.org/1999/xlink',
|
||||
svg: 'http://www.w3.org/2000/svg'
|
||||
};
|
||||
|
||||
function setInlineStyles(svg, emptySvgDeclarationComputed) {
|
||||
function explicitlySetStyle(element) {
|
||||
const cSSStyleDeclarationComputed = getComputedStyle(element);
|
||||
let value;
|
||||
let computedStyleStr = '';
|
||||
_.each(cSSStyleDeclarationComputed, key => {
|
||||
value = cSSStyleDeclarationComputed.getPropertyValue(key);
|
||||
if (value !== emptySvgDeclarationComputed.getPropertyValue(key)) {
|
||||
computedStyleStr += key + ':' + value + ';';
|
||||
}
|
||||
});
|
||||
element.setAttribute('style', computedStyleStr);
|
||||
}
|
||||
|
||||
function traverse(obj) {
|
||||
const tree = [];
|
||||
|
||||
function visit(node) {
|
||||
if (node && node.hasChildNodes()) {
|
||||
let child = node.firstChild;
|
||||
while (child) {
|
||||
if (child.nodeType === 1 && child.nodeName !== 'SCRIPT') {
|
||||
tree.push(child);
|
||||
visit(child);
|
||||
}
|
||||
child = child.nextSibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tree.push(obj);
|
||||
visit(obj);
|
||||
return tree;
|
||||
}
|
||||
|
||||
// hardcode computed css styles inside svg
|
||||
const allElements = traverse(svg);
|
||||
let i = allElements.length;
|
||||
while (i--) {
|
||||
explicitlySetStyle(allElements[i]);
|
||||
}
|
||||
// set font
|
||||
svg.setAttribute('style', 'font-family: "Roboto", sans-serif;');
|
||||
}
|
||||
|
||||
function download(source) {
|
||||
let filename = 'untitled';
|
||||
|
||||
if (source.id) {
|
||||
filename = source.id;
|
||||
} else if (source.class) {
|
||||
filename = source.class;
|
||||
} else if (window.document.title) {
|
||||
filename = window.document.title.replace(/[^a-z0-9]/gi, '-').toLowerCase();
|
||||
}
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob(source.source,
|
||||
{'type': 'text\/xml'}
|
||||
));
|
||||
|
||||
const a = document.createElement('a');
|
||||
document.body.appendChild(a);
|
||||
a.setAttribute('class', 'svg-crowbar');
|
||||
a.setAttribute('download', filename + '.svg');
|
||||
a.setAttribute('href', url);
|
||||
a.style.display = 'none';
|
||||
a.click();
|
||||
|
||||
setTimeout(function() {
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function getSVG(doc, emptySvgDeclarationComputed) {
|
||||
const svg = document.getElementById('nodes-chart-canvas');
|
||||
|
||||
svg.setAttribute('version', '1.1');
|
||||
|
||||
// removing attributes so they aren't doubled up
|
||||
svg.removeAttribute('xmlns');
|
||||
svg.removeAttribute('xlink');
|
||||
|
||||
// These are needed for the svg
|
||||
if (!svg.hasAttributeNS(prefix.xmlns, 'xmlns')) {
|
||||
svg.setAttributeNS(prefix.xmlns, 'xmlns', prefix.svg);
|
||||
}
|
||||
|
||||
if (!svg.hasAttributeNS(prefix.xmlns, 'xmlns:xlink')) {
|
||||
svg.setAttributeNS(prefix.xmlns, 'xmlns:xlink', prefix.xlink);
|
||||
}
|
||||
|
||||
setInlineStyles(svg, emptySvgDeclarationComputed);
|
||||
|
||||
const source = (new XMLSerializer()).serializeToString(svg);
|
||||
|
||||
return {
|
||||
class: svg.getAttribute('class'),
|
||||
id: svg.getAttribute('id'),
|
||||
childElementCount: svg.childElementCount,
|
||||
source: [doctype + source]
|
||||
};
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
const crowbarElements = document.querySelectorAll('.svg-crowbar');
|
||||
|
||||
[].forEach.call(crowbarElements, function(el) {
|
||||
el.parentNode.removeChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
export function saveGraph() {
|
||||
window.URL = (window.URL || window.webkitURL);
|
||||
|
||||
// add empty svg element
|
||||
const emptySvg = window.document.createElementNS(prefix.svg, 'svg');
|
||||
window.document.body.appendChild(emptySvg);
|
||||
const emptySvgDeclarationComputed = getComputedStyle(emptySvg);
|
||||
|
||||
const svgSource = getSVG(document, emptySvgDeclarationComputed);
|
||||
download(svgSource);
|
||||
|
||||
cleanup();
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"browser-filesaver": "^1.1.0",
|
||||
"classnames": "^2.2.1",
|
||||
"d3": "~3.5.5",
|
||||
"dagre": "0.7.4",
|
||||
|
||||
Reference in New Issue
Block a user