diff --git a/esp/src/eclwatch/InfoGridWidget.js b/esp/src/eclwatch/InfoGridWidget.js index 3effa1b5c72..2e5ab2fb00b 100755 --- a/esp/src/eclwatch/InfoGridWidget.js +++ b/esp/src/eclwatch/InfoGridWidget.js @@ -222,7 +222,7 @@ define([ this.widget.ErrWarnDialogTextArea.domNode.select(); }, _onDownload: function (evt) { - Utility.downloadText(Utility.toCSV(this.infoData, ","), "ErrWarn.csv"); + Utility.downloadCSV(Utility.toCSV(this.infoData, ","), "ErrWarn.csv"); }, _onErrors: function (args) { diff --git a/esp/src/src-react/components/Common.tsx b/esp/src/src-react/components/Common.tsx index 7c122c8e0d6..d39e49bcfb8 100644 --- a/esp/src/src-react/components/Common.tsx +++ b/esp/src/src-react/components/Common.tsx @@ -17,7 +17,7 @@ export function createCopyDownloadSelection(columns, selection: any, filename: s key: "download", text: nlsHPCC.DownloadSelectionAsCSV, disabled: !selection.length, iconOnly: true, iconProps: { iconName: "Download" }, onClick: () => { const csv = Utility.formatAsDelim(columns, selection, ","); - Utility.downloadText(csv, filename); + Utility.downloadCSV(csv, filename); } }]; } diff --git a/esp/src/src-react/components/Metrics.tsx b/esp/src/src-react/components/Metrics.tsx index ee87e620eec..ed3d5c14ae2 100644 --- a/esp/src/src-react/components/Metrics.tsx +++ b/esp/src/src-react/components/Metrics.tsx @@ -303,7 +303,7 @@ export const Metrics: React.FunctionComponent = ({ for (let i = 0; i < minLen; ++i) { const item = lineages[0][i]; if (lineages.every(lineage => lineage[i] === item)) { - if (metricGraph.isSubgraph(item) && item.name) { + if (metricGraph.isSubgraph(item) && item.id && !metricGraph.isVertex(item)) { newLineage.push(item); } } else { @@ -642,7 +642,7 @@ export const Metrics: React.FunctionComponent = ({ iconProps: { iconName: "Table" }, onClick: () => { const csv = Utility.formatAsDelim(formatColumns, metrics, ","); - Utility.downloadText(csv, `metrics-${wuid}.csv`); + Utility.downloadCSV(csv, `metrics-${wuid}.csv`); } }, { @@ -650,7 +650,7 @@ export const Metrics: React.FunctionComponent = ({ text: nlsHPCC.DownloadToDOT, iconProps: { iconName: "Relationship" }, onClick: () => { - Utility.downloadText(dot, `metrics-${wuid}.dot`); + Utility.downloadPlain(dot, `metrics-${wuid}.dot`); } }] } diff --git a/esp/src/src-react/util/metricGraph.ts b/esp/src/src-react/util/metricGraph.ts index 9e28bfea2d3..f88a375a143 100644 --- a/esp/src/src-react/util/metricGraph.ts +++ b/esp/src/src-react/util/metricGraph.ts @@ -10,6 +10,10 @@ const logger = scopedLogger("src-react/util/metricGraph.ts"); declare const dojoConfig; +const TypeShape = { + "function": 'plain" fillcolor="" style="' +}; + const KindShape = { 2: "cylinder", // Disk Write 3: "tripleoctagon", // Local Sort @@ -36,8 +40,8 @@ const KindShape = { 196: "cylinder", // Spill Write }; -function shape(kind: string) { - return KindShape[kind] || "rectangle"; +function shape(v: IScope) { + return TypeShape[v.type] ?? KindShape[v.kind] ?? "rectangle"; } const CHARS = new Set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); @@ -243,11 +247,18 @@ export class MetricGraph extends Graph2 { return this.outEdges(v.name).filter(e => e.__parentName === v.__parentName); } + protected _dedupVerticies: { [scopeName: string]: boolean } = {}; vertexTpl(v: IScope, options: MetricsOptions): string { - return `"${v.id}" [id="${encodeID(v.name)}" label="${encodeLabel(this.vertexLabel(v, options))}" shape="${shape(v.Kind)}" class="${this.vertexStatus(v)}"]`; + if (this._dedupVerticies[v.id] === true) return ""; + this._dedupVerticies[v.id] = true; + return `"${v.id}" [id="${encodeID(v.name)}" label="${encodeLabel(this.vertexLabel(v, options))}" shape="${shape(v)}" class="${this.vertexStatus(v)}"]`; } - protected _dedupEdges: { [scopeName: string]: boolean } = {}; + hiddenTpl(v: IScope, options: MetricsOptions): string { + if (this._dedupVerticies[v.id] === true) return ""; + this._dedupVerticies[v.id] = true; + return `"${v.id}" [id="${encodeID(v.name)}" label="${encodeLabel(this.vertexLabel(v, options))}" shape="${shape(v)}" class="${this.vertexStatus(v)}" rank="min"]`; + } findFirstVertex(scopeName: string) { if (this.vertexExists(scopeName)) { @@ -275,14 +286,15 @@ export class MetricGraph extends Graph2 { return "unknown"; } + protected _dedupEdges: { [scopeName: string]: boolean } = {}; edgeTpl(e: IScopeEdge, options: MetricsOptions) { if (this._dedupEdges[e.id] === true) return ""; this._dedupEdges[e.id] = true; if (options.ignoreGlobalStoreOutEdges && this.vertex(this._activityIndex[e.IdSource]).Kind === "22") { return ""; } - const ltail = this.subgraphExists(this._sourceFunc(e.IdSource)) ? `ltail="cluster_${e.IdSource}"` : ""; - const lhead = this.subgraphExists(this._targetFunc(e.IdTarget)) ? `lhead="cluster_${e.IdTarget}"` : ""; + const ltail = this.subgraphExists(this._sourceFunc(e)) ? `ltail=cluster_${e.IdSource}` : ""; + const lhead = this.subgraphExists(this._targetFunc(e)) ? `lhead=cluster_${e.IdTarget}` : ""; return `"${e.IdSource}" -> "${e.IdTarget}" [id="${encodeID(e.name)}" label="${encodeLabel(format(options.edgeTpl, { ...e, ...e.__formattedProps }))}" style="${this.vertexParent(this._activityIndex[e.IdSource]) === this.vertexParent(this._activityIndex[e.IdTarget]) ? "solid" : "dashed"}" class="${this.edgeStatus(e)}" ${ltail} ${lhead}]`; } @@ -298,11 +310,17 @@ export class MetricGraph extends Graph2 { return "unknown"; } + protected _dedupSubgraphs: { [scopeName: string]: boolean } = {}; subgraphTpl(sg: IScope, options: MetricsOptions): string { + if (this._dedupSubgraphs[sg.id] === true) return ""; + this._dedupSubgraphs[sg.id] = true; const childTpls: string[] = []; this.subgraphSubgraphs(sg.name).forEach(child => { childTpls.push(this.subgraphTpl(child, options)); }); + if (this.vertexExists(this.id(sg))) { + childTpls.push(this.hiddenTpl(this.vertex(this.id(sg)), options)); + } this.subgraphVertices(sg.name).forEach(child => { childTpls.push(this.vertexTpl(child, options)); }); @@ -324,6 +342,8 @@ subgraph cluster_${encodeID(sg.id)} { graphTpl(items: IScope[] = [], options: MetricsOptions) { // subgraphs.sort(); + this._dedupSubgraphs = {}; + this._dedupVerticies = {}; this._dedupEdges = {}; const childTpls: string[] = []; if (items?.length) { @@ -359,6 +379,8 @@ subgraph cluster_${encodeID(sg.id)} { } return `\ digraph G { + compound=true; + oredering=in; graph [fontname="arial"];// fontsize=11.0]; // graph [rankdir=TB]; // node [shape=rect fontname=arial fontsize=11.0 fixedsize=true]; diff --git a/esp/src/src/Utility.ts b/esp/src/src/Utility.ts index 010e403cc53..0c225ad67c0 100644 --- a/esp/src/src/Utility.ts +++ b/esp/src/src/Utility.ts @@ -1056,17 +1056,25 @@ export function toCSV(data, delim = ",") { return retVal; } -export function downloadText(content: string, fileName: string) { - const encodedUri = "data:text/csv;charset=utf-8,\uFEFF" + encodeURI(content); +function downloadText(content: string, fileName: string, type: "csv" | "plain" = "csv") { + const textBlob = new Blob([content], { type: `text/${type}` }); const link = document.createElement("a"); - link.setAttribute("href", encodedUri); link.setAttribute("download", fileName); + link.setAttribute("href", window.URL.createObjectURL(textBlob)); link.style.visibility = "hidden"; document.body.appendChild(link); link.click(); document.body.removeChild(link); } +export function downloadCSV(content: string, fileName: string) { + downloadText(content, fileName, "csv"); +} + +export function downloadPlain(content: string, fileName: string) { + downloadText(content, fileName, "plain"); +} + const d3FormatNum = d3Format(","); export function parseCookies(): Record {