diff --git a/README.md b/README.md index 400770a..2dde7d1 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/). 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/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/data/petclinic/Dockerfile b/data/petclinic/Dockerfile index 1749182..40e9e61 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} @@ -7,9 +7,9 @@ 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 && \ bin/neo4j start && sleep 5 && \ - tail -f logs/neo4j.log \ No newline at end of file + tail -f logs/neo4j.log 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/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); diff --git a/src/api/models/Hotspots.js b/src/api/models/Hotspots.js index 1a29e7c..e18177e 100644 --- a/src/api/models/Hotspots.js +++ b/src/api/models/Hotspots.js @@ -114,7 +114,6 @@ class HotspotModel { flatData ); cpHelper.normalizeHotspots(hierarchicalData); //this function works by reference - neo4jSession.close(); //normalize the root element diff --git a/src/api/models/LayersModel.js b/src/api/models/LayersModel.js new file mode 100644 index 0000000..b75f512 --- /dev/null +++ b/src/api/models/LayersModel.js @@ -0,0 +1,283 @@ +import { neo4jSession } from "../../views/Dashboard/AbstractDashboardComponent"; +import * as d3 from "d3"; + +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 " + + "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, dependent"; + + 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, + dependencyDefinitionQuery: dependencyDefinitionQuery, + validDependencies: [] + }; + } + + 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.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)) { + dependencies.push({ + id: record.get("dependent"), + dependency: record.get("dependency"), + isValid: false + }); + this.appendDependency( + treeData, + record.get("dependent"), + record.get("dependency"), + record.get("dependentLayer"), + record.get("dependencyLayer"), + false + ); + } else { + dependencies.push({ + id: record.get("dependent"), + dependency: record.get("dependency"), + isValid: true + }); + this.appendDependency( + treeData, + record.get("dependent"), + record.get("dependency"), + record.get("dependentLayer"), + record.get("dependencyLayer"), + true + ); + } + }); + }); + + neo4jSession + .run(this.state.layersQuery) + .then(result => { + result.records.forEach(record => { + 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.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, + record.get("child"), + record.get("layer"), + COLORS.INVALID_LEAF, + record.get("loc").low + ); + } else { + this.appendLeafNode( + visualizationData, + record.get("child"), + record.get("layer"), + COLORS.VALID_LEAF, + record.get("loc").low + ); + } + }); + }) + .then(() => { + visualizationRoot = d3 + .stratify() + .id(node => { + return node.id; + }) + .parentId(node => { + return node.parent; + })(visualizationData); + + treeRoot = d3 + .stratify() + .id(node => { + return node.id; + }) + .parentId(node => { + return node.parent; + })(treeData); + }) + .then(() => { + this.convertDataForVisualization(visualizationRoot); + this.convertDataForTree(treeRoot); + }) + .then(() => { + that.setState({ + visualizationData: visualizationRoot, + treeData: treeRoot, + dependencies: dependencies + }); + }) + .catch(e => { + console.error( + "Something went wrong while initializing the given data.", + e + ); + }); + } + + appendDependency( + dataset, + dependent, + dependency, + dependentLayer, + dependencyLayer, + isValid + ) { + dataset.push({ + id: dependent, + dependency: dependency, + parent: dependentLayer, + dependencyLayer: dependencyLayer, + isValid: isValid + }); + } + + dependencyIsValid(record) { + 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) { + 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]); + } + } + } + + 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; + }); + } + } + } + + appendTreeNode(dataset, id, parent) { + dataset.push({ + id: id, + parent: parent + }); + } + + appendVisualizationNode(dataset, id, parent, color) { + dataset.push({ + id: id, + parent: parent, + color: color + }); + } + + appendLeafNode(dataset, id, parent, color, loc) { + dataset.push({ + id: id, + parent: parent, + color: color, + loc: loc + }); + } + + nodeExists(nodes, nodeId) { + return nodes.some(node => node.id === nodeId); + } +} + +export default LayersModel; diff --git a/src/routes.js b/src/routes.js index a1e3551..2f6256f 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,86 +1,161 @@ -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