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
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/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 d058fa5..4bd6591 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; @@ -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 new file mode 100644 index 0000000..8a19d10 --- /dev/null +++ b/src/views/Dashboard/Architecture/Layers/CustomHeader.js @@ -0,0 +1,68 @@ +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"); + document + .getElementById(node.data.dependency) + .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"; + } else { + newBaseStyle.color = "#35a47f"; + } + } + + return ( +
onMouseOver(node)} + onMouseLeave={() => onMouseLeave(node)} + > + {node.name} +
+ ); +}; + +export default CustomHeader; diff --git a/src/views/Dashboard/Architecture/Layers/Layers.js b/src/views/Dashboard/Architecture/Layers/Layers.js new file mode 100644 index 0000000..c0c3e11 --- /dev/null +++ b/src/views/Dashboard/Architecture/Layers/Layers.js @@ -0,0 +1,146 @@ +import React from "react"; +import DashboardAbstract, { + databaseCredentialsProvided +} from "../../AbstractDashboardComponent"; +import { Row, Col, Card, CardBody } from "reactstrap"; +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"); + +class Layers extends DashboardAbstract { + constructor(props) { + super(props); + this.state = { + visualizationData: {}, + treeData: {}, + dependencies: [] + }; + + this.onToggle = this.onToggle.bind(this); + } + + componentDidMount() { + super.componentDidMount(); + + 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() { + if (!this.state.visualizationData.name) { + return "Loading..."; + } + + console.log(this.state); + + return ( +
+ + + + + + + + + + + + + + + + +
+ { + return node.color; + }} + animate={true} + enableLabel={false} + borderWidth={2} + tooltip={({ id }) => ( +
+ + {id} + +
+ )} + /> +
+
+
+ +
+
+
+ +
+
+ ); + } +} + +export default Layers; diff --git a/src/views/Dashboard/Architecture/Layers/TreebeardCustomTheme.js b/src/views/Dashboard/Architecture/Layers/TreebeardCustomTheme.js new file mode 100644 index 0000000..3fc03a8 --- /dev/null +++ b/src/views/Dashboard/Architecture/Layers/TreebeardCustomTheme.js @@ -0,0 +1,78 @@ +//'use strict'; + +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" + } + } + } +};