From f5a1903328ef4314aeeeb38a127829fe20fc9eef Mon Sep 17 00:00:00 2001 From: Sebastian Wendorf Date: Mon, 30 Sep 2019 11:03:23 +0200 Subject: [PATCH 01/22] Removed ProjectFile concept --- data/jqassistant/dashboard.adoc | 12 ------------ src/api/models/Dashboard.js | 16 ++++++++-------- src/api/models/Dependencies.js | 2 +- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/data/jqassistant/dashboard.adoc b/data/jqassistant/dashboard.adoc index b9877f0..10fcfec 100644 --- a/data/jqassistant/dashboard.adoc +++ b/data/jqassistant/dashboard.adoc @@ -145,15 +145,3 @@ RETURN ORDER BY FilesOfType desc ---- - -[[jqassistant-dashboard:ProjectFile]] -[source,cypher,role="concept",verify="aggregation"] -.Every `:Type` which is contained in an artifact is labeled as `:ProjectFile`. ----- -MATCH - (:Artifact)-[:CONTAINS]->(t:Type) -SET - t:ProjectFile -RETURN - count(t) as NumberOfProjectFiles ----- diff --git a/src/api/models/Dashboard.js b/src/api/models/Dashboard.js index 3255fa7..5c8873d 100644 --- a/src/api/models/Dashboard.js +++ b/src/api/models/Dashboard.js @@ -17,10 +17,10 @@ class DashboardModel { "OPTIONAL MATCH (t:Type:Annotation) " + "WITH classes, interfaces, enums, count(t) as annotations " + // number of methods and lines of code - "OPTIONAL MATCH (t:Type:ProjectFile)-[:DECLARES]->(m:Method) " + + "OPTIONAL MATCH (:Artifact)-[:CONTAINS]->(t:Type), (t)-[:DECLARES]->(m:Method) " + "WITH classes, interfaces, enums, annotations, count(m) as methods, sum(m.effectiveLineCount) as loc " + // number of fields - "OPTIONAL MATCH (t:Type:ProjectFile)-[:DECLARES]->(f:Field) " + + "OPTIONAL MATCH (:Artifact)-[:CONTAINS]->(t:Type), (t)-[:DECLARES]->(f:Field) " + "RETURN classes, interfaces, enums, annotations, methods, loc, count(f) as fields"; localStorage.setItem( @@ -31,23 +31,23 @@ class DashboardModel { const dashboardDependenciesQuery = // relation metrics (table 2) // dependencies - "OPTIONAL MATCH (:Type:ProjectFile)-[d:DEPENDS_ON]->(:Type) " + + "OPTIONAL MATCH (:Artifact)-[:CONTAINS]->(t:Type), (t)-[d:DEPENDS_ON]->(:Type) " + "WITH count(d) as dependencies " + // extends - "OPTIONAL MATCH (:Type:ProjectFile)-[e:EXTENDS]->(superType:Type) " + + "OPTIONAL MATCH (:Artifact)-[:CONTAINS]->(t:Type), (t)-[e:EXTENDS]->(superType:Type) " + 'WHERE superType.name <> "Object" ' + "WITH dependencies, count(e) as extends " + // implements - "OPTIONAL MATCH (:Type:ProjectFile)-[i:IMPLEMENTS]->(:Type) " + + "OPTIONAL MATCH (:Artifact)-[:CONTAINS]->(t:Type), (t)-[i:IMPLEMENTS]->(:Type) " + "WITH dependencies, extends, count(i) as implements " + // calls - "OPTIONAL MATCH (:Type:ProjectFile)-[:DECLARES]->(m:Method)-[i:INVOKES]->(:Method) " + + "OPTIONAL MATCH (:Artifact)-[:CONTAINS]->(t:Type), (t)-[:DECLARES]->(m:Method)-[i:INVOKES]->(:Method) " + "WITH dependencies, extends, implements, count(i) as invocations " + // reads - "OPTIONAL MATCH (:Type:ProjectFile)-[:DECLARES]->(m:Method)-[r:READS]->(:Field) " + + "OPTIONAL MATCH (:Artifact)-[:CONTAINS]->(t:Type), (t)-[:DECLARES]->(m:Method)-[r:READS]->(:Field) " + "WITH dependencies, extends, implements, invocations, count(r) as reads " + // writes - "OPTIONAL MATCH (:Type:ProjectFile)-[:DECLARES]->(m:Method)-[w:WRITES]->(:Field) " + + "OPTIONAL MATCH (:Artifact)-[:CONTAINS]->(t:Type), (t)-[:DECLARES]->(m:Method)-[w:WRITES]->(:Field) " + "RETURN dependencies, extends, implements, invocations, reads, count(w) as writes"; localStorage.setItem( "dashboard_dependencies_original_query", diff --git a/src/api/models/Dependencies.js b/src/api/models/Dependencies.js index 3672dc5..eda202b 100644 --- a/src/api/models/Dependencies.js +++ b/src/api/models/Dependencies.js @@ -3,7 +3,7 @@ import { neo4jSession } from "../../views/Dashboard/AbstractDashboardComponent"; class DependenciesModel { constructor(props) { const dependenciesQuery = - "MATCH (dependent_package:Package)-[:CONTAINS]->(dependent:Type:ProjectFile)-[depends:DEPENDS_ON]->(dependency:Type:ProjectFile)<-[:CONTAINS]-(dependency_package:Package) " + + "MATCH (:Artifact)-[:CONTAINS]->(dependent:Type), (:Artifact)-[:CONTAINS]->(dependency:Type), (dependent_package:Package)-[:CONTAINS]->(dependent)-[depends:DEPENDS_ON]->(dependency)<-[:CONTAINS]-(dependency_package:Package) " + "WITH dependent_package.fqn as dependent, dependency_package.fqn as dependency, count(dependency) as dependencies " + "RETURN dependent , dependency, dependencies ORDER BY dependent, dependency"; localStorage.setItem("dependencies_original_query", dependenciesQuery); From 41997dd9722d209f3d376f8e58fe058c03c470a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20M=C3=BCller?= Date: Wed, 29 Jan 2020 13:12:11 +0100 Subject: [PATCH 02/22] add publications --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 33807e5..26dfca6 100644 --- a/README.md +++ b/README.md @@ -75,3 +75,11 @@ $ docker run -it -p 7474:7474 -p 7687:7687 visualsoftwareanalytics/jqa-dashboard * [React Table](https://github.com/react-tools/react-table) * [Graph App Kit](https://github.com/neo4j-apps/graph-app-kit) * [Neo4j](https://github.com/neo4j/neo4j) + +## Publications ## + +* David Baum, Pascal Kovacs, Richard Müller: [Fostering Collaboration of Academia and Industry by Open Source Software](https://www.researchgate.net/publication/338008152_Fostering_Collaboration_of_Academia_and_Industry_by_Open_Source_Software), SE20 Software Engineering, 2020. +* Richard Müller, Dirk Mahler, Michael Hunger, Jens Nerche, Markus Harrer: [Towards an Open Source Stack to Create a Unified Data Source for Software Analysis and Visualization](https://www.researchgate.net/publication/328282991_Towards_an_Open_Source_Stack_to_Create_a_Unified_Data_Source_for_Software_Analysis_and_Visualization), 6th IEEE Working Conference on Software Visualization, 2018. +* Tino Mewes: [Konzeption und prototypische Implementierung eines web-basierten Dashboards zur Softwarevisualisierung](http://nbn-resolving.de/urn:nbn:de:bsz:15-qucosa2-323826), Masterarbeit, 2018. + +A full list of publications you can find on [our website](http://home.uni-leipzig.de/svis/Publications/). From e5b4b2926408713bfedaf9f122e6a8f6c1839af0 Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Wed, 15 Jul 2020 18:26:15 +0200 Subject: [PATCH 03/22] Tidy Tree Visualization --- data/junit/Dockerfile | 4 +- package.json | 1 + src/_nav.js | 70 ++++---- src/routes.js | 157 +++++++++++++----- .../Architecture/Dependencies/Dependencies.js | 1 - .../Dashboard/Architecture/Layers/Layers.js | 34 ++++ .../Architecture/Layers/LayersWrapper.js | 20 +++ .../visualizations/LayersVisualization.js | 81 +++++++++ 8 files changed, 289 insertions(+), 79 deletions(-) create mode 100644 src/views/Dashboard/Architecture/Layers/Layers.js create mode 100644 src/views/Dashboard/Architecture/Layers/LayersWrapper.js create mode 100644 src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js diff --git a/data/junit/Dockerfile b/data/junit/Dockerfile index d49aebb..8f4da86 100644 --- a/data/junit/Dockerfile +++ b/data/junit/Dockerfile @@ -1,4 +1,4 @@ -FROM neo4j:latest +FROM neo4j:3.5 ENV NEO4J_PASSWD neo4j ENV NEO4J_AUTH neo4j/${NEO4J_PASSWD} @@ -12,4 +12,4 @@ CMD sed -e 's/^#dbms.read_only=.*$/dbms.read_only=true/' -i /var/lib/neo4j/conf/ bin/neo4j-admin set-initial-password ${NEO4J_PASSWD} || true && \ bin/neo4j-admin load --from=/var/lib/neo4j/import/junit.dump --force && \ bin/neo4j start && sleep 5 && \ - tail -f logs/neo4j.log \ No newline at end of file + tail -f logs/neo4j.log diff --git a/package.json b/package.json index 4bb5305..b241d7d 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "codecov": "^3.2.0", "core-js": "^2.6.5", "d3": "^5.9.2", + "d3-selection": "^1.4.1", "dateformat": "^3.0.3", "dns": "^0.2.2", "enzyme": "^3.9.0", diff --git a/src/_nav.js b/src/_nav.js index abfc621..dd4a50a 100644 --- a/src/_nav.js +++ b/src/_nav.js @@ -1,73 +1,77 @@ export default { items: [ { - name: 'Dashboard', - url: '/dashboard', - icon: 'icon-speedometer' + name: "Dashboard", + url: "/dashboard", + icon: "icon-speedometer" }, { - name: 'Architecture', - url: '/architecture', - icon: 'fa fa-sitemap', + name: "Architecture", + url: "/architecture", + icon: "fa fa-sitemap", children: [ { - name: 'Structure', - url: '/architecture/structure' + name: "Structure", + url: "/architecture/structure" }, { - name: 'File Types', - url: '/architecture/file-types' + name: "File Types", + url: "/architecture/file-types" }, { - name: 'Dependencies', - url: '/architecture/dependencies' + name: "Dependencies", + url: "/architecture/dependencies" + }, + { + name: "Layers", + url: "/architecture/layers" } ] }, { - name: 'Resource Management', - url: '/resource-management', - icon: 'icon-people', + name: "Resource Management", + url: "/resource-management", + icon: "icon-people", children: [ { - name: 'Activity', - url: '/resource-management/activity' + name: "Activity", + url: "/resource-management/activity" }, { - name: 'Knowledge Distribution', - url: '/resource-management/knowledge-distribution' + name: "Knowledge Distribution", + url: "/resource-management/knowledge-distribution" } ] }, { - name: 'Risk Management', - url: '/risk-management', - icon: 'fa fa-exclamation-triangle', + name: "Risk Management", + url: "/risk-management", + icon: "fa fa-exclamation-triangle", children: [ { - name: 'Hotspots', - url: '/risk-management/hotspots' + name: "Hotspots", + url: "/risk-management/hotspots" } ] }, { - name: 'Quality Management', - url: '/quality-management', - icon: 'icon-badge', + name: "Quality Management", + url: "/quality-management", + icon: "icon-badge", children: [ { - name: 'Static Code Analysis', - url: '/quality-management/static-code-analysis', + name: "Static Code Analysis", + url: "/quality-management/static-code-analysis", children: [ { - name: 'PMD', - url: '/quality-management/static-code-analysis/pmd' + name: "PMD", + url: "/quality-management/static-code-analysis/pmd" } ] }, { - name: 'Test Coverage', - url: '/quality-management/test-coverage' + name: "Test Coverage", + url: "/quality-management/test-coverage" } ] } diff --git a/src/routes.js b/src/routes.js index a1e3551..3314637 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,86 +1,157 @@ -import React from 'react'; -import Loadable from 'react-loadable' +import React from "react"; +import Loadable from "react-loadable"; -import DefaultLayout from './containers/DefaultLayout'; +import DefaultLayout from "./containers/DefaultLayout"; function Loading() { - return
Loading...
; + return
Loading...
; } const Dashboard = Loadable({ - loader: () => import('./views/Dashboard'), - loading: Loading, + loader: () => import("./views/Dashboard"), + loading: Loading }); const Structure = Loadable({ - loader: () => import('./views/Dashboard/Architecture/Structure/Structure'), - loading: Loading, + loader: () => import("./views/Dashboard/Architecture/Structure/Structure"), + loading: Loading }); const FileTypes = Loadable({ - loader: () => import('./views/Dashboard/Architecture/FileTypes/FileTypes'), - loading: Loading, + loader: () => import("./views/Dashboard/Architecture/FileTypes/FileTypes"), + loading: Loading }); const Dependencies = Loadable({ - loader: () => import('./views/Dashboard/Architecture/Dependencies/Dependencies'), - loading: Loading, + loader: () => + import("./views/Dashboard/Architecture/Dependencies/Dependencies"), + loading: Loading +}); + +const Layers = Loadable({ + loader: () => import("./views/Dashboard/Architecture/Layers/Layers"), + loading: Loading }); const Activity = Loadable({ - loader: () => import('./views/Dashboard/ResourceManagement/Activity/Activity'), - loading: Loading, + loader: () => + import("./views/Dashboard/ResourceManagement/Activity/Activity"), + loading: Loading }); const KnowledgeDistribution = Loadable({ - loader: () => import('./views/Dashboard/ResourceManagement/KnowledgeDistribution/KnowledgeDistribution'), - loading: Loading, + loader: () => + import( + "./views/Dashboard/ResourceManagement/KnowledgeDistribution/KnowledgeDistribution" + ), + loading: Loading }); const Hotspots = Loadable({ - loader: () => import('./views/Dashboard/RiskManagement/Hotspots/Hotspots'), - loading: Loading, + loader: () => import("./views/Dashboard/RiskManagement/Hotspots/Hotspots"), + loading: Loading }); const StaticCodeAnalysisPMD = Loadable({ - loader: () => import('./views/Dashboard/QualityManagement/StaticCodeAnalysis/PMD/PMD'), - loading: Loading, + loader: () => + import( + "./views/Dashboard/QualityManagement/StaticCodeAnalysis/PMD/PMD" + ), + loading: Loading }); const TestCoverage = Loadable({ - loader: () => import('./views/Dashboard/QualityManagement/TestCoverage/TestCoverage'), - loading: Loading, + loader: () => + import("./views/Dashboard/QualityManagement/TestCoverage/TestCoverage"), + loading: Loading }); const Settings = Loadable({ - loader: () => import('./views/Dashboard/Header/Settings'), - loading: Loading, + loader: () => import("./views/Dashboard/Header/Settings"), + loading: Loading }); const CustomQuery = Loadable({ - loader: () => import('./views/Dashboard/Header/CustomQuery'), - loading: Loading, + loader: () => import("./views/Dashboard/Header/CustomQuery"), + loading: Loading }); // https://github.com/ReactTraining/react-router/tree/master/packages/react-router-config const routes = [ - { path: '/', exact: true, name: 'Home', component: DefaultLayout }, - { path: '/dashboard', name: 'Dashboard', component: Dashboard }, - { path: '/architecture', exact: true, name: 'Architecture', component: Structure }, - { path: '/architecture/structure', name: 'Structure', component: Structure }, - { path: '/architecture/file-types', name: 'File Types', component: FileTypes }, - { path: '/architecture/dependencies', name: 'Dependencies', component: Dependencies }, - { path: '/resource-management', exact: true, name: 'Resource Management', component: Activity }, - { path: '/resource-management/activity', name: 'Activity', component: Activity }, - { path: '/resource-management/knowledge-distribution', name: 'Knowledge Distribution', component: KnowledgeDistribution }, - { path: '/risk-management', exact: true, name: 'Risk Management', component: Hotspots }, - { path: '/risk-management/hotspots', name: 'Hotspots', component: Hotspots }, - { path: '/quality-management', exact: true, name: 'Quality Management', component: StaticCodeAnalysisPMD }, - { path: '/quality-management/static-code-analysis', name: 'Static Code Analysis', component: StaticCodeAnalysisPMD }, - { path: '/quality-management/static-code-analysis/pmd', name: 'PMD', component: StaticCodeAnalysisPMD }, - { path: '/quality-management/test-coverage', name: 'Test Coverage', component: TestCoverage }, - { path: '/settings', name: 'Settings', component: Settings }, - { path: '/custom-query', name: 'Custom Query', component: CustomQuery }, + { path: "/", exact: true, name: "Home", component: DefaultLayout }, + { path: "/dashboard", name: "Dashboard", component: Dashboard }, + { + path: "/architecture", + exact: true, + name: "Architecture", + component: Structure + }, + { + path: "/architecture/structure", + name: "Structure", + component: Structure + }, + { + path: "/architecture/file-types", + name: "File Types", + component: FileTypes + }, + { + path: "/architecture/dependencies", + name: "Dependencies", + component: Dependencies + }, + { path: "/architecture/layers", name: "Layers", component: Layers }, + { + path: "/resource-management", + exact: true, + name: "Resource Management", + component: Activity + }, + { + path: "/resource-management/activity", + name: "Activity", + component: Activity + }, + { + path: "/resource-management/knowledge-distribution", + name: "Knowledge Distribution", + component: KnowledgeDistribution + }, + { + path: "/risk-management", + exact: true, + name: "Risk Management", + component: Hotspots + }, + { + path: "/risk-management/hotspots", + name: "Hotspots", + component: Hotspots + }, + { + path: "/quality-management", + exact: true, + name: "Quality Management", + component: StaticCodeAnalysisPMD + }, + { + path: "/quality-management/static-code-analysis", + name: "Static Code Analysis", + component: StaticCodeAnalysisPMD + }, + { + path: "/quality-management/static-code-analysis/pmd", + name: "PMD", + component: StaticCodeAnalysisPMD + }, + { + path: "/quality-management/test-coverage", + name: "Test Coverage", + component: TestCoverage + }, + { path: "/settings", name: "Settings", component: Settings }, + { path: "/custom-query", name: "Custom Query", component: CustomQuery } ]; export default routes; diff --git a/src/views/Dashboard/Architecture/Dependencies/Dependencies.js b/src/views/Dashboard/Architecture/Dependencies/Dependencies.js index 995bacf..389b926 100644 --- a/src/views/Dashboard/Architecture/Dependencies/Dependencies.js +++ b/src/views/Dashboard/Architecture/Dependencies/Dependencies.js @@ -4,7 +4,6 @@ import CustomCardHeader from "../../CustomCardHeader/CustomCardHeader"; import DependencyChord from "./visualizations/DependencyChord"; import { CypherEditor } from "graph-app-kit/components/Editor"; import { Button, Row, Col, Card, CardBody } from "reactstrap"; - var AppDispatcher = require("../../../../AppDispatcher"); class ArchitectureDependencies extends DashboardAbstract { diff --git a/src/views/Dashboard/Architecture/Layers/Layers.js b/src/views/Dashboard/Architecture/Layers/Layers.js new file mode 100644 index 0000000..a6a08b0 --- /dev/null +++ b/src/views/Dashboard/Architecture/Layers/Layers.js @@ -0,0 +1,34 @@ +import React from "react"; +import DashboardAbstract from "../../AbstractDashboardComponent"; +import CustomCardHeader from "../../CustomCardHeader/CustomCardHeader"; +import { Card, CardBody, Col, Row } from "reactstrap"; +import LayersWrapper from "./LayersWrapper"; + +class Layers extends DashboardAbstract { + render() { + return ( +
+ + + + + + + + + + +
+ ); + } +} + +export default Layers; diff --git a/src/views/Dashboard/Architecture/Layers/LayersWrapper.js b/src/views/Dashboard/Architecture/Layers/LayersWrapper.js new file mode 100644 index 0000000..6d43368 --- /dev/null +++ b/src/views/Dashboard/Architecture/Layers/LayersWrapper.js @@ -0,0 +1,20 @@ +import React, { Component } from "react"; +import LayersVisualization from "./visualizations/LayersVisualization"; + +class DependencyArcWrapper extends Component { + componentDidMount() { + new LayersVisualization(this.refs.arc); + } + + render() { + return ( +
+
+
+
+
+ ); + } +} + +export default DependencyArcWrapper; diff --git a/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js b/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js new file mode 100644 index 0000000..4190d03 --- /dev/null +++ b/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js @@ -0,0 +1,81 @@ +import * as d3 from "d3"; + +const url = + "https://raw.githubusercontent.com/d3/d3-hierarchy/master/test/data/flare.json"; + +export default class LayersVisualization { + constructor(element) { + const width = 954; + + const tree = data => { + const root = d3.hierarchy(data); + root.dx = 10; + root.dy = width / (root.height + 1); + return d3.tree().nodeSize([root.dx, root.dy])(root); + }; + + d3.json(url).then(data => { + const root = tree(data); + console.log(root); + + let x0 = Infinity; + let x1 = -x0; + root.each(d => { + if (d.x > x1) x1 = d.x; + if (d.x < x0) x0 = d.x; + }); + + const svg = d3 + .select(element) + .append("svg") + .attr("viewBox", [0, 0, width, x1 - x0 + root.dx * 2]); + + const g = svg + .append("g") + .attr("font-family", "sans-serif") + .attr("font-size", 10) + .attr("transform", `translate(${root.dy / 3},${root.dx - x0})`); + + const link = g + .append("g") + .attr("fill", "none") + .attr("stroke", "#555") + .attr("stroke-opacity", 0.4) + .attr("stroke-width", 1.5) + .selectAll("path") + .data(root.links()) + .join("path") + .attr( + "d", + d3 + .linkHorizontal() + .x(d => d.y) + .y(d => d.x) + ); + + const node = g + .append("g") + .attr("stroke-linejoin", "round") + .attr("stroke-width", 3) + .selectAll("g") + .data(root.descendants()) + .join("g") + .attr("transform", d => `translate(${d.y},${d.x})`); + + node.append("circle") + .attr("fill", d => (d.children ? "#555" : "#999")) + .attr("r", 2.5); + + node.append("text") + .attr("dy", "0.31em") + .attr("x", d => (d.children ? -6 : 6)) + .attr("text-anchor", d => (d.children ? "end" : "start")) + .text(d => d.data.name) + .clone(true) + .lower() + .attr("stroke", "white"); + + return svg.node(); + }); + } +} From 2dacc0dd65b41e094036c21bfddccbdb45554b7d Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Wed, 15 Jul 2020 18:50:44 +0200 Subject: [PATCH 04/22] Sample visualization of collapsible tree --- .../visualizations/LayersVisualization.js | 204 +++++++++++++----- 1 file changed, 150 insertions(+), 54 deletions(-) diff --git a/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js b/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js index 4190d03..5490651 100644 --- a/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js +++ b/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js @@ -5,75 +5,171 @@ const url = export default class LayersVisualization { constructor(element) { + const margin = { top: 10, right: 120, bottom: 10, left: 40 }; const width = 954; + const dy = width / 6; + const dx = 10; + const tree = d3.tree().nodeSize([dx, dy]); + const diagonal = d3 + .linkHorizontal() + .x(d => d.y) + .y(d => d.x); - const tree = data => { + d3.json(url).then(data => { const root = d3.hierarchy(data); - root.dx = 10; - root.dy = width / (root.height + 1); - return d3.tree().nodeSize([root.dx, root.dy])(root); - }; - d3.json(url).then(data => { - const root = tree(data); - console.log(root); - - let x0 = Infinity; - let x1 = -x0; - root.each(d => { - if (d.x > x1) x1 = d.x; - if (d.x < x0) x0 = d.x; + root.x0 = dy / 2; + root.y0 = 0; + root.descendants().forEach((d, i) => { + d.id = i; + d._children = d.children; + if (d.depth && d.data.name.length !== 7) d.children = null; }); const svg = d3 .select(element) .append("svg") - .attr("viewBox", [0, 0, width, x1 - x0 + root.dx * 2]); - - const g = svg - .append("g") - .attr("font-family", "sans-serif") - .attr("font-size", 10) - .attr("transform", `translate(${root.dy / 3},${root.dx - x0})`); + .attr("viewBox", [-margin.left, -margin.top, width, dx]) + .style("font", "10px sans-serif") + .style("user-select", "none"); - const link = g + const gLink = svg .append("g") .attr("fill", "none") .attr("stroke", "#555") .attr("stroke-opacity", 0.4) - .attr("stroke-width", 1.5) - .selectAll("path") - .data(root.links()) - .join("path") - .attr( - "d", - d3 - .linkHorizontal() - .x(d => d.y) - .y(d => d.x) - ); - - const node = g + .attr("stroke-width", 1.5); + + const gNode = svg .append("g") - .attr("stroke-linejoin", "round") - .attr("stroke-width", 3) - .selectAll("g") - .data(root.descendants()) - .join("g") - .attr("transform", d => `translate(${d.y},${d.x})`); - - node.append("circle") - .attr("fill", d => (d.children ? "#555" : "#999")) - .attr("r", 2.5); - - node.append("text") - .attr("dy", "0.31em") - .attr("x", d => (d.children ? -6 : 6)) - .attr("text-anchor", d => (d.children ? "end" : "start")) - .text(d => d.data.name) - .clone(true) - .lower() - .attr("stroke", "white"); + .attr("cursor", "pointer") + .attr("pointer-events", "all"); + + function update(source) { + const duration = d3.event && d3.event.altKey ? 2500 : 250; + const nodes = root.descendants().reverse(); + const links = root.links(); + + // Compute the new tree layout. + tree(root); + + let left = root; + let right = root; + root.eachBefore(node => { + if (node.x < left.x) left = node; + if (node.x > right.x) right = node; + }); + + const height = right.x - left.x + margin.top + margin.bottom; + + const transition = svg + .transition() + .duration(duration) + .attr("viewBox", [ + -margin.left, + left.x - margin.top, + width, + height + ]) + .tween( + "resize", + window.ResizeObserver + ? null + : () => () => svg.dispatch("toggle") + ); + + // Update the nodes… + const node = gNode.selectAll("g").data(nodes, d => d.id); + + // Enter any new nodes at the parent's previous position. + const nodeEnter = node + .enter() + .append("g") + .attr( + "transform", + d => `translate(${source.y0},${source.x0})` + ) + .attr("fill-opacity", 0) + .attr("stroke-opacity", 0) + .on("click", d => { + d.children = d.children ? null : d._children; + update(d); + }); + + nodeEnter + .append("circle") + .attr("r", 2.5) + .attr("fill", d => (d._children ? "#555" : "#999")) + .attr("stroke-width", 10); + + nodeEnter + .append("text") + .attr("dy", "0.31em") + .attr("x", d => (d._children ? -6 : 6)) + .attr("text-anchor", d => (d._children ? "end" : "start")) + .text(d => d.data.name) + .clone(true) + .lower() + .attr("stroke-linejoin", "round") + .attr("stroke-width", 3) + .attr("stroke", "white"); + + // Transition nodes to their new position. + const nodeUpdate = node + .merge(nodeEnter) + .transition(transition) + .attr("transform", d => `translate(${d.y},${d.x})`) + .attr("fill-opacity", 1) + .attr("stroke-opacity", 1); + + // Transition exiting nodes to the parent's new position. + const nodeExit = node + .exit() + .transition(transition) + .remove() + .attr( + "transform", + d => `translate(${source.y},${source.x})` + ) + .attr("fill-opacity", 0) + .attr("stroke-opacity", 0); + + // Update the links… + const link = gLink + .selectAll("path") + .data(links, d => d.target.id); + + // Enter any new links at the parent's previous position. + const linkEnter = link + .enter() + .append("path") + .attr("d", d => { + const o = { x: source.x0, y: source.y0 }; + return diagonal({ source: o, target: o }); + }); + + // Transition links to their new position. + link.merge(linkEnter) + .transition(transition) + .attr("d", diagonal); + + // Transition exiting nodes to the parent's new position. + link.exit() + .transition(transition) + .remove() + .attr("d", d => { + const o = { x: source.x, y: source.y }; + return diagonal({ source: o, target: o }); + }); + + // Stash the old positions for transition. + root.eachBefore(d => { + d.x0 = d.x; + d.y0 = d.y; + }); + } + + update(root); return svg.node(); }); From b06c4d08f03edfd9ec864004a6f5ae6b24be3d36 Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Tue, 21 Jul 2020 11:39:50 +0200 Subject: [PATCH 05/22] Visualization of d3 indented tree --- src/api/models/LayersModel.js | 42 ++++ .../Dashboard/Architecture/Layers/Layers.js | 8 +- .../Architecture/Layers/LayersWrapper.js | 20 -- .../visualizations/CollapsibleIndentedTree.js | 124 ++++++++++++ .../visualizations/LayersVisualization.js | 190 +++--------------- 5 files changed, 195 insertions(+), 189 deletions(-) create mode 100644 src/api/models/LayersModel.js delete mode 100644 src/views/Dashboard/Architecture/Layers/LayersWrapper.js create mode 100644 src/views/Dashboard/Architecture/Layers/visualizations/CollapsibleIndentedTree.js diff --git a/src/api/models/LayersModel.js b/src/api/models/LayersModel.js new file mode 100644 index 0000000..6a5c4a1 --- /dev/null +++ b/src/api/models/LayersModel.js @@ -0,0 +1,42 @@ +import { neo4jSession } from "../../views/Dashboard/AbstractDashboardComponent"; + +class LayersModel { + constructor(props) { + const layersQuery = + "MATCH (l1:Directory)-[:CONTAINS]->(t1:Directory) " + + "WITH l1, collect(t1.name) AS Children " + + "WHERE l1.fqn = 'org.junit' " + + "RETURN l1.name AS Node, Children " + + "ORDER BY Node, Children"; + localStorage.setItem("layers_query", layersQuery); + + this.state = { + queryString: layersQuery + }; + } + + readLayers(visualization) { + let layersData = []; + neo4jSession + .run(this.state.queryString) + .then(result => { + result.records.forEach(record => { + let convertedRecord = { + name: record.get("Node"), + children: record.get("Children") + }; + layersData.push(convertedRecord); + }); + }) + .then(() => { + visualization.setState({ + layersData: layersData + }); + }) + .catch(error => { + console.log("Error: " + error); + }); + } +} + +export default LayersModel; diff --git a/src/views/Dashboard/Architecture/Layers/Layers.js b/src/views/Dashboard/Architecture/Layers/Layers.js index a6a08b0..eab983f 100644 --- a/src/views/Dashboard/Architecture/Layers/Layers.js +++ b/src/views/Dashboard/Architecture/Layers/Layers.js @@ -2,7 +2,7 @@ import React from "react"; import DashboardAbstract from "../../AbstractDashboardComponent"; import CustomCardHeader from "../../CustomCardHeader/CustomCardHeader"; import { Card, CardBody, Col, Row } from "reactstrap"; -import LayersWrapper from "./LayersWrapper"; +import LayersVisualization from "./visualizations/LayersVisualization"; class Layers extends DashboardAbstract { render() { @@ -21,7 +21,11 @@ class Layers extends DashboardAbstract { } /> - +
+
+ +
+
diff --git a/src/views/Dashboard/Architecture/Layers/LayersWrapper.js b/src/views/Dashboard/Architecture/Layers/LayersWrapper.js deleted file mode 100644 index 6d43368..0000000 --- a/src/views/Dashboard/Architecture/Layers/LayersWrapper.js +++ /dev/null @@ -1,20 +0,0 @@ -import React, { Component } from "react"; -import LayersVisualization from "./visualizations/LayersVisualization"; - -class DependencyArcWrapper extends Component { - componentDidMount() { - new LayersVisualization(this.refs.arc); - } - - render() { - return ( -
-
-
-
-
- ); - } -} - -export default DependencyArcWrapper; diff --git a/src/views/Dashboard/Architecture/Layers/visualizations/CollapsibleIndentedTree.js b/src/views/Dashboard/Architecture/Layers/visualizations/CollapsibleIndentedTree.js new file mode 100644 index 0000000..3cfd616 --- /dev/null +++ b/src/views/Dashboard/Architecture/Layers/visualizations/CollapsibleIndentedTree.js @@ -0,0 +1,124 @@ +import React, { Component } from "react"; +import * as d3 from "d3"; + +const url = + "https://raw.githubusercontent.com/d3/d3-hierarchy/master/test/data/flare.json"; + +export default class CollapsibleIndentedTree extends Component { + constructor(props) { + super(props); + } + + componentDidMount() { + this.createVisualization(); + } + + createVisualization() { + const nodeSize = 17; + const width = 1100; + + d3.json(url).then(data => { + let i = 0; + const root = d3.hierarchy(data).eachBefore(d => (d.index = i++)); + const format = d3.format(","); + + const columns = [ + { + label: "Size", + value: d => d.value, + format, + x: 280 + }, + { + label: "Count", + value: d => (d.children ? 0 : 1), + format: (value, d) => (d.children ? format(value) : "-"), + x: 340 + } + ]; + + const nodes = root.descendants(); + + const svg = d3 + .select(this.refs.vis) + .attr("font-family", "sans-serif") + .attr("font-size", 10) + .style("overflow", "visible"); + + const link = svg + .append("g") + .attr("fill", "none") + .attr("stroke", "#999") + .selectAll("path") + .data(root.links()) + .join("path") + .attr( + "d", + d => ` + M${d.source.depth * nodeSize},${d.source.index * nodeSize} + V${d.target.index * nodeSize} + h${nodeSize} + ` + ); + + const node = svg + .append("g") + .selectAll("g") + .data(nodes) + .join("g") + .attr("transform", d => `translate(0,${d.index * nodeSize})`); + + node.append("circle") + .attr("cx", d => d.depth * nodeSize) + .attr("r", 2.5) + .attr("fill", d => (d.children ? null : "#999")); + + node.append("text") + .attr("dy", "0.32em") + .attr("x", d => d.depth * nodeSize + 6) + .text(d => d.data.name); + + node.append("title").text(d => + d + .ancestors() + .reverse() + .map(d => d.data.name) + .join("/") + ); + + for (const { label, value, format, x } of columns) { + svg.append("text") + .attr("dy", "0.32em") + .attr("y", -nodeSize) + .attr("x", x) + .attr("text-anchor", "end") + .attr("font-weight", "bold") + .text(label); + + node.append("text") + .attr("dy", "0.32em") + .attr("x", x) + .attr("text-anchor", "end") + .attr("fill", d => (d.children ? null : "#555")) + .data( + root + .copy() + .sum(value) + .descendants() + ) + .text(d => format(d.value, d)); + } + }); + } + + render() { + return ( + + ); + } +} diff --git a/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js b/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js index 5490651..b4ad6cd 100644 --- a/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js +++ b/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js @@ -1,177 +1,33 @@ +import React from "react"; import * as d3 from "d3"; +import DashboardAbstract, { + databaseCredentialsProvided +} from "../../../AbstractDashboardComponent"; +import LayersModel from "../../../../../api/models/LayersModel"; +import CollapsibleIndentedTree from "./CollapsibleIndentedTree"; const url = "https://raw.githubusercontent.com/d3/d3-hierarchy/master/test/data/flare.json"; -export default class LayersVisualization { - constructor(element) { - const margin = { top: 10, right: 120, bottom: 10, left: 40 }; - const width = 954; - const dy = width / 6; - const dx = 10; - const tree = d3.tree().nodeSize([dx, dy]); - const diagonal = d3 - .linkHorizontal() - .x(d => d.y) - .y(d => d.x); +class LayersVisualization extends DashboardAbstract { + constructor(props) { + super(props); - d3.json(url).then(data => { - const root = d3.hierarchy(data); - - root.x0 = dy / 2; - root.y0 = 0; - root.descendants().forEach((d, i) => { - d.id = i; - d._children = d.children; - if (d.depth && d.data.name.length !== 7) d.children = null; - }); - - const svg = d3 - .select(element) - .append("svg") - .attr("viewBox", [-margin.left, -margin.top, width, dx]) - .style("font", "10px sans-serif") - .style("user-select", "none"); - - const gLink = svg - .append("g") - .attr("fill", "none") - .attr("stroke", "#555") - .attr("stroke-opacity", 0.4) - .attr("stroke-width", 1.5); - - const gNode = svg - .append("g") - .attr("cursor", "pointer") - .attr("pointer-events", "all"); - - function update(source) { - const duration = d3.event && d3.event.altKey ? 2500 : 250; - const nodes = root.descendants().reverse(); - const links = root.links(); - - // Compute the new tree layout. - tree(root); - - let left = root; - let right = root; - root.eachBefore(node => { - if (node.x < left.x) left = node; - if (node.x > right.x) right = node; - }); - - const height = right.x - left.x + margin.top + margin.bottom; - - const transition = svg - .transition() - .duration(duration) - .attr("viewBox", [ - -margin.left, - left.x - margin.top, - width, - height - ]) - .tween( - "resize", - window.ResizeObserver - ? null - : () => () => svg.dispatch("toggle") - ); - - // Update the nodes… - const node = gNode.selectAll("g").data(nodes, d => d.id); - - // Enter any new nodes at the parent's previous position. - const nodeEnter = node - .enter() - .append("g") - .attr( - "transform", - d => `translate(${source.y0},${source.x0})` - ) - .attr("fill-opacity", 0) - .attr("stroke-opacity", 0) - .on("click", d => { - d.children = d.children ? null : d._children; - update(d); - }); - - nodeEnter - .append("circle") - .attr("r", 2.5) - .attr("fill", d => (d._children ? "#555" : "#999")) - .attr("stroke-width", 10); - - nodeEnter - .append("text") - .attr("dy", "0.31em") - .attr("x", d => (d._children ? -6 : 6)) - .attr("text-anchor", d => (d._children ? "end" : "start")) - .text(d => d.data.name) - .clone(true) - .lower() - .attr("stroke-linejoin", "round") - .attr("stroke-width", 3) - .attr("stroke", "white"); - - // Transition nodes to their new position. - const nodeUpdate = node - .merge(nodeEnter) - .transition(transition) - .attr("transform", d => `translate(${d.y},${d.x})`) - .attr("fill-opacity", 1) - .attr("stroke-opacity", 1); - - // Transition exiting nodes to the parent's new position. - const nodeExit = node - .exit() - .transition(transition) - .remove() - .attr( - "transform", - d => `translate(${source.y},${source.x})` - ) - .attr("fill-opacity", 0) - .attr("stroke-opacity", 0); - - // Update the links… - const link = gLink - .selectAll("path") - .data(links, d => d.target.id); - - // Enter any new links at the parent's previous position. - const linkEnter = link - .enter() - .append("path") - .attr("d", d => { - const o = { x: source.x0, y: source.y0 }; - return diagonal({ source: o, target: o }); - }); - - // Transition links to their new position. - link.merge(linkEnter) - .transition(transition) - .attr("d", diagonal); - - // Transition exiting nodes to the parent's new position. - link.exit() - .transition(transition) - .remove() - .attr("d", d => { - const o = { x: source.x, y: source.y }; - return diagonal({ source: o, target: o }); - }); - - // Stash the old positions for transition. - root.eachBefore(d => { - d.x0 = d.x; - d.y0 = d.y; - }); - } + this.state = { + layersData: [] + }; + } - update(root); + componentDidMount() { + if (databaseCredentialsProvided) { + let layersModel = new LayersModel(); + layersModel.readLayers(this); + } + } - return svg.node(); - }); + render() { + return ; } } + +export default LayersVisualization; From edffb3c1615306e315e3cb1822a1a62603c606a7 Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Tue, 21 Jul 2020 16:02:26 +0200 Subject: [PATCH 06/22] visualization with react-d3-tree --- package.json | 1 + src/api/models/LayersModel.js | 2 + .../Dashboard/Architecture/Layers/Layers.js | 2 +- .../visualizations/CollapsibleIndentedTree.js | 124 ------------------ .../visualizations/LayersVisualization.js | 23 ++-- 5 files changed, 17 insertions(+), 135 deletions(-) delete mode 100644 src/views/Dashboard/Architecture/Layers/visualizations/CollapsibleIndentedTree.js diff --git a/package.json b/package.json index b241d7d..d9a58af 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "react-app-polyfill": "^0.2.2", "react-bootstrap-daterangepicker": "^4.1.0", "react-chartjs-2": "^2.7.4", + "react-d3-tree": "^1.16.1", "react-dom": "^16.8.5", "react-load-script": "0.0.6", "react-loadable": "^5.5.0", diff --git a/src/api/models/LayersModel.js b/src/api/models/LayersModel.js index 6a5c4a1..8bb52de 100644 --- a/src/api/models/LayersModel.js +++ b/src/api/models/LayersModel.js @@ -15,6 +15,8 @@ class LayersModel { }; } + getData() {} + readLayers(visualization) { let layersData = []; neo4jSession diff --git a/src/views/Dashboard/Architecture/Layers/Layers.js b/src/views/Dashboard/Architecture/Layers/Layers.js index eab983f..52836b6 100644 --- a/src/views/Dashboard/Architecture/Layers/Layers.js +++ b/src/views/Dashboard/Architecture/Layers/Layers.js @@ -22,7 +22,7 @@ class Layers extends DashboardAbstract { />
-
+
diff --git a/src/views/Dashboard/Architecture/Layers/visualizations/CollapsibleIndentedTree.js b/src/views/Dashboard/Architecture/Layers/visualizations/CollapsibleIndentedTree.js deleted file mode 100644 index 3cfd616..0000000 --- a/src/views/Dashboard/Architecture/Layers/visualizations/CollapsibleIndentedTree.js +++ /dev/null @@ -1,124 +0,0 @@ -import React, { Component } from "react"; -import * as d3 from "d3"; - -const url = - "https://raw.githubusercontent.com/d3/d3-hierarchy/master/test/data/flare.json"; - -export default class CollapsibleIndentedTree extends Component { - constructor(props) { - super(props); - } - - componentDidMount() { - this.createVisualization(); - } - - createVisualization() { - const nodeSize = 17; - const width = 1100; - - d3.json(url).then(data => { - let i = 0; - const root = d3.hierarchy(data).eachBefore(d => (d.index = i++)); - const format = d3.format(","); - - const columns = [ - { - label: "Size", - value: d => d.value, - format, - x: 280 - }, - { - label: "Count", - value: d => (d.children ? 0 : 1), - format: (value, d) => (d.children ? format(value) : "-"), - x: 340 - } - ]; - - const nodes = root.descendants(); - - const svg = d3 - .select(this.refs.vis) - .attr("font-family", "sans-serif") - .attr("font-size", 10) - .style("overflow", "visible"); - - const link = svg - .append("g") - .attr("fill", "none") - .attr("stroke", "#999") - .selectAll("path") - .data(root.links()) - .join("path") - .attr( - "d", - d => ` - M${d.source.depth * nodeSize},${d.source.index * nodeSize} - V${d.target.index * nodeSize} - h${nodeSize} - ` - ); - - const node = svg - .append("g") - .selectAll("g") - .data(nodes) - .join("g") - .attr("transform", d => `translate(0,${d.index * nodeSize})`); - - node.append("circle") - .attr("cx", d => d.depth * nodeSize) - .attr("r", 2.5) - .attr("fill", d => (d.children ? null : "#999")); - - node.append("text") - .attr("dy", "0.32em") - .attr("x", d => d.depth * nodeSize + 6) - .text(d => d.data.name); - - node.append("title").text(d => - d - .ancestors() - .reverse() - .map(d => d.data.name) - .join("/") - ); - - for (const { label, value, format, x } of columns) { - svg.append("text") - .attr("dy", "0.32em") - .attr("y", -nodeSize) - .attr("x", x) - .attr("text-anchor", "end") - .attr("font-weight", "bold") - .text(label); - - node.append("text") - .attr("dy", "0.32em") - .attr("x", x) - .attr("text-anchor", "end") - .attr("fill", d => (d.children ? null : "#555")) - .data( - root - .copy() - .sum(value) - .descendants() - ) - .text(d => format(d.value, d)); - } - }); - } - - render() { - return ( - - ); - } -} diff --git a/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js b/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js index b4ad6cd..041dfe6 100644 --- a/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js +++ b/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js @@ -1,32 +1,35 @@ import React from "react"; -import * as d3 from "d3"; import DashboardAbstract, { databaseCredentialsProvided } from "../../../AbstractDashboardComponent"; import LayersModel from "../../../../../api/models/LayersModel"; -import CollapsibleIndentedTree from "./CollapsibleIndentedTree"; +import { Tree } from "react-d3-tree"; const url = "https://raw.githubusercontent.com/d3/d3-hierarchy/master/test/data/flare.json"; +const styles = {}; + class LayersVisualization extends DashboardAbstract { constructor(props) { super(props); - this.state = { - layersData: [] + data: null }; } - componentDidMount() { - if (databaseCredentialsProvided) { - let layersModel = new LayersModel(); - layersModel.readLayers(this); - } + async componentDidMount() { + await fetch(url) + .then(res => res.json()) + .then(data => this.setState({ data: data })); } render() { - return ; + if (this.state.data == null) { + return

Loading...

; + } else { + return ; + } } } From f9db22fe9a47547dfde6c00c922b6a8394aae379 Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Wed, 5 Aug 2020 16:05:18 +0200 Subject: [PATCH 07/22] nivo network layer visualization without dependencies --- data/petclinic/Dockerfile | 4 +- package.json | 1 + src/api/models/LayersModel.js | 114 +++++++++++++----- .../visualizations/DependencyChord.js | 1 + .../Dashboard/Architecture/Layers/Layers.js | 107 +++++++++++++--- .../Layers/TreebeardCustomTheme.js | 76 ++++++++++++ .../visualizations/LayersVisualization.js | 36 ------ 7 files changed, 254 insertions(+), 85 deletions(-) create mode 100644 src/views/Dashboard/Architecture/Layers/TreebeardCustomTheme.js delete mode 100644 src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js diff --git a/data/petclinic/Dockerfile b/data/petclinic/Dockerfile index 1749182..3683a97 100644 --- a/data/petclinic/Dockerfile +++ b/data/petclinic/Dockerfile @@ -1,4 +1,4 @@ -FROM neo4j:latest +FROM neo4j:3.5 ENV NEO4J_PASSWD neo4j ENV NEO4J_AUTH neo4j/${NEO4J_PASSWD} @@ -12,4 +12,4 @@ CMD sed -e 's/^#dbms.read_only=.*$/dbms.read_only=true/' -i /var/lib/neo4j/conf/ bin/neo4j-admin set-initial-password ${NEO4J_PASSWD} || true && \ bin/neo4j-admin load --from=/var/lib/neo4j/import/petclinic.dump --force && \ bin/neo4j start && sleep 5 && \ - tail -f logs/neo4j.log \ No newline at end of file + tail -f logs/neo4j.log diff --git a/package.json b/package.json index d9a58af..3e8bd1c 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@nivo/chord": "^0.52.0", "@nivo/circle-packing": "^0.52.0", "@nivo/legends": "^0.52.0", + "@nivo/network": "^0.62.0", "@nivo/pie": "^0.52.0", "@nivo/radar": "^0.52.0", "@nivo/treemap": "^0.52.0", diff --git a/src/api/models/LayersModel.js b/src/api/models/LayersModel.js index 8bb52de..d054a8a 100644 --- a/src/api/models/LayersModel.js +++ b/src/api/models/LayersModel.js @@ -2,42 +2,96 @@ import { neo4jSession } from "../../views/Dashboard/AbstractDashboardComponent"; class LayersModel { constructor(props) { - const layersQuery = - "MATCH (l1:Directory)-[:CONTAINS]->(t1:Directory) " + - "WITH l1, collect(t1.name) AS Children " + - "WHERE l1.fqn = 'org.junit' " + - "RETURN l1.name AS Node, Children " + - "ORDER BY Node, Children"; - localStorage.setItem("layers_query", layersQuery); + const networkQuery = + "MATCH (package:Package)-[:CONTAINS]->(layer:Layer), (layer)-[:CONTAINS]->(dependent:Type) " + + "RETURN package.name, layer.name, collect(dependent.name) as dependents"; + const dependencyQuery = + "MATCH (:Layer)-[:CONTAINS]->(dependent:Type)-[:DEPENDS_ON]->(dependency:Type)<-[:CONTAINS]-(:Layer) " + + "RETURN dependent.name, collect(dependency.name) as dependencies"; this.state = { - queryString: layersQuery + networkQuery: networkQuery, + dependencyQuery: dependencyQuery }; } - getData() {} - - readLayers(visualization) { - let layersData = []; - neo4jSession - .run(this.state.queryString) - .then(result => { - result.records.forEach(record => { - let convertedRecord = { - name: record.get("Node"), - children: record.get("Children") - }; - layersData.push(convertedRecord); - }); - }) - .then(() => { - visualization.setState({ - layersData: layersData - }); - }) - .catch(error => { - console.log("Error: " + error); + readLayers(thisBackup) { + let nodes = []; + let links = []; + + neo4jSession.run(this.state.networkQuery).then(result => { + console.log(result); + result.records.forEach(record => { + if (!this.nodeExists(nodes, record)) { + this.appendNode( + nodes, + record.get("package.name"), + 16, + 1, + "rgb(108,121,241)" + ); + } + if (!nodes.includes(record.get("layer.name"))) { + this.appendNode( + nodes, + record.get("layer.name"), + 12, + 1, + "rgb(97, 205, 187)" + ); + this.appendLink( + links, + record.get("package.name"), + record.get("layer.name") + ); + record.get("dependents").forEach(dependent => { + if (!nodes.includes(dependent)) { + this.appendNode( + nodes, + dependent, + 4, + 2, + "rgb(232, 193, 160)" + ); + this.appendLink( + links, + record.get("layer.name"), + dependent + ); + } + }); + } + }); + }); + + neo4jSession.run(this.state.dependencyQuery).then(() => { + console.log(nodes, links); + thisBackup.setState({ + nodes: nodes, + links: links }); + }); + } + + appendLink(links, source, target) { + links.push({ + source: source, + target: target, + distance: 100 + }); + } + + appendNode(nodes, id, radius, depth, color) { + nodes.push({ + id: id, + radius: radius, + depth: depth, + color: color + }); + } + + nodeExists(nodes, record) { + return nodes.some(node => node.id === record.get("package.name")); } } diff --git a/src/views/Dashboard/Architecture/Dependencies/visualizations/DependencyChord.js b/src/views/Dashboard/Architecture/Dependencies/visualizations/DependencyChord.js index 45f820d..6fb7dfe 100644 --- a/src/views/Dashboard/Architecture/Dependencies/visualizations/DependencyChord.js +++ b/src/views/Dashboard/Architecture/Dependencies/visualizations/DependencyChord.js @@ -39,6 +39,7 @@ class DependencyChord extends DashboardAbstract { } render() { + console.log(this.state); var redirect = super.render(); if (redirect.length > 0) { return redirect; diff --git a/src/views/Dashboard/Architecture/Layers/Layers.js b/src/views/Dashboard/Architecture/Layers/Layers.js index 52836b6..2ada2d4 100644 --- a/src/views/Dashboard/Architecture/Layers/Layers.js +++ b/src/views/Dashboard/Architecture/Layers/Layers.js @@ -1,29 +1,102 @@ import React from "react"; -import DashboardAbstract from "../../AbstractDashboardComponent"; -import CustomCardHeader from "../../CustomCardHeader/CustomCardHeader"; -import { Card, CardBody, Col, Row } from "reactstrap"; -import LayersVisualization from "./visualizations/LayersVisualization"; +import DashboardAbstract, { + databaseCredentialsProvided +} from "../../AbstractDashboardComponent"; +import { Col, Row, Card, CardBody } from "reactstrap"; +import HotspotModel from "../../../../api/models/Hotspots"; +import { Treebeard } from "react-treebeard"; +import LayersModel from "../../../../api/models/LayersModel"; +import { ResponsiveNetwork } from "@nivo/network"; + +const treebeardCustomTheme = require("./TreebeardCustomTheme"); class Layers extends DashboardAbstract { + constructor(props) { + super(props); + this.state = { + layersData: {}, + nodes: [], + links: [] + }; + + this.onToggle = this.onToggle.bind(this); + } + + componentDidMount() { + if (databaseCredentialsProvided) { + let hotspotModel = new HotspotModel(); + hotspotModel.readHotspots("projectName").then(data => { + this.setState({ layersData: data.hierarchicalData }); + }); + + let layersModel = new LayersModel(); + layersModel.readLayers(this); + } + } + + onToggle(node, toggled) { + if (this.state.cursor) { + this.state.cursor.active = false; + } + node.active = true; + if (node.children) { + node.toggled = toggled; + } + this.setState({ cursor: node }); + } + render() { + console.log(this.state); + + if (this.state.nodes.length === 0) { + return

Loading...

; + } + return (
- + + + + + + + + - -
-
- +
+
+ { + return t.color; + }} + nodeBorderWidth={2} + nodeBorderColor={{ + from: "color", + modifiers: [["darker", 1.2]] + }} + linkThickness={function(t) { + return 2 * (2 - t.source.depth); + }} + motionStiffness={160} + motionDamping={10} + />
diff --git a/src/views/Dashboard/Architecture/Layers/TreebeardCustomTheme.js b/src/views/Dashboard/Architecture/Layers/TreebeardCustomTheme.js new file mode 100644 index 0000000..9c62bc1 --- /dev/null +++ b/src/views/Dashboard/Architecture/Layers/TreebeardCustomTheme.js @@ -0,0 +1,76 @@ +export default { + tree: { + base: { + listStyle: "none", + backgroundColor: "#FFFFFF", + margin: "0px", + padding: "0px", + color: "#151b1e" + }, + node: { + base: { + position: "relative" + }, + link: { + cursor: "pointer", + position: "relative", + padding: "0px 5px", + display: "block" + }, + activeLink: { + background: "#EEEEEE", + border: "1px solid #DDDDDD", + borderRadius: "3px" + }, + toggle: { + base: { + position: "relative", + display: "inline-block", + verticalAlign: "top", + marginLeft: "-5px", + height: "24px", + width: "24px" + }, + wrapper: { + position: "absolute", + top: "50%", + left: "56%", + margin: "-11px 0px 0px -7px", + height: "10px" + }, + height: 12, + width: 12, + arrow: { + fill: "#9DA5AB", + strokeWidth: "0px" + } + }, + header: { + base: { + display: "inline-block", + verticalAlign: "middle", + color: "#151b1e" + }, + connector: { + width: "2px", + height: "12px", + borderLeft: "solid 2px black", + borderBottom: "solid 2px black", + position: "absolute", + top: "0px", + left: "-21px" + }, + title: { + verticalAlign: "middle" + } + }, + subtree: { + listStyle: "none", + paddingLeft: "19px" + }, + loading: { + color: "#E2C089" + } + } + } +}; diff --git a/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js b/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js deleted file mode 100644 index 041dfe6..0000000 --- a/src/views/Dashboard/Architecture/Layers/visualizations/LayersVisualization.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from "react"; -import DashboardAbstract, { - databaseCredentialsProvided -} from "../../../AbstractDashboardComponent"; -import LayersModel from "../../../../../api/models/LayersModel"; -import { Tree } from "react-d3-tree"; - -const url = - "https://raw.githubusercontent.com/d3/d3-hierarchy/master/test/data/flare.json"; - -const styles = {}; - -class LayersVisualization extends DashboardAbstract { - constructor(props) { - super(props); - this.state = { - data: null - }; - } - - async componentDidMount() { - await fetch(url) - .then(res => res.json()) - .then(data => this.setState({ data: data })); - } - - render() { - if (this.state.data == null) { - return

Loading...

; - } else { - return ; - } - } -} - -export default LayersVisualization; From d0e8f3be5bffa1f2a46ee912b6c03caecdfc37b4 Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Wed, 5 Aug 2020 19:09:11 +0200 Subject: [PATCH 08/22] Implement visualization - nivo/network with dependencies between classes --- src/api/models/LayersModel.js | 38 +++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/api/models/LayersModel.js b/src/api/models/LayersModel.js index d054a8a..346015f 100644 --- a/src/api/models/LayersModel.js +++ b/src/api/models/LayersModel.js @@ -22,7 +22,7 @@ class LayersModel { neo4jSession.run(this.state.networkQuery).then(result => { console.log(result); result.records.forEach(record => { - if (!this.nodeExists(nodes, record)) { + if (!this.nodeExists(nodes, record.get("package.name"))) { this.appendNode( nodes, record.get("package.name"), @@ -31,7 +31,7 @@ class LayersModel { "rgb(108,121,241)" ); } - if (!nodes.includes(record.get("layer.name"))) { + if (!this.nodeExists(nodes, record.get("layer.name"))) { this.appendNode( nodes, record.get("layer.name"), @@ -45,12 +45,12 @@ class LayersModel { record.get("layer.name") ); record.get("dependents").forEach(dependent => { - if (!nodes.includes(dependent)) { + if (!this.nodeExists(nodes, dependent)) { this.appendNode( nodes, dependent, 4, - 2, + 1, "rgb(232, 193, 160)" ); this.appendLink( @@ -64,20 +64,32 @@ class LayersModel { }); }); - neo4jSession.run(this.state.dependencyQuery).then(() => { - console.log(nodes, links); - thisBackup.setState({ - nodes: nodes, - links: links + neo4jSession + .run(this.state.dependencyQuery) + .then(result => { + console.log(nodes, links); + console.log(result); + result.records.forEach(record => { + console.log(record); + let dependent = record.get("dependent.name"); + record.get("dependencies").forEach(dependency => { + this.appendLink(links, dependent, dependency); + }); + }); + }) + .then(() => { + thisBackup.setState({ + nodes: nodes, + links: links + }); }); - }); } appendLink(links, source, target) { links.push({ source: source, target: target, - distance: 100 + distance: 50 }); } @@ -90,8 +102,8 @@ class LayersModel { }); } - nodeExists(nodes, record) { - return nodes.some(node => node.id === record.get("package.name")); + nodeExists(nodes, nodeId) { + return nodes.some(node => node.id === nodeId); } } From 775323ef4e42a28c0b8f340b4a870f1193d52d9c Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Sat, 8 Aug 2020 15:15:13 +0200 Subject: [PATCH 09/22] Current state --- package.json | 1 - src/api/models/Hotspots.js | 1 + src/api/models/LayersModel.js | 138 ++++++++---------- .../Dashboard/Architecture/Layers/Layers.js | 99 ++++--------- .../Layers/TreebeardCustomTheme.js | 2 + 5 files changed, 87 insertions(+), 154 deletions(-) diff --git a/package.json b/package.json index 3e8bd1c..d9a58af 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "@nivo/chord": "^0.52.0", "@nivo/circle-packing": "^0.52.0", "@nivo/legends": "^0.52.0", - "@nivo/network": "^0.62.0", "@nivo/pie": "^0.52.0", "@nivo/radar": "^0.52.0", "@nivo/treemap": "^0.52.0", diff --git a/src/api/models/Hotspots.js b/src/api/models/Hotspots.js index 1a29e7c..e22ca00 100644 --- a/src/api/models/Hotspots.js +++ b/src/api/models/Hotspots.js @@ -109,6 +109,7 @@ class HotspotModel { } }); + console.log(flatData); hierarchicalData = cpHelper.circlePackingByName( projectName, flatData diff --git a/src/api/models/LayersModel.js b/src/api/models/LayersModel.js index 346015f..4d1be1e 100644 --- a/src/api/models/LayersModel.js +++ b/src/api/models/LayersModel.js @@ -1,109 +1,89 @@ import { neo4jSession } from "../../views/Dashboard/AbstractDashboardComponent"; +import * as d3 from "d3"; class LayersModel { constructor(props) { - const networkQuery = - "MATCH (package:Package)-[:CONTAINS]->(layer:Layer), (layer)-[:CONTAINS]->(dependent:Type) " + - "RETURN package.name, layer.name, collect(dependent.name) as dependents"; - const dependencyQuery = - "MATCH (:Layer)-[:CONTAINS]->(dependent:Type)-[:DEPENDS_ON]->(dependency:Type)<-[:CONTAINS]-(:Layer) " + - "RETURN dependent.name, collect(dependency.name) as dependencies"; + const layersQuery = + "MATCH (package:Package)-[:CONTAINS]->(layer:Layer), (layer)-[:CONTAINS]->(dependent:Type), (dependent)-[:DECLARES]->(method:Method) " + + "RETURN package.name, layer.name, dependent.name, sum(method.effectiveLineCount) as loc"; this.state = { - networkQuery: networkQuery, - dependencyQuery: dependencyQuery + layersQuery: layersQuery }; } - readLayers(thisBackup) { - let nodes = []; - let links = []; - - neo4jSession.run(this.state.networkQuery).then(result => { + readLayers(that) { + neo4jSession.run(this.state.layersQuery).then(result => { console.log(result); result.records.forEach(record => { - if (!this.nodeExists(nodes, record.get("package.name"))) { + if (!this.hasNode(root, record.get("package.name"))) { + root.name = record.get("package.name"); + root.color = "hsl(228, 70%, 50%)"; + root.children = []; + } + if (!this.hasNode(root, record.get("layer.name"))) { this.appendNode( - nodes, + root, record.get("package.name"), - 16, - 1, - "rgb(108,121,241)" + this.createNode( + record.get("layer.name"), + "hsl(111, 70%, 50%)" + ) ); - } - if (!this.nodeExists(nodes, record.get("layer.name"))) { this.appendNode( - nodes, + root, record.get("layer.name"), - 12, - 1, - "rgb(97, 205, 187)" + this.createNode( + record.get("dependent.name"), + "hsl(204, 70%, 50%)" + ) ); - this.appendLink( - links, - record.get("package.name"), - record.get("layer.name") - ); - record.get("dependents").forEach(dependent => { - if (!this.nodeExists(nodes, dependent)) { - this.appendNode( - nodes, - dependent, - 4, - 1, - "rgb(232, 193, 160)" - ); - this.appendLink( - links, - record.get("layer.name"), - dependent - ); - } - }); } }); + console.log(root); }); + } - neo4jSession - .run(this.state.dependencyQuery) - .then(result => { - console.log(nodes, links); - console.log(result); - result.records.forEach(record => { - console.log(record); - let dependent = record.get("dependent.name"); - record.get("dependencies").forEach(dependency => { - this.appendLink(links, dependent, dependency); - }); - }); - }) - .then(() => { - thisBackup.setState({ - nodes: nodes, - links: links - }); - }); + hasNode(root, name) { + if (this.isEmpty(root)) return false; + if (root.name === name) return true; + for (let child of root.children) { + if (child.name === name) { + return true; + } + } } - appendLink(links, source, target) { - links.push({ - source: source, - target: target, - distance: 50 - }); + isEmpty(root) { + return Object.keys(root).length === 0 && root.constructor === Object; } - appendNode(nodes, id, radius, depth, color) { - nodes.push({ - id: id, - radius: radius, - depth: depth, - color: color - }); + createNode(name, color) { + return { + name: name, + color: color, + children: [] + }; + } + + appendNode(root, parent, child) { + debugger; + const parentNode = this.findParent(root, parent); + console.log(parentNode); + parentNode.children.push(child); } - nodeExists(nodes, nodeId) { - return nodes.some(node => node.id === nodeId); + findParent(root, parentName) { + if (root.name === parentName) { + return parentName; + } + debugger; + let result; + for (let child of root.children) { + console.log(child); + // result = this.findParent(child.children, name) + // if (result) return result; + } } } diff --git a/src/views/Dashboard/Architecture/Layers/Layers.js b/src/views/Dashboard/Architecture/Layers/Layers.js index 2ada2d4..bbe6cbc 100644 --- a/src/views/Dashboard/Architecture/Layers/Layers.js +++ b/src/views/Dashboard/Architecture/Layers/Layers.js @@ -2,103 +2,54 @@ import React from "react"; import DashboardAbstract, { databaseCredentialsProvided } from "../../AbstractDashboardComponent"; -import { Col, Row, Card, CardBody } from "reactstrap"; -import HotspotModel from "../../../../api/models/Hotspots"; -import { Treebeard } from "react-treebeard"; +import { Button, Row, Col, Card, CardBody } from "reactstrap"; +import { ResponsiveBubble } from "@nivo/circle-packing"; import LayersModel from "../../../../api/models/LayersModel"; -import { ResponsiveNetwork } from "@nivo/network"; - -const treebeardCustomTheme = require("./TreebeardCustomTheme"); class Layers extends DashboardAbstract { constructor(props) { super(props); this.state = { - layersData: {}, - nodes: [], - links: [] + data: {} }; - - this.onToggle = this.onToggle.bind(this); } componentDidMount() { - if (databaseCredentialsProvided) { - let hotspotModel = new HotspotModel(); - hotspotModel.readHotspots("projectName").then(data => { - this.setState({ layersData: data.hierarchicalData }); - }); + super.componentDidMount(); - let layersModel = new LayersModel(); + if (databaseCredentialsProvided) { + const layersModel = new LayersModel(); layersModel.readLayers(this); } } - onToggle(node, toggled) { - if (this.state.cursor) { - this.state.cursor.active = false; - } - node.active = true; - if (node.children) { - node.toggled = toggled; - } - this.setState({ cursor: node }); - } - render() { console.log(this.state); - if (this.state.nodes.length === 0) { - return

Loading...

; - } - return (
- - - - - - - - + -
-
- { - return t.color; - }} - nodeBorderWidth={2} - nodeBorderColor={{ - from: "color", - modifiers: [["darker", 1.2]] - }} - linkThickness={function(t) { - return 2 * (2 - t.source.depth); - }} - motionStiffness={160} - motionDamping={10} - /> -
-
+ + + + +
+ +
+
+
+ +
diff --git a/src/views/Dashboard/Architecture/Layers/TreebeardCustomTheme.js b/src/views/Dashboard/Architecture/Layers/TreebeardCustomTheme.js index 9c62bc1..a3b9bd6 100644 --- a/src/views/Dashboard/Architecture/Layers/TreebeardCustomTheme.js +++ b/src/views/Dashboard/Architecture/Layers/TreebeardCustomTheme.js @@ -1,3 +1,5 @@ +//'use strict'; + export default { tree: { base: { From 2af20f782edd0605c7dc59f0ae46d8ed90377737 Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Tue, 11 Aug 2020 12:17:48 +0200 Subject: [PATCH 10/22] Change color scheme --- src/api/models/Hotspots.js | 3 +- src/api/models/LayersModel.js | 135 ++++++++++-------- .../Dashboard/Architecture/Layers/Layers.js | 33 ++++- .../Architecture/Structure/Structure.js | 2 + 4 files changed, 109 insertions(+), 64 deletions(-) diff --git a/src/api/models/Hotspots.js b/src/api/models/Hotspots.js index e22ca00..ddca7f9 100644 --- a/src/api/models/Hotspots.js +++ b/src/api/models/Hotspots.js @@ -114,8 +114,9 @@ class HotspotModel { projectName, flatData ); + console.log(hierarchicalData); cpHelper.normalizeHotspots(hierarchicalData); //this function works by reference - + console.log(hierarchicalData); neo4jSession.close(); //normalize the root element diff --git a/src/api/models/LayersModel.js b/src/api/models/LayersModel.js index 4d1be1e..da5718d 100644 --- a/src/api/models/LayersModel.js +++ b/src/api/models/LayersModel.js @@ -4,8 +4,8 @@ import * as d3 from "d3"; class LayersModel { constructor(props) { const layersQuery = - "MATCH (package:Package)-[:CONTAINS]->(layer:Layer), (layer)-[:CONTAINS]->(dependent:Type), (dependent)-[:DECLARES]->(method:Method) " + - "RETURN package.name, layer.name, dependent.name, sum(method.effectiveLineCount) as loc"; + "MATCH (package:Package)-[:CONTAINS]->(layer:Layer)-[:CONTAINS]->(child:Type)-[:DECLARES]->(method:Method) " + + "RETURN package.name as package, layer.name as layer, child.name as child, sum(method.effectiveLineCount) as loc"; this.state = { layersQuery: layersQuery @@ -13,77 +13,92 @@ class LayersModel { } readLayers(that) { - neo4jSession.run(this.state.layersQuery).then(result => { - console.log(result); - result.records.forEach(record => { - if (!this.hasNode(root, record.get("package.name"))) { - root.name = record.get("package.name"); - root.color = "hsl(228, 70%, 50%)"; - root.children = []; - } - if (!this.hasNode(root, record.get("layer.name"))) { - this.appendNode( - root, - record.get("package.name"), - this.createNode( - record.get("layer.name"), - "hsl(111, 70%, 50%)" - ) + let data = []; + let root; + neo4jSession + .run(this.state.layersQuery) + .then(result => { + result.records.forEach(record => { + if (!this.nodeExists(data, record.get("package"))) { + this.appendNonLeafNode( + data, + record.get("package"), + "#F6FBFC", + "" + ); + } + if (!this.nodeExists(data, record.get("layer"))) { + this.appendNonLeafNode( + data, + record.get("layer"), + "#CCECE6", + record.get("package") + ); + } + if (record.get("loc").low < 5) { + record.get("loc").low = 5; + } else { + record.get("loc").low = record.get("loc").low + 5; + } + this.appendLeafNode( + data, + record.get("child"), + "#66C2A4", + record.get("layer"), + record.get("loc").low ); - this.appendNode( - root, - record.get("layer.name"), - this.createNode( - record.get("dependent.name"), - "hsl(204, 70%, 50%)" - ) - ); - } + }); + }) + .then(() => { + root = d3 + .stratify() + .id(node => { + return node.id; + }) + .parentId(node => { + return node.parent; + })(data); + }) + .then(() => { + this.convertData(root); + }) + .then(() => { + that.setState({ + data: root + }); }); - console.log(root); - }); } - hasNode(root, name) { - if (this.isEmpty(root)) return false; - if (root.name === name) return true; - for (let child of root.children) { - if (child.name === name) { - return true; + convertData(node) { + node.color = node.data.color; + for (let i = 0; i < node.children.length; i++) { + node.children[i].color = node.children[i].data.color; + node.children[i].loc = node.children[i].data.loc; + if (node.children[i].children) { + this.convertData(node.children[i]); } } } - isEmpty(root) { - return Object.keys(root).length === 0 && root.constructor === Object; - } - - createNode(name, color) { - return { - name: name, + appendNonLeafNode(data, id, color, parent) { + data.push({ + id: id, color: color, - children: [] - }; + parent: parent + }); } - appendNode(root, parent, child) { - debugger; - const parentNode = this.findParent(root, parent); - console.log(parentNode); - parentNode.children.push(child); + appendLeafNode(data, id, color, parent, loc) { + data.push({ + id: id, + color: color, + loc: loc, + parent: parent + }); } - findParent(root, parentName) { - if (root.name === parentName) { - return parentName; - } - debugger; - let result; - for (let child of root.children) { - console.log(child); - // result = this.findParent(child.children, name) - // if (result) return result; - } + nodeExists(nodes, nodeId) { + return nodes.some(node => node.id === nodeId); } } diff --git a/src/views/Dashboard/Architecture/Layers/Layers.js b/src/views/Dashboard/Architecture/Layers/Layers.js index bbe6cbc..0658f96 100644 --- a/src/views/Dashboard/Architecture/Layers/Layers.js +++ b/src/views/Dashboard/Architecture/Layers/Layers.js @@ -5,6 +5,7 @@ import DashboardAbstract, { import { Button, Row, Col, Card, CardBody } from "reactstrap"; import { ResponsiveBubble } from "@nivo/circle-packing"; import LayersModel from "../../../../api/models/LayersModel"; +import { Treebeard } from "react-treebeard"; class Layers extends DashboardAbstract { constructor(props) { @@ -24,8 +25,11 @@ class Layers extends DashboardAbstract { } render() { - console.log(this.state); - + { + { + console.log(this.state.data); + } + } return (
@@ -33,6 +37,18 @@ class Layers extends DashboardAbstract { + + + + + @@ -41,9 +57,20 @@ class Layers extends DashboardAbstract { > { + return node.color; + }} animate={true} + motionStiffness={90} + motionDamping={12} />
diff --git a/src/views/Dashboard/Architecture/Structure/Structure.js b/src/views/Dashboard/Architecture/Structure/Structure.js index 47a185b..f59b0ad 100644 --- a/src/views/Dashboard/Architecture/Structure/Structure.js +++ b/src/views/Dashboard/Architecture/Structure/Structure.js @@ -294,6 +294,8 @@ class ArchitectureStructure extends DashboardAbstract { return ""; } + console.log(this.state.hotSpotData); + return (
From fe9884dc1f9224b1e53bea011a70b617b79cb1e0 Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Tue, 11 Aug 2020 14:58:09 +0200 Subject: [PATCH 11/22] Visualize invalid dependencies between layers --- src/api/models/LayersModel.js | 90 +++++++++++++------ .../Dashboard/Architecture/Layers/Layers.js | 2 - 2 files changed, 61 insertions(+), 31 deletions(-) diff --git a/src/api/models/LayersModel.js b/src/api/models/LayersModel.js index da5718d..a14145f 100644 --- a/src/api/models/LayersModel.js +++ b/src/api/models/LayersModel.js @@ -7,46 +7,67 @@ class LayersModel { "MATCH (package:Package)-[:CONTAINS]->(layer:Layer)-[:CONTAINS]->(child:Type)-[:DECLARES]->(method:Method) " + "RETURN package.name as package, layer.name as layer, child.name as child, sum(method.effectiveLineCount) as loc"; + const dependenciesQuery = + "MATCH (l1:Layer)-[:CONTAINS]->(dependent:Type)-[:DEPENDS_ON]->(dependency:Type)<-[:CONTAINS]-(l2:Layer) " + + "WHERE NOT (l1.name)=(l2.name) " + + "RETURN l1.name AS dependentLayer, l2.name AS dependencyLayer, dependent.name AS dependent, dependency.name AS dependency " + + "ORDER BY dependentLayer, dependencyLayer"; + this.state = { - layersQuery: layersQuery + layersQuery: layersQuery, + dependenciesQuery: dependenciesQuery, + validDependencyDirection: ["web", "service", "model", "repository"] }; } readLayers(that) { let data = []; let root; + neo4jSession.run(this.state.layersQuery).then(result => { + result.records.forEach(record => { + if (!this.nodeExists(data, record.get("package"))) { + this.appendNonLeafNode( + data, + record.get("package"), + "#F6FBFC", + "" + ); + } + if (!this.nodeExists(data, record.get("layer"))) { + this.appendNonLeafNode( + data, + record.get("layer"), + "#CCECE6", + record.get("package") + ); + } + if (record.get("loc").low < 5) { + record.get("loc").low = 5; + } else { + record.get("loc").low = record.get("loc").low + 5; + } + this.appendLeafNode( + data, + record.get("child"), + "#66C2A4", + record.get("layer"), + record.get("loc").low + ); + }); + }); + neo4jSession - .run(this.state.layersQuery) + .run(this.state.dependenciesQuery) .then(result => { result.records.forEach(record => { - if (!this.nodeExists(data, record.get("package"))) { - this.appendNonLeafNode( - data, - record.get("package"), - "#F6FBFC", - "" - ); - } - if (!this.nodeExists(data, record.get("layer"))) { - this.appendNonLeafNode( - data, - record.get("layer"), - "#CCECE6", - record.get("package") - ); + if (!this.dependencyIsValid(record)) { + data.find( + node => node.id === record.get("dependent") + ).color = "#EF6548"; + data.find( + node => node.id === record.get("dependency") + ).color = "#EF6548"; } - if (record.get("loc").low < 5) { - record.get("loc").low = 5; - } else { - record.get("loc").low = record.get("loc").low + 5; - } - this.appendLeafNode( - data, - record.get("child"), - "#66C2A4", - record.get("layer"), - record.get("loc").low - ); }); }) .then(() => { @@ -69,6 +90,17 @@ class LayersModel { }); } + dependencyIsValid(record) { + return ( + this.state.validDependencyDirection.indexOf( + record.get("dependentLayer") + ) < + this.state.validDependencyDirection.indexOf( + record.get("dependencyLayer") + ) + ); + } + convertData(node) { node.color = node.data.color; for (let i = 0; i < node.children.length; i++) { diff --git a/src/views/Dashboard/Architecture/Layers/Layers.js b/src/views/Dashboard/Architecture/Layers/Layers.js index 0658f96..c4cf609 100644 --- a/src/views/Dashboard/Architecture/Layers/Layers.js +++ b/src/views/Dashboard/Architecture/Layers/Layers.js @@ -69,8 +69,6 @@ class Layers extends DashboardAbstract { return node.color; }} animate={true} - motionStiffness={90} - motionDamping={12} />
From f2f68cc41b1890d3fa7500bb6bd4c37cebf8781a Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Wed, 12 Aug 2020 17:34:41 +0200 Subject: [PATCH 12/22] Implementation of Tree --- src/api/models/LayersModel.js | 126 +++++++++++++----- .../Dashboard/Architecture/Layers/Layers.js | 46 +++++-- 2 files changed, 129 insertions(+), 43 deletions(-) diff --git a/src/api/models/LayersModel.js b/src/api/models/LayersModel.js index a14145f..94e271d 100644 --- a/src/api/models/LayersModel.js +++ b/src/api/models/LayersModel.js @@ -21,33 +21,48 @@ class LayersModel { } readLayers(that) { - let data = []; - let root; + let visualizationData = []; + let treeData = []; + let visualizationRoot; + let treeRoot; + neo4jSession.run(this.state.layersQuery).then(result => { result.records.forEach(record => { - if (!this.nodeExists(data, record.get("package"))) { + if ( + !this.nodeExists(visualizationData, record.get("package")) + ) { + this.appendNonLeafNode( + visualizationData, + record.get("package"), + "", + "#F6FBFC" + ); this.appendNonLeafNode( - data, + treeData, record.get("package"), - "#F6FBFC", + "", "" ); } - if (!this.nodeExists(data, record.get("layer"))) { + if (!this.nodeExists(visualizationData, record.get("layer"))) { this.appendNonLeafNode( - data, + visualizationData, record.get("layer"), - "#CCECE6", - record.get("package") + record.get("package"), + "#CCECE6" + ); + this.appendNonLeafNode( + treeData, + record.get("layer"), + record.get("package"), + "#CCECE6" ); } - if (record.get("loc").low < 5) { - record.get("loc").low = 5; - } else { - record.get("loc").low = record.get("loc").low + 5; + if (record.get("loc").low === 0) { + record.get("loc").low = 1; } this.appendLeafNode( - data, + visualizationData, record.get("child"), "#66C2A4", record.get("layer"), @@ -61,35 +76,61 @@ class LayersModel { .then(result => { result.records.forEach(record => { if (!this.dependencyIsValid(record)) { - data.find( - node => node.id === record.get("dependent") - ).color = "#EF6548"; - data.find( - node => node.id === record.get("dependency") - ).color = "#EF6548"; + treeData.push({ + id: record.get("dependent"), + parent: record.get("dependentLayer"), + dependency: record.get("dependency"), + dependencyLayer: record.get("dependencyLayer") + }); + this.changeColor( + visualizationData, + record.get("dependent") + ); + this.changeColor( + visualizationData, + record.get("dependency") + ); } }); }) .then(() => { - root = d3 + console.log("treedata", treeData); + visualizationRoot = d3 .stratify() .id(node => { return node.id; }) .parentId(node => { return node.parent; - })(data); + })(visualizationData); + + treeRoot = d3 + .stratify() + .id(node => { + return node.id; + }) + .parentId(node => { + return node.parent; + })(treeData); }) .then(() => { - this.convertData(root); + console.log("visualizationRoot", visualizationRoot); + console.log("treeRoot", treeRoot); + this.convertDataForVisualization(visualizationRoot); + this.convertDataForTree(treeRoot); }) .then(() => { that.setState({ - data: root + visualizationData: visualizationRoot, + treeData: treeRoot }); }); } + changeColor(data, nodeId) { + data.find(node => node.id === nodeId).color = "#EF6548"; + } + dependencyIsValid(record) { return ( this.state.validDependencyDirection.indexOf( @@ -101,22 +142,39 @@ class LayersModel { ); } - convertData(node) { - node.color = node.data.color; - for (let i = 0; i < node.children.length; i++) { - node.children[i].color = node.children[i].data.color; - node.children[i].loc = node.children[i].data.loc; - if (node.children[i].children) { - this.convertData(node.children[i]); + convertDataForVisualization(root) { + root.color = root.data.color; + root.name = root.id; + for (let i = 0; i < root.children.length; i++) { + root.children[i].color = root.children[i].data.color; + root.children[i].loc = root.children[i].data.loc; + if (root.children[i].children) { + this.convertDataForVisualization(root.children[i]); } } } - appendNonLeafNode(data, id, color, parent) { - data.push({ + convertDataForTree(root) { + root.name = root.id; + root.toggled = false; + for (let i = 0; i < root.children.length; i++) { + root.children[i].name = root.children[i].id; + root.children[i].toggled = false; + if (root.children[i].children) { + root.children[i].children.forEach(child => { + child.name = + child.data.id + " accesses " + child.data.dependency; + }); + } + } + } + + appendNonLeafNode(dataset, id, parent, color) { + dataset.push({ id: id, + parent: parent, color: color, - parent: parent + violations: [] }); } diff --git a/src/views/Dashboard/Architecture/Layers/Layers.js b/src/views/Dashboard/Architecture/Layers/Layers.js index c4cf609..bd31ffe 100644 --- a/src/views/Dashboard/Architecture/Layers/Layers.js +++ b/src/views/Dashboard/Architecture/Layers/Layers.js @@ -2,17 +2,22 @@ import React from "react"; import DashboardAbstract, { databaseCredentialsProvided } from "../../AbstractDashboardComponent"; -import { Button, Row, Col, Card, CardBody } from "reactstrap"; +import { Row, Col, Card, CardBody } from "reactstrap"; import { ResponsiveBubble } from "@nivo/circle-packing"; import LayersModel from "../../../../api/models/LayersModel"; import { Treebeard } from "react-treebeard"; +const treebeardCustomTheme = require("./TreebeardCustomTheme"); + class Layers extends DashboardAbstract { constructor(props) { super(props); this.state = { - data: {} + visualizationData: {}, + treeData: {} }; + + this.onToggle = this.onToggle.bind(this); } componentDidMount() { @@ -24,12 +29,23 @@ class Layers extends DashboardAbstract { } } - render() { - { - { - console.log(this.state.data); - } + onToggle(node, toggled) { + const { cursor, treeData } = this.state; + if (cursor) { + this.setState(() => ({ cursor, active: false })); } + node.active = true; + if (node.children) { + node.toggled = toggled; + } + this.setState(() => ({ + cursor: node, + treeData: Object.assign({}, treeData) + })); + } + + render() { + console.log(this.state); return (
@@ -46,7 +62,15 @@ class Layers extends DashboardAbstract { overflow: "hidden" }} > - + + + @@ -56,7 +80,10 @@ class Layers extends DashboardAbstract { style={{ height: "600px" }} >
From f67e73719ffc14e0db8f8b9a9e19471c24bff2d6 Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Fri, 14 Aug 2020 12:23:28 +0200 Subject: [PATCH 13/22] Display all Dependencies in Tree. Switching to HTML component in order to access circles. --- src/api/models/LayersModel.js | 22 +++++++------- .../Dashboard/Architecture/Layers/Layers.js | 30 ++++++++++++++----- .../Layers/TreebeardCustomTheme.js | 2 +- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/api/models/LayersModel.js b/src/api/models/LayersModel.js index 94e271d..09cc04a 100644 --- a/src/api/models/LayersModel.js +++ b/src/api/models/LayersModel.js @@ -75,25 +75,27 @@ class LayersModel { .run(this.state.dependenciesQuery) .then(result => { result.records.forEach(record => { - if (!this.dependencyIsValid(record)) { + if (this.dependencyIsValid(record)) { treeData.push({ id: record.get("dependent"), parent: record.get("dependentLayer"), dependency: record.get("dependency"), - dependencyLayer: record.get("dependencyLayer") + dependencyLayer: record.get("dependencyLayer"), + dependencyIsValid: true + }); + } else { + treeData.push({ + id: record.get("dependent"), + parent: record.get("dependentLayer"), + dependency: record.get("dependency"), + dependencyLayer: record.get("dependencyLayer"), + dependencyIsValid: false }); - this.changeColor( - visualizationData, - record.get("dependent") - ); - this.changeColor( - visualizationData, - record.get("dependency") - ); } }); }) .then(() => { + console.log("visualizationData", visualizationData); console.log("treedata", treeData); visualizationRoot = d3 .stratify() diff --git a/src/views/Dashboard/Architecture/Layers/Layers.js b/src/views/Dashboard/Architecture/Layers/Layers.js index bd31ffe..f53fb50 100644 --- a/src/views/Dashboard/Architecture/Layers/Layers.js +++ b/src/views/Dashboard/Architecture/Layers/Layers.js @@ -3,7 +3,7 @@ import DashboardAbstract, { databaseCredentialsProvided } from "../../AbstractDashboardComponent"; import { Row, Col, Card, CardBody } from "reactstrap"; -import { ResponsiveBubble } from "@nivo/circle-packing"; +import { ResponsiveBubbleHtml } from "@nivo/circle-packing"; import LayersModel from "../../../../api/models/LayersModel"; import { Treebeard } from "react-treebeard"; @@ -29,23 +29,36 @@ class Layers extends DashboardAbstract { } } + onClick(node) { + console.log(node); + const dependentId = node.id; + const dependencyId = node.data.dependency; + + if (dependentId) { + const nodeCircle = document.querySelector(); + } + } + onToggle(node, toggled) { - const { cursor, treeData } = this.state; - if (cursor) { - this.setState(() => ({ cursor, active: false })); + if (this.state.cursor) { + this.state.cursor.active = false; } node.active = true; if (node.children) { node.toggled = toggled; } this.setState(() => ({ - cursor: node, - treeData: Object.assign({}, treeData) + cursor: node })); } render() { console.log(this.state); + + if (!this.state.visualizationData.name) { + return ""; + } + return (
@@ -77,9 +90,12 @@ class Layers extends DashboardAbstract {
- Date: Tue, 18 Aug 2020 15:30:35 +0200 Subject: [PATCH 14/22] Implementation of Treebeard component. Invalid Dependencies get highlighted. Refactoring --- src/api/models/LayersModel.js | 193 ++++++++++++------ .../Dashboard/Architecture/Layers/Layers.js | 26 ++- .../Layers/TreebeardCustomTheme.js | 2 +- 3 files changed, 140 insertions(+), 81 deletions(-) diff --git a/src/api/models/LayersModel.js b/src/api/models/LayersModel.js index 09cc04a..3a07a04 100644 --- a/src/api/models/LayersModel.js +++ b/src/api/models/LayersModel.js @@ -21,82 +21,125 @@ class LayersModel { } readLayers(that) { + const COLORS = { + PACKAGE: "#F6FBFC", + LAYER: "#CCECE6", + VALID_LEAF: "#66C2A4", + INVALID_LEAF: "#ef654c" + }; let visualizationData = []; let treeData = []; + let dependencies = []; let visualizationRoot; let treeRoot; - neo4jSession.run(this.state.layersQuery).then(result => { + neo4jSession.run(this.state.dependenciesQuery).then(result => { result.records.forEach(record => { - if ( - !this.nodeExists(visualizationData, record.get("package")) - ) { - this.appendNonLeafNode( - visualizationData, - record.get("package"), - "", - "#F6FBFC" - ); - this.appendNonLeafNode( + if (!this.dependencyIsValid(record)) { + dependencies.push({ + id: record.get("dependent"), + dependency: record.get("dependency"), + isValid: false + }); + this.appendDependency( treeData, - record.get("package"), - "", - "" + record.get("dependent"), + record.get("dependency"), + record.get("dependentLayer"), + record.get("dependencyLayer"), + false ); - } - if (!this.nodeExists(visualizationData, record.get("layer"))) { - this.appendNonLeafNode( - visualizationData, - record.get("layer"), - record.get("package"), - "#CCECE6" - ); - this.appendNonLeafNode( + } else { + dependencies.push({ + id: record.get("dependent"), + dependency: record.get("dependency"), + isValid: true + }); + this.appendDependency( treeData, - record.get("layer"), - record.get("package"), - "#CCECE6" + record.get("dependent"), + record.get("dependency"), + record.get("dependentLayer"), + record.get("dependencyLayer"), + true ); } - if (record.get("loc").low === 0) { - record.get("loc").low = 1; - } - this.appendLeafNode( - visualizationData, - record.get("child"), - "#66C2A4", - record.get("layer"), - record.get("loc").low - ); }); }); neo4jSession - .run(this.state.dependenciesQuery) + .run(this.state.layersQuery) .then(result => { result.records.forEach(record => { - if (this.dependencyIsValid(record)) { - treeData.push({ - id: record.get("dependent"), - parent: record.get("dependentLayer"), - dependency: record.get("dependency"), - dependencyLayer: record.get("dependencyLayer"), - dependencyIsValid: true - }); + if ( + !this.nodeExists( + visualizationData, + record.get("package") + ) + ) { + this.appendVisualizationNode( + visualizationData, + record.get("package"), + "", + COLORS.PACKAGE + ); + this.appendTreeNode( + treeData, + record.get("package"), + "" + ); + } + if ( + !this.nodeExists(visualizationData, record.get("layer")) + ) { + this.appendVisualizationNode( + visualizationData, + record.get("layer"), + record.get("package"), + COLORS.LAYER + ); + this.appendTreeNode( + treeData, + record.get("layer"), + record.get("package") + ); + } + if (record.get("loc").low === 0) { + record.get("loc").low = 1; + } + if ( + (dependencies.find( + node => node.id === record.get("child") + ) !== undefined && + !dependencies.find( + node => node.id === record.get("child") + ).isValid) || + (dependencies.find( + node => node.dependency === record.get("child") + ) !== undefined && + !dependencies.find( + node => node.dependency === record.get("child") + ).isValid) + ) { + this.appendLeafNode( + visualizationData, + record.get("child"), + record.get("layer"), + COLORS.INVALID_LEAF, + record.get("loc").low + ); } else { - treeData.push({ - id: record.get("dependent"), - parent: record.get("dependentLayer"), - dependency: record.get("dependency"), - dependencyLayer: record.get("dependencyLayer"), - dependencyIsValid: false - }); + this.appendLeafNode( + visualizationData, + record.get("child"), + record.get("layer"), + COLORS.VALID_LEAF, + record.get("loc").low + ); } }); }) .then(() => { - console.log("visualizationData", visualizationData); - console.log("treedata", treeData); visualizationRoot = d3 .stratify() .id(node => { @@ -116,21 +159,33 @@ class LayersModel { })(treeData); }) .then(() => { - console.log("visualizationRoot", visualizationRoot); - console.log("treeRoot", treeRoot); this.convertDataForVisualization(visualizationRoot); this.convertDataForTree(treeRoot); }) .then(() => { that.setState({ visualizationData: visualizationRoot, - treeData: treeRoot + treeData: treeRoot, + dependencies: dependencies }); }); } - changeColor(data, nodeId) { - data.find(node => node.id === nodeId).color = "#EF6548"; + appendDependency( + dataset, + dependent, + dependency, + dependentLayer, + dependencyLayer, + isValid + ) { + dataset.push({ + id: dependent, + dependency: dependency, + parent: dependentLayer, + dependencyLayer: dependencyLayer, + isValid: isValid + }); } dependencyIsValid(record) { @@ -171,21 +226,27 @@ class LayersModel { } } - appendNonLeafNode(dataset, id, parent, color) { + appendTreeNode(dataset, id, parent) { + dataset.push({ + id: id, + parent: parent + }); + } + + appendVisualizationNode(dataset, id, parent, color) { dataset.push({ id: id, parent: parent, - color: color, - violations: [] + color: color }); } - appendLeafNode(data, id, color, parent, loc) { - data.push({ + appendLeafNode(dataset, id, parent, color, loc) { + dataset.push({ id: id, + parent: parent, color: color, - loc: loc, - parent: parent + loc: loc }); } diff --git a/src/views/Dashboard/Architecture/Layers/Layers.js b/src/views/Dashboard/Architecture/Layers/Layers.js index f53fb50..cbafff8 100644 --- a/src/views/Dashboard/Architecture/Layers/Layers.js +++ b/src/views/Dashboard/Architecture/Layers/Layers.js @@ -14,7 +14,8 @@ class Layers extends DashboardAbstract { super(props); this.state = { visualizationData: {}, - treeData: {} + treeData: {}, + dependencies: [] }; this.onToggle = this.onToggle.bind(this); @@ -29,21 +30,12 @@ class Layers extends DashboardAbstract { } } - onClick(node) { - console.log(node); - const dependentId = node.id; - const dependencyId = node.data.dependency; - - if (dependentId) { - const nodeCircle = document.querySelector(); - } - } - onToggle(node, toggled) { if (this.state.cursor) { this.state.cursor.active = false; } node.active = true; + // this.highlightCircles(node); if (node.children) { node.toggled = toggled; } @@ -53,10 +45,8 @@ class Layers extends DashboardAbstract { } render() { - console.log(this.state); - if (!this.state.visualizationData.name) { - return ""; + return "Loading..."; } return ( @@ -113,6 +103,14 @@ class Layers extends DashboardAbstract { }} animate={true} enableLabel={false} + borderWidth={2} + tooltip={({ id }) => ( +
+ + {id} + +
+ )} />
diff --git a/src/views/Dashboard/Architecture/Layers/TreebeardCustomTheme.js b/src/views/Dashboard/Architecture/Layers/TreebeardCustomTheme.js index 7a32a84..3fc03a8 100644 --- a/src/views/Dashboard/Architecture/Layers/TreebeardCustomTheme.js +++ b/src/views/Dashboard/Architecture/Layers/TreebeardCustomTheme.js @@ -4,7 +4,7 @@ export default { tree: { base: { listStyle: "none", - backgroundColor: "#FFFFFF", + backgroundColor: "#ffffff", margin: "0px", padding: "0px", color: "#151b1e" From 50fd1a2ccc0be4067756ca9516c55692bf69c95b Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Wed, 19 Aug 2020 14:37:33 +0200 Subject: [PATCH 15/22] Valid and invalid dependencies are highlighted in tree view --- src/scss/style.css | 16 ++++----- .../Architecture/Layers/CustomHeader.js | 34 +++++++++++++++++++ .../Architecture/Layers/HeaderDecorator.js | 11 ++++++ .../Dashboard/Architecture/Layers/Layers.js | 10 ++++-- 4 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 src/views/Dashboard/Architecture/Layers/CustomHeader.js create mode 100644 src/views/Dashboard/Architecture/Layers/HeaderDecorator.js diff --git a/src/scss/style.css b/src/scss/style.css index d058fa5..4dc5e35 100644 --- a/src/scss/style.css +++ b/src/scss/style.css @@ -1,10 +1,10 @@ @charset "UTF-8"; -/*! - * CoreUI - Open Source Dashboard UI Kit - * @version v2.1.12 - * @link https://coreui.io - * Copyright (c) 2018 creativeLabs Łukasz Holeczek - * Licensed under MIT (https://coreui.io/license) +/*! + * CoreUI - Open Source Dashboard UI Kit + * @version v2.1.12 + * @link https://coreui.io + * Copyright (c) 2018 creativeLabs Łukasz Holeczek + * Licensed under MIT (https://coreui.io/license) */ /*! * Bootstrap v4.3.1 (https://getbootstrap.com/) @@ -7108,8 +7108,8 @@ a.text-dark:hover, a.text-dark:focus { background-color: #eee; opacity: .9; } -/* - * Scrollbar thumb styles +/* + * Scrollbar thumb styles */ .ps__thumb-x { position: absolute; diff --git a/src/views/Dashboard/Architecture/Layers/CustomHeader.js b/src/views/Dashboard/Architecture/Layers/CustomHeader.js new file mode 100644 index 0000000..1b803a2 --- /dev/null +++ b/src/views/Dashboard/Architecture/Layers/CustomHeader.js @@ -0,0 +1,34 @@ +import React, { useState } from "react"; + +const CustomHeader = ({ node, style }) => { + const newBaseStyle = { ...style.base }; + + if (node.data.isValid != null) { + if (!node.data.isValid) { + newBaseStyle.color = "#EF654C"; + } else { + newBaseStyle.color = "#35a47f"; + } + } + + /* + const onMouseOver = node => { + nodes.push(document.getElementById(node.id)); + if (node.dependency) { + nodes.push(document.getElementById(node.dependency)); + } + document.getElementById(node.id).style.borderColor = "#82d3e7" + //document.getElementById(node.dependency).style.borderColor = "#82d3e7" + }; + + const onMouseLeave = node => { + document.getElementById(node.id).style.borderColor = nodes.find(node => node.id).borderColor + nodes.splice(nodes.indexOf(node.id)) + }; + + + */ + return
{node.name}
; +}; + +export default CustomHeader; diff --git a/src/views/Dashboard/Architecture/Layers/HeaderDecorator.js b/src/views/Dashboard/Architecture/Layers/HeaderDecorator.js new file mode 100644 index 0000000..777ab00 --- /dev/null +++ b/src/views/Dashboard/Architecture/Layers/HeaderDecorator.js @@ -0,0 +1,11 @@ +import React from "react"; + +const HeaderDecorator = ({ node, style }) => { + return ( +
+
{node.name}
+
+ ); +}; + +export default HeaderDecorator; diff --git a/src/views/Dashboard/Architecture/Layers/Layers.js b/src/views/Dashboard/Architecture/Layers/Layers.js index cbafff8..fa17f11 100644 --- a/src/views/Dashboard/Architecture/Layers/Layers.js +++ b/src/views/Dashboard/Architecture/Layers/Layers.js @@ -5,7 +5,8 @@ import DashboardAbstract, { import { Row, Col, Card, CardBody } from "reactstrap"; import { ResponsiveBubbleHtml } from "@nivo/circle-packing"; import LayersModel from "../../../../api/models/LayersModel"; -import { Treebeard } from "react-treebeard"; +import { Treebeard, decorators } from "react-treebeard"; +import CustomHeader from "./CustomHeader"; const treebeardCustomTheme = require("./TreebeardCustomTheme"); @@ -35,7 +36,6 @@ class Layers extends DashboardAbstract { this.state.cursor.active = false; } node.active = true; - // this.highlightCircles(node); if (node.children) { node.toggled = toggled; } @@ -49,6 +49,8 @@ class Layers extends DashboardAbstract { return "Loading..."; } + console.log(this.state); + return (
@@ -72,6 +74,10 @@ class Layers extends DashboardAbstract { style={ treebeardCustomTheme.default } + decorators={{ + ...decorators, + Header: CustomHeader + }} /> From 47a9a49bf6e43849becbdaa202ec47f26cf7d992 Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Wed, 19 Aug 2020 15:57:25 +0200 Subject: [PATCH 16/22] Refactoring --- .../Architecture/Layers/CustomHeader.js | 19 +------------------ .../Dashboard/Architecture/Layers/Layers.js | 2 -- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/views/Dashboard/Architecture/Layers/CustomHeader.js b/src/views/Dashboard/Architecture/Layers/CustomHeader.js index 1b803a2..5555482 100644 --- a/src/views/Dashboard/Architecture/Layers/CustomHeader.js +++ b/src/views/Dashboard/Architecture/Layers/CustomHeader.js @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React from "react"; const CustomHeader = ({ node, style }) => { const newBaseStyle = { ...style.base }; @@ -11,23 +11,6 @@ const CustomHeader = ({ node, style }) => { } } - /* - const onMouseOver = node => { - nodes.push(document.getElementById(node.id)); - if (node.dependency) { - nodes.push(document.getElementById(node.dependency)); - } - document.getElementById(node.id).style.borderColor = "#82d3e7" - //document.getElementById(node.dependency).style.borderColor = "#82d3e7" - }; - - const onMouseLeave = node => { - document.getElementById(node.id).style.borderColor = nodes.find(node => node.id).borderColor - nodes.splice(nodes.indexOf(node.id)) - }; - - - */ return
{node.name}
; }; diff --git a/src/views/Dashboard/Architecture/Layers/Layers.js b/src/views/Dashboard/Architecture/Layers/Layers.js index fa17f11..2b57d6d 100644 --- a/src/views/Dashboard/Architecture/Layers/Layers.js +++ b/src/views/Dashboard/Architecture/Layers/Layers.js @@ -49,8 +49,6 @@ class Layers extends DashboardAbstract { return "Loading..."; } - console.log(this.state); - return (
From a57c35b6e0f272e018f11b2b9f1dd44a24981609 Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Fri, 21 Aug 2020 12:42:56 +0200 Subject: [PATCH 17/22] Implement dependency definition query with :DEFINES_DEPENDENCY relationship type --- src/api/models/LayersModel.js | 38 ++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/api/models/LayersModel.js b/src/api/models/LayersModel.js index 3a07a04..8aeb047 100644 --- a/src/api/models/LayersModel.js +++ b/src/api/models/LayersModel.js @@ -13,10 +13,15 @@ class LayersModel { "RETURN l1.name AS dependentLayer, l2.name AS dependencyLayer, dependent.name AS dependent, dependency.name AS dependency " + "ORDER BY dependentLayer, dependencyLayer"; + const dependencyDefinitionQuery = + "MATCH (dependent:Layer)-[:DEFINES_DEPENDENCY]->(dependency:Layer) " + + "RETURN dependent.name as dependent, collect(dependency.name) as dependencies"; + this.state = { layersQuery: layersQuery, dependenciesQuery: dependenciesQuery, - validDependencyDirection: ["web", "service", "model", "repository"] + dependencyDefinitionQuery: dependencyDefinitionQuery, + validDependencies: [] }; } @@ -33,6 +38,19 @@ class LayersModel { let visualizationRoot; let treeRoot; + neo4jSession.run(this.state.dependencyDefinitionQuery).then(result => { + const validDependencies = []; + result.records.forEach(record => { + record.get("dependencies").forEach(dependency => { + validDependencies.push({ + dependent: record.get("dependent"), + dependency: dependency + }); + }); + }); + this.state.validDependencies = validDependencies; + }); + neo4jSession.run(this.state.dependenciesQuery).then(result => { result.records.forEach(record => { if (!this.dependencyIsValid(record)) { @@ -189,14 +207,16 @@ class LayersModel { } dependencyIsValid(record) { - return ( - this.state.validDependencyDirection.indexOf( - record.get("dependentLayer") - ) < - this.state.validDependencyDirection.indexOf( - record.get("dependencyLayer") - ) - ); + let isValid = false; + this.state.validDependencies.forEach(validDependency => { + if ( + record.get("dependentLayer") === validDependency.dependent && + record.get("dependencyLayer") === validDependency.dependency + ) { + isValid = true; + } + }); + return isValid; } convertDataForVisualization(root) { From 9924f905730701768b94895c937d91bf9b2bad13 Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Mon, 24 Aug 2020 18:38:46 +0200 Subject: [PATCH 18/22] Highlight respective bubbles on hover in tree --- data/petclinic/Dockerfile | 2 +- src/api/models/LayersModel.js | 22 ++++---- src/scss/_custom.scss | 4 ++ src/scss/style.css | 12 +++++ .../Architecture/Layers/CustomHeader.js | 50 ++++++++++++++++++- .../Architecture/Layers/HeaderDecorator.js | 11 ---- .../Dashboard/Architecture/Layers/Layers.js | 2 + 7 files changed, 78 insertions(+), 25 deletions(-) delete mode 100644 src/views/Dashboard/Architecture/Layers/HeaderDecorator.js diff --git a/data/petclinic/Dockerfile b/data/petclinic/Dockerfile index 3683a97..40e9e61 100644 --- a/data/petclinic/Dockerfile +++ b/data/petclinic/Dockerfile @@ -7,7 +7,7 @@ COPY petclinic.dump /var/lib/neo4j/import/ VOLUME /data -CMD sed -e 's/^#dbms.read_only=.*$/dbms.read_only=true/' -i /var/lib/neo4j/conf/neo4j.conf && \ +CMD sed -e 's/^#dbms.read_only=.*$/dbms.read_only=false/' -i /var/lib/neo4j/conf/neo4j.conf && \ mkdir -p /var/lib/neo4j/data/databases/graph.db && \ bin/neo4j-admin set-initial-password ${NEO4J_PASSWD} || true && \ bin/neo4j-admin load --from=/var/lib/neo4j/import/petclinic.dump --force && \ diff --git a/src/api/models/LayersModel.js b/src/api/models/LayersModel.js index 8aeb047..35ac44f 100644 --- a/src/api/models/LayersModel.js +++ b/src/api/models/LayersModel.js @@ -125,19 +125,17 @@ class LayersModel { if (record.get("loc").low === 0) { record.get("loc").low = 1; } + if ( - (dependencies.find( - node => node.id === record.get("child") - ) !== undefined && - !dependencies.find( - node => node.id === record.get("child") - ).isValid) || - (dependencies.find( - node => node.dependency === record.get("child") - ) !== undefined && - !dependencies.find( - node => node.dependency === record.get("child") - ).isValid) + dependencies.filter( + node => + node.id === record.get("child") && !node.isValid + ).length > 0 || + dependencies.filter( + node => + node.dependency === record.get("child") && + !node.isValid + ).length > 0 ) { this.appendLeafNode( visualizationData, diff --git a/src/scss/_custom.scss b/src/scss/_custom.scss index 6d45633..235559c 100644 --- a/src/scss/_custom.scss +++ b/src/scss/_custom.scss @@ -407,3 +407,7 @@ a.breadcrumb-item { .height-auto { height: auto !important; } + +.circle-hovered { + border-color: #f00 !important; +} diff --git a/src/scss/style.css b/src/scss/style.css index 4dc5e35..4bd6591 100644 --- a/src/scss/style.css +++ b/src/scss/style.css @@ -11837,3 +11837,15 @@ a.breadcrumb-item { html body .app.flex-row.align-items-center { height: 100vh; } + +.circle-hovered-default { + border-color: #2b3cab !important; +} + +.circle-hovered-valid { + border-color: #235d20 !important; +} + +.circle-hovered-invalid { + border-color: #601926 !important; +} diff --git a/src/views/Dashboard/Architecture/Layers/CustomHeader.js b/src/views/Dashboard/Architecture/Layers/CustomHeader.js index 5555482..1314f14 100644 --- a/src/views/Dashboard/Architecture/Layers/CustomHeader.js +++ b/src/views/Dashboard/Architecture/Layers/CustomHeader.js @@ -3,6 +3,46 @@ import React from "react"; const CustomHeader = ({ node, style }) => { const newBaseStyle = { ...style.base }; + const onMouseOver = node => { + if (node.data.isValid) { + document + .getElementById(node.id) + .classList.add("circle-hovered-valid"); + if (node.data.hasOwnProperty("dependency")) { + document + .getElementById(node.data.dependency) + .classList.add("circle-hovered-valid"); + } + } else if (node.data.hasOwnProperty("isValid") && !node.data.isValid) { + document + .getElementById(node.id) + .classList.add("circle-hovered-invalid"); + } else { + document + .getElementById(node.id) + .classList.add("circle-hovered-default"); + } + }; + + function removeHighlight(nodeId) { + document + .getElementById(nodeId) + .classList.remove("circle-hovered-valid"); + document + .getElementById(nodeId) + .classList.remove("circle-hovered-invalid"); + document + .getElementById(nodeId) + .classList.remove("circle-hovered-default"); + } + + const onMouseLeave = node => { + removeHighlight(node.id); + if (node.data.hasOwnProperty("dependency")) { + removeHighlight(node.data.dependency); + } + }; + if (node.data.isValid != null) { if (!node.data.isValid) { newBaseStyle.color = "#EF654C"; @@ -11,7 +51,15 @@ const CustomHeader = ({ node, style }) => { } } - return
{node.name}
; + return ( +
onMouseOver(node)} + onMouseLeave={() => onMouseLeave(node)} + > + {node.name} +
+ ); }; export default CustomHeader; diff --git a/src/views/Dashboard/Architecture/Layers/HeaderDecorator.js b/src/views/Dashboard/Architecture/Layers/HeaderDecorator.js deleted file mode 100644 index 777ab00..0000000 --- a/src/views/Dashboard/Architecture/Layers/HeaderDecorator.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; - -const HeaderDecorator = ({ node, style }) => { - return ( -
-
{node.name}
-
- ); -}; - -export default HeaderDecorator; diff --git a/src/views/Dashboard/Architecture/Layers/Layers.js b/src/views/Dashboard/Architecture/Layers/Layers.js index 2b57d6d..8646e35 100644 --- a/src/views/Dashboard/Architecture/Layers/Layers.js +++ b/src/views/Dashboard/Architecture/Layers/Layers.js @@ -49,6 +49,8 @@ class Layers extends DashboardAbstract { return "Loading..."; } + console.log(this.state.treeData); + return (
From fbcfc3ac67c1155f45814ee3b18df30d9bf90c97 Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Wed, 26 Aug 2020 13:26:32 +0200 Subject: [PATCH 19/22] Lexicographical ordering of tree nodes --- src/api/models/LayersModel.js | 5 +++-- src/views/Dashboard/Architecture/Layers/Layers.js | 2 -- src/views/Dashboard/Architecture/Structure/Structure.js | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/api/models/LayersModel.js b/src/api/models/LayersModel.js index 35ac44f..4852a69 100644 --- a/src/api/models/LayersModel.js +++ b/src/api/models/LayersModel.js @@ -5,13 +5,14 @@ class LayersModel { constructor(props) { const layersQuery = "MATCH (package:Package)-[:CONTAINS]->(layer:Layer)-[:CONTAINS]->(child:Type)-[:DECLARES]->(method:Method) " + - "RETURN package.name as package, layer.name as layer, child.name as child, sum(method.effectiveLineCount) as loc"; + "RETURN package.name as package, layer.name as layer, child.name as child, sum(method.effectiveLineCount) as loc " + + "ORDER BY layer.name, child.name"; const dependenciesQuery = "MATCH (l1:Layer)-[:CONTAINS]->(dependent:Type)-[:DEPENDS_ON]->(dependency:Type)<-[:CONTAINS]-(l2:Layer) " + "WHERE NOT (l1.name)=(l2.name) " + "RETURN l1.name AS dependentLayer, l2.name AS dependencyLayer, dependent.name AS dependent, dependency.name AS dependency " + - "ORDER BY dependentLayer, dependencyLayer"; + "ORDER BY dependentLayer, dependencyLayer, dependent"; const dependencyDefinitionQuery = "MATCH (dependent:Layer)-[:DEFINES_DEPENDENCY]->(dependency:Layer) " + diff --git a/src/views/Dashboard/Architecture/Layers/Layers.js b/src/views/Dashboard/Architecture/Layers/Layers.js index 8646e35..2b57d6d 100644 --- a/src/views/Dashboard/Architecture/Layers/Layers.js +++ b/src/views/Dashboard/Architecture/Layers/Layers.js @@ -49,8 +49,6 @@ class Layers extends DashboardAbstract { return "Loading..."; } - console.log(this.state.treeData); - return (
diff --git a/src/views/Dashboard/Architecture/Structure/Structure.js b/src/views/Dashboard/Architecture/Structure/Structure.js index f59b0ad..7636d4b 100644 --- a/src/views/Dashboard/Architecture/Structure/Structure.js +++ b/src/views/Dashboard/Architecture/Structure/Structure.js @@ -290,12 +290,12 @@ class ArchitectureStructure extends DashboardAbstract { return redirect; } + console.log(this.state.hotSpotData); + if (!this.state.hotSpotData.name) { return ""; } - console.log(this.state.hotSpotData); - return (
From 34fb96a2b198125f6353fc34b11e40dd504a9061 Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Mon, 31 Aug 2020 17:20:10 +0200 Subject: [PATCH 20/22] Highlight invalid bubbles --- src/views/Dashboard/Architecture/Layers/CustomHeader.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/views/Dashboard/Architecture/Layers/CustomHeader.js b/src/views/Dashboard/Architecture/Layers/CustomHeader.js index 1314f14..8a19d10 100644 --- a/src/views/Dashboard/Architecture/Layers/CustomHeader.js +++ b/src/views/Dashboard/Architecture/Layers/CustomHeader.js @@ -17,6 +17,9 @@ const CustomHeader = ({ node, style }) => { document .getElementById(node.id) .classList.add("circle-hovered-invalid"); + document + .getElementById(node.data.dependency) + .classList.add("circle-hovered-invalid"); } else { document .getElementById(node.id) From f29c75c9ad44fad7aa1c3fc3a07c65ebb1b5e098 Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Mon, 31 Aug 2020 18:11:02 +0200 Subject: [PATCH 21/22] Error handling. Change Routes. --- src/api/models/LayersModel.js | 6 ++++++ src/routes.js | 6 +++++- src/views/Dashboard/Architecture/Layers/Layers.js | 13 +++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/api/models/LayersModel.js b/src/api/models/LayersModel.js index 4852a69..b75f512 100644 --- a/src/api/models/LayersModel.js +++ b/src/api/models/LayersModel.js @@ -185,6 +185,12 @@ class LayersModel { treeData: treeRoot, dependencies: dependencies }); + }) + .catch(e => { + console.error( + "Something went wrong while initializing the given data.", + e + ); }); } diff --git a/src/routes.js b/src/routes.js index 3314637..2f6256f 100644 --- a/src/routes.js +++ b/src/routes.js @@ -101,7 +101,11 @@ const routes = [ name: "Dependencies", component: Dependencies }, - { path: "/architecture/layers", name: "Layers", component: Layers }, + { + path: "/architecture/layers", + name: "Layers", + component: Layers + }, { path: "/resource-management", exact: true, diff --git a/src/views/Dashboard/Architecture/Layers/Layers.js b/src/views/Dashboard/Architecture/Layers/Layers.js index 2b57d6d..c0c3e11 100644 --- a/src/views/Dashboard/Architecture/Layers/Layers.js +++ b/src/views/Dashboard/Architecture/Layers/Layers.js @@ -7,6 +7,7 @@ import { ResponsiveBubbleHtml } from "@nivo/circle-packing"; import LayersModel from "../../../../api/models/LayersModel"; import { Treebeard, decorators } from "react-treebeard"; import CustomHeader from "./CustomHeader"; +import CustomCardHeader from "../../CustomCardHeader/CustomCardHeader"; const treebeardCustomTheme = require("./TreebeardCustomTheme"); @@ -49,11 +50,23 @@ class Layers extends DashboardAbstract { return "Loading..."; } + console.log(this.state); + return (
+ From 0ba7d1ce5e7a18eb5098fab550b0bb331742d6b8 Mon Sep 17 00:00:00 2001 From: Jeremy Puchta Date: Mon, 31 Aug 2020 21:58:26 +0200 Subject: [PATCH 22/22] Cleanup --- package.json | 2 -- src/api/models/Hotspots.js | 3 --- src/views/Dashboard/Architecture/Dependencies/Dependencies.js | 1 + .../Dependencies/visualizations/DependencyChord.js | 1 - src/views/Dashboard/Architecture/Structure/Structure.js | 2 -- 5 files changed, 1 insertion(+), 8 deletions(-) diff --git a/package.json b/package.json index d9a58af..4bb5305 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "codecov": "^3.2.0", "core-js": "^2.6.5", "d3": "^5.9.2", - "d3-selection": "^1.4.1", "dateformat": "^3.0.3", "dns": "^0.2.2", "enzyme": "^3.9.0", @@ -57,7 +56,6 @@ "react-app-polyfill": "^0.2.2", "react-bootstrap-daterangepicker": "^4.1.0", "react-chartjs-2": "^2.7.4", - "react-d3-tree": "^1.16.1", "react-dom": "^16.8.5", "react-load-script": "0.0.6", "react-loadable": "^5.5.0", diff --git a/src/api/models/Hotspots.js b/src/api/models/Hotspots.js index ddca7f9..e18177e 100644 --- a/src/api/models/Hotspots.js +++ b/src/api/models/Hotspots.js @@ -109,14 +109,11 @@ class HotspotModel { } }); - console.log(flatData); hierarchicalData = cpHelper.circlePackingByName( projectName, flatData ); - console.log(hierarchicalData); cpHelper.normalizeHotspots(hierarchicalData); //this function works by reference - console.log(hierarchicalData); neo4jSession.close(); //normalize the root element diff --git a/src/views/Dashboard/Architecture/Dependencies/Dependencies.js b/src/views/Dashboard/Architecture/Dependencies/Dependencies.js index 389b926..995bacf 100644 --- a/src/views/Dashboard/Architecture/Dependencies/Dependencies.js +++ b/src/views/Dashboard/Architecture/Dependencies/Dependencies.js @@ -4,6 +4,7 @@ import CustomCardHeader from "../../CustomCardHeader/CustomCardHeader"; import DependencyChord from "./visualizations/DependencyChord"; import { CypherEditor } from "graph-app-kit/components/Editor"; import { Button, Row, Col, Card, CardBody } from "reactstrap"; + var AppDispatcher = require("../../../../AppDispatcher"); class ArchitectureDependencies extends DashboardAbstract { diff --git a/src/views/Dashboard/Architecture/Dependencies/visualizations/DependencyChord.js b/src/views/Dashboard/Architecture/Dependencies/visualizations/DependencyChord.js index 6fb7dfe..45f820d 100644 --- a/src/views/Dashboard/Architecture/Dependencies/visualizations/DependencyChord.js +++ b/src/views/Dashboard/Architecture/Dependencies/visualizations/DependencyChord.js @@ -39,7 +39,6 @@ class DependencyChord extends DashboardAbstract { } render() { - console.log(this.state); var redirect = super.render(); if (redirect.length > 0) { return redirect; diff --git a/src/views/Dashboard/Architecture/Structure/Structure.js b/src/views/Dashboard/Architecture/Structure/Structure.js index 7636d4b..47a185b 100644 --- a/src/views/Dashboard/Architecture/Structure/Structure.js +++ b/src/views/Dashboard/Architecture/Structure/Structure.js @@ -290,8 +290,6 @@ class ArchitectureStructure extends DashboardAbstract { return redirect; } - console.log(this.state.hotSpotData); - if (!this.state.hotSpotData.name) { return ""; }