diff --git a/microcatalog.jdl b/microcatalog.jdl index acc5cf7..eacc5fe 100644 --- a/microcatalog.jdl +++ b/microcatalog.jdl @@ -1,3 +1,5 @@ +// Core entities + entity Team { name String required, teamLead String required, @@ -23,10 +25,10 @@ entity Dependency { description TextBlob } + relationship ManyToOne { Microservice{team required} to Team Microservice{status required} to Status Dependency{source required} to Microservice Dependency{target required} to Microservice } - diff --git a/pom.xml b/pom.xml index a308e53..f8b3ef6 100644 --- a/pom.xml +++ b/pom.xml @@ -79,6 +79,7 @@ 1.0.0 1.0.0 3.7.0.1746 + 1.5.0 ${project.build.directory}/jacoco/test ${jacoco.utReportFolder}/test.exec ${project.build.directory}/jacoco/integrationTest @@ -102,6 +103,11 @@ + + org.jgrapht + jgrapht-core + ${jgrapht.version} + io.github.jhipster jhipster-framework @@ -1172,13 +1178,17 @@ liquibase-maven-plugin src/main/resources/config/liquibase/master.xml - src/main/resources/config/liquibase/changelog/${maven.build.timestamp}_changelog.xml + + src/main/resources/config/liquibase/changelog/${maven.build.timestamp}_changelog.xml + ${env.JDBC_DATABASE_URL} ${env.JDBC_DATABASE_USERNAME} ${env.JDBC_DATABASE_PASSWORD} - hibernate:spring:com.github.microcatalog.domain?dialect=io.github.jhipster.domain.util.FixedPostgreSQL10Dialect&hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy + + hibernate:spring:com.github.microcatalog.domain?dialect=io.github.jhipster.domain.util.FixedPostgreSQL10Dialect&hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy + true debug false diff --git a/src/main/java/com/github/microcatalog/config/CacheConfiguration.java b/src/main/java/com/github/microcatalog/config/CacheConfiguration.java index 3dc3cad..026465e 100644 --- a/src/main/java/com/github/microcatalog/config/CacheConfiguration.java +++ b/src/main/java/com/github/microcatalog/config/CacheConfiguration.java @@ -2,6 +2,9 @@ import java.time.Duration; +import com.github.microcatalog.domain.custom.ReleaseGroup; +import com.github.microcatalog.domain.custom.ReleasePath; +import com.github.microcatalog.domain.custom.ReleaseStep; import org.ehcache.config.builders.*; import org.ehcache.jsr107.Eh107Configuration; @@ -52,6 +55,11 @@ public JCacheManagerCustomizer cacheManagerCustomizer() { createCache(cm, com.github.microcatalog.domain.Team.class.getName()); createCache(cm, com.github.microcatalog.domain.Status.class.getName()); createCache(cm, com.github.microcatalog.domain.Dependency.class.getName()); + createCache(cm, ReleaseStep.class.getName()); + createCache(cm, ReleaseGroup.class.getName()); + createCache(cm, ReleaseGroup.class.getName() + ".steps"); + createCache(cm, ReleasePath.class.getName()); + createCache(cm, ReleasePath.class.getName() + ".groups"); // jhipster-needle-ehcache-add-entry }; } diff --git a/src/main/java/com/github/microcatalog/domain/Microservice.java b/src/main/java/com/github/microcatalog/domain/Microservice.java index 5eb91e7..2444509 100644 --- a/src/main/java/com/github/microcatalog/domain/Microservice.java +++ b/src/main/java/com/github/microcatalog/domain/Microservice.java @@ -29,7 +29,7 @@ public class Microservice implements Serializable { @Column(name = "name", nullable = false) private String name; - + @Lob @Type(type = "org.hibernate.type.TextType") @Column(name = "description", nullable = false) @@ -188,7 +188,11 @@ public boolean equals(Object o) { @Override public int hashCode() { - return 31; + if (id == null) { + return 31; + } + + return id.intValue(); } // prettier-ignore diff --git a/src/main/java/com/github/microcatalog/domain/custom/ReleaseGroup.java b/src/main/java/com/github/microcatalog/domain/custom/ReleaseGroup.java new file mode 100644 index 0000000..ad497ef --- /dev/null +++ b/src/main/java/com/github/microcatalog/domain/custom/ReleaseGroup.java @@ -0,0 +1,38 @@ +package com.github.microcatalog.domain.custom; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import java.util.HashSet; +import java.util.Set; + +/** + * A ReleaseGroup. + */ +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +public class ReleaseGroup { + private Set steps = new HashSet<>(); + + public Set getSteps() { + return steps; + } + + public ReleaseGroup steps(Set releaseSteps) { + this.steps = releaseSteps; + return this; + } + + public ReleaseGroup addSteps(ReleaseStep releaseStep) { + this.steps.add(releaseStep); + return this; + } + + public ReleaseGroup removeSteps(ReleaseStep releaseStep) { + this.steps.remove(releaseStep); + return this; + } + + public void setSteps(Set releaseSteps) { + this.steps = releaseSteps; + } +} diff --git a/src/main/java/com/github/microcatalog/domain/custom/ReleasePath.java b/src/main/java/com/github/microcatalog/domain/custom/ReleasePath.java new file mode 100644 index 0000000..f124a29 --- /dev/null +++ b/src/main/java/com/github/microcatalog/domain/custom/ReleasePath.java @@ -0,0 +1,68 @@ +package com.github.microcatalog.domain.custom; + +import com.github.microcatalog.domain.Microservice; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +/** + * A ReleasePath. + */ +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +public class ReleasePath { + private Instant createdOn; + private List groups = new ArrayList<>(); + private Microservice target; + + public Instant getCreatedOn() { + return createdOn; + } + + public ReleasePath createdOn(Instant createdOn) { + this.createdOn = createdOn; + return this; + } + + public void setCreatedOn(Instant createdOn) { + this.createdOn = createdOn; + } + + public List getGroups() { + return groups; + } + + public ReleasePath groups(List releaseGroups) { + this.groups = releaseGroups; + return this; + } + + public ReleasePath addGroups(ReleaseGroup releaseGroup) { + this.groups.add(releaseGroup); + return this; + } + + public ReleasePath removeGroups(ReleaseGroup releaseGroup) { + this.groups.remove(releaseGroup); + return this; + } + + public void setGroups(List releaseGroups) { + this.groups = releaseGroups; + } + + public Microservice getTarget() { + return target; + } + + public ReleasePath target(Microservice microservice) { + this.target = microservice; + return this; + } + + public void setTarget(Microservice microservice) { + this.target = microservice; + } +} diff --git a/src/main/java/com/github/microcatalog/domain/custom/ReleaseStep.java b/src/main/java/com/github/microcatalog/domain/custom/ReleaseStep.java new file mode 100644 index 0000000..09844b8 --- /dev/null +++ b/src/main/java/com/github/microcatalog/domain/custom/ReleaseStep.java @@ -0,0 +1,42 @@ +package com.github.microcatalog.domain.custom; + +import com.github.microcatalog.domain.Microservice; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import java.util.List; + +/** + * A ReleaseStep. + */ +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +public class ReleaseStep { + private Microservice workItem; + private List parentWorkItems; + + public Microservice getWorkItem() { + return workItem; + } + + public ReleaseStep workItem(Microservice microservice) { + this.workItem = microservice; + return this; + } + + public void setWorkItem(Microservice microservice) { + this.workItem = microservice; + } + + public List getParentWorkItems() { + return parentWorkItems; + } + + public ReleaseStep parentWorkItems(List microservices) { + this.parentWorkItems = microservices; + return this; + } + + public void setParentWorkItems(List parentWorkItems) { + this.parentWorkItems = parentWorkItems; + } +} diff --git a/src/main/java/com/github/microcatalog/repository/DependencyRepository.java b/src/main/java/com/github/microcatalog/repository/DependencyRepository.java index 9729198..07062ba 100644 --- a/src/main/java/com/github/microcatalog/repository/DependencyRepository.java +++ b/src/main/java/com/github/microcatalog/repository/DependencyRepository.java @@ -1,8 +1,7 @@ package com.github.microcatalog.repository; import com.github.microcatalog.domain.Dependency; - -import org.springframework.data.jpa.repository.*; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; /** diff --git a/src/main/java/com/github/microcatalog/service/custom/ReleasePathCustomService.java b/src/main/java/com/github/microcatalog/service/custom/ReleasePathCustomService.java new file mode 100644 index 0000000..69de34a --- /dev/null +++ b/src/main/java/com/github/microcatalog/service/custom/ReleasePathCustomService.java @@ -0,0 +1,142 @@ +package com.github.microcatalog.service.custom; + +import com.github.microcatalog.domain.*; +import com.github.microcatalog.domain.custom.ReleaseGroup; +import com.github.microcatalog.domain.custom.ReleasePath; +import com.github.microcatalog.domain.custom.ReleaseStep; +import com.github.microcatalog.repository.DependencyRepository; +import com.github.microcatalog.repository.MicroserviceRepository; +import org.jgrapht.Graph; +import org.jgrapht.alg.connectivity.ConnectivityInspector; +import org.jgrapht.alg.cycle.CycleDetector; +import org.jgrapht.graph.AsSubgraph; +import org.jgrapht.graph.DefaultDirectedGraph; +import org.jgrapht.graph.DefaultEdge; +import org.jgrapht.graph.EdgeReversedGraph; +import org.jgrapht.traverse.DepthFirstIterator; +import org.jgrapht.traverse.GraphIterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Service for release path calculation + */ +@Service +@Transactional +public class ReleasePathCustomService { + + private final Logger log = LoggerFactory.getLogger(ReleasePathCustomService.class); + + private final MicroserviceRepository microserviceRepository; + private final DependencyRepository dependencyRepository; + + public ReleasePathCustomService(MicroserviceRepository microserviceRepository, + DependencyRepository dependencyRepository) { + this.microserviceRepository = microserviceRepository; + this.dependencyRepository = dependencyRepository; + } + + public Optional getReleasePath(final Long microserviceId) { + final List microservices = microserviceRepository.findAll(); + final List dependencies = dependencyRepository.findAll(); + final Optional maybeTarget = + microservices.stream().filter(m -> Objects.equals(m.getId(), microserviceId)).findFirst(); + + if (!maybeTarget.isPresent()) { + return Optional.empty(); + } + + final Graph graph = new DefaultDirectedGraph<>(DefaultEdge.class); + + microservices.forEach(m -> graph.addVertex(m.getId())); + dependencies.forEach(d -> graph.addEdge(d.getSource().getId(), d.getTarget().getId())); + + final ConnectivityInspector inspector = new ConnectivityInspector<>(graph); + final Microservice target = maybeTarget.get(); + final Set connectedSet = inspector.connectedSetOf(target.getId()); + + // Connected subgraph, that contains target microservice + final AsSubgraph targetSubgraph = new AsSubgraph<>(graph, connectedSet); + log.debug("Connected subgraph, that contains target microservice: {}", targetSubgraph); + + final CycleDetector cycleDetector = new CycleDetector<>(targetSubgraph); + if (cycleDetector.detectCycles()) { + final Set cycles = cycleDetector.findCycles(); + throw new IllegalArgumentException(String.format("There are cyclic dependencies between microservices : %s", cycles)); + } + + final Set pathMicroservices = new HashSet<>(); + GraphIterator iterator = new DepthFirstIterator<>(targetSubgraph, target.getId()); + while (iterator.hasNext()) { + pathMicroservices.add(iterator.next()); + } + + // TODO new use-case and visualisation + // For each element of pathSet calculate all nodes who depends on items from pathSet. Microservices which will be possibly affected if we build target and it's direct dependencies + + final Graph pathGraph = new AsSubgraph<>(targetSubgraph, pathMicroservices); + log.debug("Connected subgraph, which contains all paths from target microservice to it's dependencies {}", pathGraph); + + final Graph reversed = new EdgeReversedGraph<>(pathGraph); + final Graph reversedCopy = new AsSubgraph<>(reversed); + + return Optional.of(convert(reversedCopy, microservices, target)); + } + + private ReleasePath convert(final Graph graph, final List microservices, final Microservice target) { + final ReleasePath result = new ReleasePath(); + result.setCreatedOn(Instant.now()); + result.setTarget(target); + + final List groups = new ArrayList<>(); + final Map microserviceMap = microservices.stream() + .collect(Collectors.toMap(Microservice::getId, m -> m)); + + do { + final List verticesWithoutIncomingEdges = graph.vertexSet().stream() + .filter(v -> graph.incomingEdgesOf(v).isEmpty()) + .collect(Collectors.toList()); + log.debug("Leaves: {}", verticesWithoutIncomingEdges); + + final ReleaseGroup group = new ReleaseGroup(); + group.setSteps(convertSteps(microserviceMap, verticesWithoutIncomingEdges, graph)); + groups.add(group); + + verticesWithoutIncomingEdges.forEach(graph::removeVertex); + } while (!graph.vertexSet().isEmpty()); + + result.setGroups(groups); + + return result; + } + + private Set convertSteps(final Map microserviceMap, + final List microserviceIds, + final Graph graph) { + final Set result = new HashSet<>(); + + microserviceIds.forEach(id -> { + final List parentWorkItems = new ArrayList<>(); + + final Set outgoingEdges = graph.outgoingEdgesOf(id); + for (DefaultEdge e : outgoingEdges) { + final Long edgeTarget = graph.getEdgeTarget(e); + parentWorkItems.add(microserviceMap.get(edgeTarget)); + } + + result.add( + new ReleaseStep() + .workItem(microserviceMap.get(id)) + .parentWorkItems(parentWorkItems) + ); + }); + + return result; + } +} diff --git a/src/main/java/com/github/microcatalog/utils/DependencyBuilder.java b/src/main/java/com/github/microcatalog/utils/DependencyBuilder.java new file mode 100644 index 0000000..07097f0 --- /dev/null +++ b/src/main/java/com/github/microcatalog/utils/DependencyBuilder.java @@ -0,0 +1,34 @@ +package com.github.microcatalog.utils; + +import com.github.microcatalog.domain.Dependency; + +public class DependencyBuilder { + private Long id; + private Long source; + private Long target; + + public DependencyBuilder withId(Long id) { + this.id = id; + return this; + } + + public DependencyBuilder withSource(Long sourceId) { + this.source = sourceId; + return this; + } + + public DependencyBuilder withTarget(Long targetId) { + this.target = targetId; + return this; + } + + public Dependency build() { + final Dependency result = new Dependency(); + + result.setId(id); + result.setSource(new MicroserviceBuilder().withId(source).build()); + result.setTarget(new MicroserviceBuilder().withId(target).build()); + + return result; + } +} diff --git a/src/main/java/com/github/microcatalog/utils/MicroserviceBuilder.java b/src/main/java/com/github/microcatalog/utils/MicroserviceBuilder.java new file mode 100644 index 0000000..4e448fd --- /dev/null +++ b/src/main/java/com/github/microcatalog/utils/MicroserviceBuilder.java @@ -0,0 +1,18 @@ +package com.github.microcatalog.utils; + +import com.github.microcatalog.domain.Microservice; + +public class MicroserviceBuilder { + private Long id; + + public MicroserviceBuilder withId(Long id) { + this.id = id; + return this; + } + + public Microservice build() { + final Microservice result = new Microservice(); + result.setId(id); + return result; + } +} diff --git a/src/main/java/com/github/microcatalog/web/rest/custom/ReleasePathCustomResource.java b/src/main/java/com/github/microcatalog/web/rest/custom/ReleasePathCustomResource.java new file mode 100644 index 0000000..21a97fc --- /dev/null +++ b/src/main/java/com/github/microcatalog/web/rest/custom/ReleasePathCustomResource.java @@ -0,0 +1,43 @@ +package com.github.microcatalog.web.rest.custom; + +import com.github.microcatalog.domain.custom.ReleasePath; +import com.github.microcatalog.service.custom.ReleasePathCustomService; +import io.github.jhipster.web.util.ResponseUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Optional; + +/** + * REST controller for calculation of release paths {@link ReleasePath}. + */ +@RestController +@RequestMapping("/api") +public class ReleasePathCustomResource { + + private final Logger log = LoggerFactory.getLogger(ReleasePathCustomResource.class); + + private final ReleasePathCustomService service; + + public ReleasePathCustomResource(ReleasePathCustomService service) { + this.service = service; + } + + /** + * {@code GET /release-path/microservice/:microserviceId} : get the "microserviceId" releasePath. + * + * @param microserviceId id of Microservice for which releasePath should be calculated + * @return release path calculated for given microservice + */ + @GetMapping("/release-path/microservice/{microserviceId}") + public ResponseEntity getPath(@PathVariable Long microserviceId) { + log.debug("REST request to get ReleasePath for microserviceId : {}", microserviceId); + final Optional releasePath = service.getReleasePath(microserviceId); + return ResponseUtil.wrapOrNotFound(releasePath); + } +} diff --git a/src/main/webapp/app/dashboard/dashboard-routing.module.ts b/src/main/webapp/app/dashboard/dashboard-routing.module.ts index e29f124..8dd37ea 100644 --- a/src/main/webapp/app/dashboard/dashboard-routing.module.ts +++ b/src/main/webapp/app/dashboard/dashboard-routing.module.ts @@ -8,6 +8,10 @@ import { RouterModule } from '@angular/router'; path: 'dependencies', loadChildren: () => import('./dependency-dashboard/dependency-dashboard.module').then(m => m.DependencyDashboardModule), }, + { + path: 'release-path', + loadChildren: () => import('./release-path-dashboard/release-path-dashboard.module').then(m => m.ReleasePathDashboardModule), + }, ]), ], }) diff --git a/src/main/webapp/app/dashboard/dependency-dashboard/dependency-dashboard.component.html b/src/main/webapp/app/dashboard/dependency-dashboard/dependency-dashboard.component.html index a194621..aa788a5 100644 --- a/src/main/webapp/app/dashboard/dependency-dashboard/dependency-dashboard.component.html +++ b/src/main/webapp/app/dashboard/dependency-dashboard/dependency-dashboard.component.html @@ -22,6 +22,11 @@ Create microservice +
+ +
diff --git a/src/main/webapp/app/dashboard/dependency-dashboard/dependency-dashboard.component.scss b/src/main/webapp/app/dashboard/dependency-dashboard/dependency-dashboard.component.scss index 0ad59f3..e69de29 100644 --- a/src/main/webapp/app/dashboard/dependency-dashboard/dependency-dashboard.component.scss +++ b/src/main/webapp/app/dashboard/dependency-dashboard/dependency-dashboard.component.scss @@ -1,3 +0,0 @@ -.vis-network-full-height { - height: 70vh; -} diff --git a/src/main/webapp/app/dashboard/dependency-dashboard/dependency-dashboard.component.ts b/src/main/webapp/app/dashboard/dependency-dashboard/dependency-dashboard.component.ts index cea85f7..9db3eb4 100644 --- a/src/main/webapp/app/dashboard/dependency-dashboard/dependency-dashboard.component.ts +++ b/src/main/webapp/app/dashboard/dependency-dashboard/dependency-dashboard.component.ts @@ -10,6 +10,8 @@ import { map } from 'rxjs/operators'; import { ISelectPayload, SelectPayload } from '../../shared/vis/events/VisEvents'; import { DeleteDialogService } from './delete-dialog.service'; import { FilterContext, GraphBuilderService } from './graph-builder.service'; +import { ReleasePathCustomService } from '../../entities/release-path/custom/release-path-custom.service'; +import { VisNetworkService } from '../../shared/vis/vis-network.service'; @Component({ selector: 'jhi-dependency-dashboard', @@ -19,10 +21,10 @@ import { FilterContext, GraphBuilderService } from './graph-builder.service'; export class DependencyDashboardComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild('visNetwork', { static: false }) visNetwork!: ElementRef; + networkInstance: any; subscription?: Subscription; - networkInstance: any; searchValue?: IMicroservice; onlyIncomingFilter = true; onlyOutgoingFilter = true; @@ -34,6 +36,8 @@ export class DependencyDashboardComponent implements OnInit, AfterViewInit, OnDe protected eventManager: JhiEventManager, protected dependencyService: DependencyService, protected microserviceService: MicroserviceService, + protected releasePathService: ReleasePathCustomService, + protected visNetworkService: VisNetworkService, protected graphBuilderService: GraphBuilderService, protected createDependencyDialogService: CreateDependencyDialogService, protected deleteDialogService: DeleteDialogService @@ -51,7 +55,7 @@ export class DependencyDashboardComponent implements OnInit, AfterViewInit, OnDe ngAfterViewInit(): void { const container = this.visNetwork; - this.networkInstance = this.graphBuilderService.createNetwork(container); + this.networkInstance = this.visNetworkService.createNetwork(container); // See Network.d.ts -> NetworkEvents this.networkInstance.on('selectNode', (params: any) => { @@ -114,7 +118,15 @@ export class DependencyDashboardComponent implements OnInit, AfterViewInit, OnDe this.refreshGraph(); } - buildDeploymentPath(): void {} + buildReleasePath(): void {} + + selectedNodeId(): number { + if (this.nodeSelection && this.nodeSelection.hasNodes()) { + return this.nodeSelection.firstNode(); + } + + return -1; + } createDependency(): void { // Use selected microservice as dependency's start diff --git a/src/main/webapp/app/dashboard/dependency-dashboard/graph-builder.service.ts b/src/main/webapp/app/dashboard/dependency-dashboard/graph-builder.service.ts index a1d7318..901d9f2 100644 --- a/src/main/webapp/app/dashboard/dependency-dashboard/graph-builder.service.ts +++ b/src/main/webapp/app/dashboard/dependency-dashboard/graph-builder.service.ts @@ -1,8 +1,8 @@ -import { ElementRef, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { DataSet } from 'vis-data/peer'; import { IDependency } from '../../shared/model/dependency.model'; import { IMicroservice } from '../../shared/model/microservice.model'; -import { Network, Options } from 'vis-network/peer'; +import { Network } from 'vis-network/peer'; import { DependencyService } from '../../entities/dependency/dependency.service'; import { MicroserviceService } from '../../entities/microservice/microservice.service'; import { forkJoin } from 'rxjs'; @@ -27,34 +27,6 @@ class GraphContext { export class GraphBuilderService { constructor(protected dependencyService: DependencyService, protected microserviceService: MicroserviceService) {} - createNetwork(element: ElementRef, options?: Options): Network { - const defaultOptions = { - height: '100%', - width: '100%', - nodes: { - shape: 'hexagon', - font: { - color: 'white', - }, - }, - clickToUse: false, - edges: { - smooth: false, - arrows: { - to: { - enabled: true, - type: 'vee', - }, - }, - }, - interaction: { - multiselect: true, - }, - }; - - return new Network(element.nativeElement, {}, options || defaultOptions); - } - refreshGraph(network: Network, filterContext: FilterContext): void { const dependencies$ = this.dependencyService.query(); const microservices$ = this.microserviceService.query(); diff --git a/src/main/webapp/app/dashboard/release-path-dashboard/node-colors.service.ts b/src/main/webapp/app/dashboard/release-path-dashboard/node-colors.service.ts new file mode 100644 index 0000000..2c28b10 --- /dev/null +++ b/src/main/webapp/app/dashboard/release-path-dashboard/node-colors.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class NodeColorsService { + private defaultColor = '#97C2FC'; + private activeColor = '#61dd45'; + private colors: string[] = ['#d33682', '#cb4b16', '#268bd2', '#2aa198', '#839496', '#b58900', '#002b36', '#9b479f']; + + constructor() {} + + getActiveColor(): string { + return this.activeColor; + } + + getColor(index: number): string { + if (index < 0 || index > this.colors.length) { + return this.defaultColor; + } else { + return this.colors[index]; + } + } +} diff --git a/src/main/webapp/app/dashboard/release-path-dashboard/release-graph/release-graph.component.html b/src/main/webapp/app/dashboard/release-path-dashboard/release-graph/release-graph.component.html new file mode 100644 index 0000000..2971e26 --- /dev/null +++ b/src/main/webapp/app/dashboard/release-path-dashboard/release-graph/release-graph.component.html @@ -0,0 +1 @@ +
diff --git a/src/main/webapp/app/dashboard/release-path-dashboard/release-graph/release-graph.component.scss b/src/main/webapp/app/dashboard/release-path-dashboard/release-graph/release-graph.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/main/webapp/app/dashboard/release-path-dashboard/release-graph/release-graph.component.ts b/src/main/webapp/app/dashboard/release-path-dashboard/release-graph/release-graph.component.ts new file mode 100644 index 0000000..e8678d3 --- /dev/null +++ b/src/main/webapp/app/dashboard/release-path-dashboard/release-graph/release-graph.component.ts @@ -0,0 +1,72 @@ +import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { VisNetworkService } from '../../../shared/vis/vis-network.service'; +import { NodeColorsService } from '../node-colors.service'; +import { IReleasePath } from '../../../shared/model/release-path.model'; +import { DataSet } from 'vis-data/peer'; + +@Component({ + selector: 'jhi-release-graph', + templateUrl: './release-graph.component.html', + styleUrls: ['./release-graph.component.scss'], +}) +export class ReleaseGraphComponent implements OnInit, AfterViewInit { + @ViewChild('visNetwork', { static: false }) + visNetwork!: ElementRef; + networkInstance: any; + + releasePath?: IReleasePath; + + constructor( + protected activatedRoute: ActivatedRoute, + protected visNetworkService: VisNetworkService, + protected nodeColorsService: NodeColorsService + ) {} + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ releasePath }) => { + this.releasePath = releasePath; + }); + } + + ngAfterViewInit(): void { + const container = this.visNetwork; + + this.networkInstance = this.visNetworkService.createNetwork(container); + + const nodes = new DataSet(); + const edges = new DataSet(); + const targetId = this.releasePath?.target?.id; + + if (this.releasePath) { + let groupIndex = 0; + this.releasePath.groups?.forEach(g => { + g.steps?.forEach(s => { + const workItemId = s.workItem?.id; + + let nodeColor = this.nodeColorsService.getColor(groupIndex); + if (workItemId === targetId) { + nodeColor = this.nodeColorsService.getActiveColor(); + } + + nodes.add({ + id: workItemId, + label: s.workItem?.name, + color: nodeColor, + }); + + s.parentWorkItems?.forEach(pw => { + edges.add({ + from: workItemId, + to: pw.id, + }); + }); + }); + + ++groupIndex; + }); + } + + this.networkInstance.setData({ nodes, edges }); + } +} diff --git a/src/main/webapp/app/dashboard/release-path-dashboard/release-graph/release-graph.module.ts b/src/main/webapp/app/dashboard/release-path-dashboard/release-graph/release-graph.module.ts new file mode 100644 index 0000000..114dc4c --- /dev/null +++ b/src/main/webapp/app/dashboard/release-path-dashboard/release-graph/release-graph.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { MicrocatalogSharedModule } from 'app/shared/shared.module'; +import { ReleaseGraphComponent } from './release-graph.component'; + +@NgModule({ + declarations: [ReleaseGraphComponent], + imports: [MicrocatalogSharedModule], + exports: [ReleaseGraphComponent], +}) +export class ReleaseGraphModule {} diff --git a/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.component.html b/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.component.html new file mode 100644 index 0000000..37cc5b8 --- /dev/null +++ b/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.component.html @@ -0,0 +1,8 @@ +
+
+ +
+
+ +
+
diff --git a/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.component.scss b/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.component.ts b/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.component.ts new file mode 100644 index 0000000..ea639ec --- /dev/null +++ b/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.component.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'jhi-release-path-dashboard', + templateUrl: './release-path-dashboard.component.html', + styleUrls: ['./release-path-dashboard.component.scss'], +}) +export class ReleasePathDashboardComponent implements OnInit { + constructor() {} + + ngOnInit(): void {} +} diff --git a/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.module.ts b/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.module.ts new file mode 100644 index 0000000..32a3dfe --- /dev/null +++ b/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { releasePathDashboardRoute } from './release-path-dashboard.route'; +import { MicrocatalogSharedModule } from '../../shared/shared.module'; +import { ReleasePathDashboardComponent } from './release-path-dashboard.component'; +import { ReleasePathModule } from './release-path/release-path.module'; +import { ReleaseGraphModule } from './release-graph/release-graph.module'; + +@NgModule({ + imports: [ReleasePathModule, ReleaseGraphModule, MicrocatalogSharedModule, RouterModule.forChild(releasePathDashboardRoute)], + declarations: [ReleasePathDashboardComponent], +}) +export class ReleasePathDashboardModule {} diff --git a/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.route.ts b/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.route.ts new file mode 100644 index 0000000..8e5dedb --- /dev/null +++ b/src/main/webapp/app/dashboard/release-path-dashboard/release-path-dashboard.route.ts @@ -0,0 +1,51 @@ +import { ActivatedRouteSnapshot, Resolve, Router, Routes } from '@angular/router'; +import { ReleasePathDashboardComponent } from './release-path-dashboard.component'; +import { Injectable } from '@angular/core'; +import { EMPTY, Observable, of } from 'rxjs'; +import { catchError, flatMap } from 'rxjs/operators'; +import { HttpResponse } from '@angular/common/http'; +import { IReleasePath, ReleasePath } from '../../shared/model/release-path.model'; +import { ReleasePathCustomService } from '../../entities/release-path/custom/release-path-custom.service'; +import { Authority } from '../../shared/constants/authority.constants'; +import { UserRouteAccessService } from '../../core/auth/user-route-access-service'; + +@Injectable({ providedIn: 'root' }) +export class ReleasePathResolve implements Resolve { + constructor(private service: ReleasePathCustomService, private router: Router) {} + + resolve(route: ActivatedRouteSnapshot): Observable | Observable { + const id = route.params['id']; + if (id) { + return this.service.find(id).pipe( + flatMap((releasePath: HttpResponse) => { + if (releasePath.body) { + return of(releasePath.body); + } else { + this.router.navigate(['404']); + return EMPTY; + } + }), + catchError(error => { + alert('Error building release path. ' + error.error.detail); + return EMPTY; + }) + ); + } + return of(new ReleasePath()); + } +} + +export const releasePathDashboardRoute: Routes = [ + { + path: ':id', + component: ReleasePathDashboardComponent, + resolve: { + releasePath: ReleasePathResolve, + }, + data: { + authorities: [Authority.USER], + pageTitle: 'microcatalogApp.microservice.home.title', + }, + canActivate: [UserRouteAccessService], + }, +]; diff --git a/src/main/webapp/app/dashboard/release-path-dashboard/release-path/release-path.component.html b/src/main/webapp/app/dashboard/release-path-dashboard/release-path/release-path.component.html new file mode 100644 index 0000000..2971e26 --- /dev/null +++ b/src/main/webapp/app/dashboard/release-path-dashboard/release-path/release-path.component.html @@ -0,0 +1 @@ +
diff --git a/src/main/webapp/app/dashboard/release-path-dashboard/release-path/release-path.component.scss b/src/main/webapp/app/dashboard/release-path-dashboard/release-path/release-path.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/main/webapp/app/dashboard/release-path-dashboard/release-path/release-path.component.ts b/src/main/webapp/app/dashboard/release-path-dashboard/release-path/release-path.component.ts new file mode 100644 index 0000000..04165a5 --- /dev/null +++ b/src/main/webapp/app/dashboard/release-path-dashboard/release-path/release-path.component.ts @@ -0,0 +1,124 @@ +import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { IReleasePath } from '../../../shared/model/release-path.model'; +import { ActivatedRoute } from '@angular/router'; +import { VisNetworkService } from '../../../shared/vis/vis-network.service'; +import { NodeColorsService } from '../node-colors.service'; +import { DataSet } from 'vis-data/peer'; +import { IReleaseGroup } from '../../../shared/model/release-group.model'; + +@Component({ + selector: 'jhi-release-path', + templateUrl: './release-path.component.html', + styleUrls: ['./release-path.component.scss'], +}) +export class ReleasePathComponent implements OnInit, AfterViewInit { + @ViewChild('visNetwork', { static: false }) + visNetwork!: ElementRef; + networkInstance: any; + + releasePath?: IReleasePath; + + constructor( + protected activatedRoute: ActivatedRoute, + protected visNetworkService: VisNetworkService, + protected nodeColorsService: NodeColorsService + ) {} + + ngOnInit(): void { + this.activatedRoute.data.subscribe(({ releasePath }) => { + this.releasePath = releasePath; + }); + } + + ngAfterViewInit(): void { + const container = this.visNetwork; + + const options = { + height: '100%', + width: '100%', + nodes: { + shape: 'box', + font: { + color: 'white', + }, + }, + clickToUse: false, + edges: { + smooth: false, + arrows: { + to: { + enabled: true, + type: 'vee', + }, + }, + }, + interaction: { + multiselect: true, + }, + layout: { + hierarchical: {}, + }, + }; + + this.networkInstance = this.visNetworkService.createNetwork(container, options); + + const nodes = new DataSet(); + const edges = new DataSet(); + const targetId = this.releasePath?.target?.id; + + if (this.releasePath && this.releasePath.groups) { + let groupIndex = 0; + const groups = this.releasePath.groups; + const groupsCount = groups.length; + this.releasePath.groups?.forEach(g => { + let nodeColor = this.nodeColorsService.getColor(groupIndex); + if (this.containsTargetMicroservice(g, targetId)) { + nodeColor = this.nodeColorsService.getActiveColor(); + } + + nodes.add({ + id: groupIndex, + label: this.generateLabel(g, groupIndex), + color: nodeColor, + }); + + if (groupIndex < groupsCount - 1) { + edges.add({ + from: groupIndex, + to: groupIndex + 1, + }); + } + + ++groupIndex; + }); + } + + this.networkInstance.setData({ nodes, edges }); + } + + /** + * Checks if given IReleaseGroup contains target microservice, for which release path is built + * @param group with workItems + * @param targetId of target microservice, for which release path is built + */ + private containsTargetMicroservice(group: IReleaseGroup, targetId?: number): boolean { + const step = group.steps?.filter(s => s.workItem?.id === targetId).pop(); + if (step) { + return true; + } else { + return false; + } + } + + private generateLabel(group: IReleaseGroup, groupIndex: number): string { + let label = groupIndex + 1 + '. '; + + group.steps?.forEach(s => { + label = label + '[' + s.workItem?.name + '], '; + }); + + label = label.substr(0, label.length - 2); + + return label; + } +} diff --git a/src/main/webapp/app/dashboard/release-path-dashboard/release-path/release-path.module.ts b/src/main/webapp/app/dashboard/release-path-dashboard/release-path/release-path.module.ts new file mode 100644 index 0000000..a06d61f --- /dev/null +++ b/src/main/webapp/app/dashboard/release-path-dashboard/release-path/release-path.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { MicrocatalogSharedModule } from 'app/shared/shared.module'; +import { ReleasePathComponent } from './release-path.component'; + +@NgModule({ + declarations: [ReleasePathComponent], + imports: [MicrocatalogSharedModule], + exports: [ReleasePathComponent], +}) +export class ReleasePathModule {} diff --git a/src/main/webapp/app/entities/release-path/custom/release-path-custom.service.ts b/src/main/webapp/app/entities/release-path/custom/release-path-custom.service.ts new file mode 100644 index 0000000..a0f1cbd --- /dev/null +++ b/src/main/webapp/app/entities/release-path/custom/release-path-custom.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { SERVER_API_URL } from '../../../app.constants'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { IReleasePath } from '../../../shared/model/release-path.model'; +import { map } from 'rxjs/operators'; +import * as moment from 'moment'; + +type EntityResponseType = HttpResponse; + +@Injectable({ + providedIn: 'root', +}) +export class ReleasePathCustomService { + public resourceUrl = SERVER_API_URL + 'api/release-path'; + + constructor(protected http: HttpClient) {} + + find(microserviceId: number): Observable { + return this.http + .get(`${this.resourceUrl}/microservice/${microserviceId}`, { observe: 'response' }) + .pipe(map((res: EntityResponseType) => this.convertDateFromServer(res))); + } + + protected convertDateFromServer(res: EntityResponseType): EntityResponseType { + if (res.body) { + res.body.createdOn = res.body.createdOn ? moment(res.body.createdOn) : undefined; + } + return res; + } +} diff --git a/src/main/webapp/app/shared/model/release-group.model.ts b/src/main/webapp/app/shared/model/release-group.model.ts new file mode 100644 index 0000000..12413f5 --- /dev/null +++ b/src/main/webapp/app/shared/model/release-group.model.ts @@ -0,0 +1,9 @@ +import { IReleaseStep } from 'app/shared/model/release-step.model'; + +export interface IReleaseGroup { + steps?: IReleaseStep[]; +} + +export class ReleaseGroup implements IReleaseGroup { + constructor(public steps?: IReleaseStep[]) {} +} diff --git a/src/main/webapp/app/shared/model/release-path.model.ts b/src/main/webapp/app/shared/model/release-path.model.ts new file mode 100644 index 0000000..8d401f8 --- /dev/null +++ b/src/main/webapp/app/shared/model/release-path.model.ts @@ -0,0 +1,13 @@ +import { Moment } from 'moment'; +import { IReleaseGroup } from 'app/shared/model/release-group.model'; +import { IMicroservice } from 'app/shared/model/microservice.model'; + +export interface IReleasePath { + createdOn?: Moment; + groups?: IReleaseGroup[]; + target?: IMicroservice; +} + +export class ReleasePath implements IReleasePath { + constructor(public createdOn?: Moment, public groups?: IReleaseGroup[], public target?: IMicroservice) {} +} diff --git a/src/main/webapp/app/shared/model/release-step.model.ts b/src/main/webapp/app/shared/model/release-step.model.ts new file mode 100644 index 0000000..9e37d65 --- /dev/null +++ b/src/main/webapp/app/shared/model/release-step.model.ts @@ -0,0 +1,10 @@ +import { IMicroservice } from 'app/shared/model/microservice.model'; + +export interface IReleaseStep { + workItem?: IMicroservice; + parentWorkItems?: IMicroservice[]; +} + +export class ReleaseStep implements IReleaseStep { + constructor(public workItem?: IMicroservice, public parentWorkItems?: IMicroservice[]) {} +} diff --git a/src/main/webapp/app/shared/vis/vis-network.service.ts b/src/main/webapp/app/shared/vis/vis-network.service.ts new file mode 100644 index 0000000..c2d5a5d --- /dev/null +++ b/src/main/webapp/app/shared/vis/vis-network.service.ts @@ -0,0 +1,35 @@ +import { ElementRef, Injectable } from '@angular/core'; +import { Network, Options } from 'vis-network/peer'; + +@Injectable({ + providedIn: 'root', +}) +export class VisNetworkService { + createNetwork(element: ElementRef, options?: Options): Network { + const defaultOptions = { + height: '100%', + width: '100%', + nodes: { + shape: 'hexagon', + font: { + color: 'white', + }, + }, + clickToUse: false, + edges: { + smooth: false, + arrows: { + to: { + enabled: true, + type: 'vee', + }, + }, + }, + interaction: { + multiselect: true, + }, + }; + + return new Network(element.nativeElement, {}, options || defaultOptions); + } +} diff --git a/src/main/webapp/content/scss/global.scss b/src/main/webapp/content/scss/global.scss index c451db0..b6eff05 100644 --- a/src/main/webapp/content/scss/global.scss +++ b/src/main/webapp/content/scss/global.scss @@ -3,6 +3,10 @@ @import '~bootswatch/dist/solar/variables'; @import '~bootstrap/scss/variables'; +.vis-network-full-height { + height: 70vh; +} + /* ============================================================== Bootstrap tweaks ===============================================================*/ diff --git a/src/test/java/com/github/microcatalog/service/custom/ReleasePathCustomServiceTest.java b/src/test/java/com/github/microcatalog/service/custom/ReleasePathCustomServiceTest.java new file mode 100644 index 0000000..dc8f795 --- /dev/null +++ b/src/test/java/com/github/microcatalog/service/custom/ReleasePathCustomServiceTest.java @@ -0,0 +1,271 @@ +package com.github.microcatalog.service.custom; + +import com.github.microcatalog.domain.Dependency; +import com.github.microcatalog.domain.Microservice; +import com.github.microcatalog.domain.custom.ReleaseGroup; +import com.github.microcatalog.domain.custom.ReleasePath; +import com.github.microcatalog.domain.custom.ReleaseStep; +import com.github.microcatalog.repository.DependencyRepository; +import com.github.microcatalog.repository.MicroserviceRepository; +import com.github.microcatalog.utils.DependencyBuilder; +import com.github.microcatalog.utils.MicroserviceBuilder; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.LongStream; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.given; + +@SpringBootTest(classes = ReleasePathCustomService.class) +class ReleasePathCustomServiceTest { + + @MockBean + private MicroserviceRepository microserviceRepository; + + @MockBean + private DependencyRepository dependencyRepository; + + @Autowired + private ReleasePathCustomService service; + + @Test + void getReleasePath_NoCycles_Success() { + given(microserviceRepository.findAll()).willReturn(createMicroservices()); + + given(dependencyRepository.findAll()).willReturn(createDependencies()); + + Optional maybePath = service.getReleasePath(1L); + assertThat(maybePath).isPresent(); + + ReleasePath path = maybePath.get(); + + ReleaseStep step = getStep(path, 0, 5L); + assertThat(step).isNotNull(); + assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(4L, 7L); + + step = getStep(path, 0, 8L); + assertThat(step).isNotNull(); + assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(4L); + + step = getStep(path, 1, 7L); + assertThat(step).isNotNull(); + assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(4L); + + step = getStep(path, 2, 4L); + assertThat(step).isNotNull(); + assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(2L); + + step = getStep(path, 3, 2L); + assertThat(step).isNotNull(); + assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(1L); + + step = getStep(path, 4, 1L); + assertThat(step).isNotNull(); + assertThat(step.getParentWorkItems()).isEmpty(); + } + + @Test + void getReleasePath_ContainsCyclesInSameComponent_ExceptionIsThrown() { + given(microserviceRepository.findAll()).willReturn(createMicroservices()); + + given(dependencyRepository.findAll()).willReturn(createDependenciesWithCycleInSameComponent()); + + assertThatIllegalArgumentException() + .isThrownBy(() -> + service.getReleasePath(1L) + ) + .withMessageStartingWith("There are cyclic dependencies between microservices"); + } + + @Test + void getReleasePath_ContainsCyclesInOtherComponent_Success() { + given(microserviceRepository.findAll()).willReturn(createMicroservices()); + + given(dependencyRepository.findAll()).willReturn(createDependenciesWithCycleInOtherComponent()); + + Optional maybePath = service.getReleasePath(1L); + assertThat(maybePath).isPresent(); + + ReleasePath path = maybePath.get(); + + ReleaseStep step = getStep(path, 0, 3); + assertThat(step).isNotNull(); + assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(2L); + + step = getStep(path, 0, 4); + assertThat(step).isNotNull(); + assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(2L); + + step = getStep(path, 1, 2); + assertThat(step).isNotNull(); + assertThat(step.getParentWorkItems()).extracting(Microservice::getId).containsExactlyInAnyOrder(1L); + + step = getStep(path, 2, 1L); + assertThat(step).isNotNull(); + assertThat(step.getParentWorkItems()).isEmpty(); + } + + private ReleaseStep getStep(final ReleasePath path, int groupIndex, long microserviceId) { + final Set steps = getSteps(path, groupIndex); + if (steps != null) { + Optional maybeStep = steps.stream() + .filter(s -> s.getWorkItem() != null && microserviceId == s.getWorkItem().getId()).findFirst(); + if (maybeStep.isPresent()) { + return maybeStep.get(); + } + } + + return null; + } + + private Set getSteps(ReleasePath path, int groupIndex) { + if (path.getGroups() == null) { + return null; + } + + if (groupIndex > path.getGroups().size()) { + return null; + } + + ReleaseGroup first = path.getGroups().get(groupIndex); + if (first != null) { + return first.getSteps(); + } else { + return Collections.emptySet(); + } + } + + private List createMicroservices() { + return LongStream.rangeClosed(1, 12) + .mapToObj(i -> new MicroserviceBuilder().withId(i).build()) + .collect(Collectors.toList()); + } + + /** + * Graph will contain two connected components, one of them has cycle + *

+ * First component with cycle 1->2->3->1 + * Second component without cycle 5->6, 5->7 + * + * @return dependencies + */ + private List createDependenciesWithCycleInSameComponent() { + final List dependencies = new ArrayList<>(); + + // First component with cycle 1->2->3->1 + dependencies.add(new DependencyBuilder() + .withId(1L).withSource(1L).withTarget(2L) + .build()); + dependencies.add(new DependencyBuilder() + .withId(2L).withSource(2L).withTarget(3L) + .build()); + dependencies.add(new DependencyBuilder() + .withId(3L).withSource(3L).withTarget(1L) + .build()); + + // Second component without cycle 5->6, 5->7 + dependencies.add(new DependencyBuilder() + .withId(4L).withSource(5L).withTarget(6L) + .build()); + dependencies.add(new DependencyBuilder() + .withId(5L).withSource(5L).withTarget(7L) + .build()); + + + return dependencies; + } + + /** + * Graph will contain two connected components, one of them has cycle + * First component without cycle 1->2, 2->3, 2->4 + * Second component with cycle 6->7->8->6 + * + * @return dependencies + */ + private List createDependenciesWithCycleInOtherComponent() { + final List dependencies = new ArrayList<>(); + + // First component without cycle 1->2, 2->3, 2->4 + dependencies.add(new DependencyBuilder() + .withId(1L).withSource(1L).withTarget(2L) + .build()); + dependencies.add(new DependencyBuilder() + .withId(2L).withSource(2L).withTarget(3L) + .build()); + dependencies.add(new DependencyBuilder() + .withId(3L).withSource(2L).withTarget(4L) + .build()); + + // Second component with cycle 6->7->8->6 + dependencies.add(new DependencyBuilder() + .withId(4L).withSource(5L).withTarget(6L) + .build()); + dependencies.add(new DependencyBuilder() + .withId(5L).withSource(6L).withTarget(7L) + .build()); + dependencies.add(new DependencyBuilder() + .withId(6L).withSource(7L).withTarget(8L) + .build()); + dependencies.add(new DependencyBuilder() + .withId(7L).withSource(8L).withTarget(6L) + .build()); + + + return dependencies; + } + + private List createDependencies() { + final List dependencies = new ArrayList<>(); + + dependencies.add(new DependencyBuilder() + .withId(1L).withSource(1L).withTarget(2L) + .build()); + + dependencies.add(new DependencyBuilder() + .withId(2L).withSource(2L).withTarget(4L) + .build()); + + dependencies.add(new DependencyBuilder() + .withId(3L).withSource(6L).withTarget(4L) + .build()); + + dependencies.add(new DependencyBuilder() + .withId(4L).withSource(4L).withTarget(5L) + .build()); + + dependencies.add(new DependencyBuilder() + .withId(5L).withSource(4L).withTarget(7L) + .build()); + + dependencies.add(new DependencyBuilder() + .withId(6L).withSource(4L).withTarget(8L) + .build()); + + dependencies.add(new DependencyBuilder() + .withId(7L).withSource(3L).withTarget(9L) + .build()); + + dependencies.add(new DependencyBuilder() + .withId(8L).withSource(3L).withTarget(11L) + .build()); + + dependencies.add(new DependencyBuilder() + .withId(9L).withSource(12L).withTarget(1L) + .build()); + + dependencies.add(new DependencyBuilder() + .withId(10L).withSource(7L).withTarget(5L) + .build()); + + dependencies.add(new DependencyBuilder() + .withId(11L).withSource(10L).withTarget(1L) + .build()); + + return dependencies; + } +} diff --git a/src/test/java/com/github/microcatalog/utils/DependencyBuilderTest.java b/src/test/java/com/github/microcatalog/utils/DependencyBuilderTest.java new file mode 100644 index 0000000..56e6472 --- /dev/null +++ b/src/test/java/com/github/microcatalog/utils/DependencyBuilderTest.java @@ -0,0 +1,19 @@ +package com.github.microcatalog.utils; + +import com.github.microcatalog.domain.Dependency; +import com.github.microcatalog.domain.Microservice; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class DependencyBuilderTest { + + @Test + void build() { + DependencyBuilder builder = new DependencyBuilder(); + Dependency dependency = builder.withId(1L).withSource(2L).withTarget(3L).build(); + assertThat(dependency).isNotNull().extracting(Dependency::getId).isEqualTo(1L); + assertThat(dependency.getSource()).isNotNull().extracting(Microservice::getId).isEqualTo(2L); + assertThat(dependency.getTarget()).isNotNull().extracting(Microservice::getId).isEqualTo(3L); + } +} diff --git a/src/test/java/com/github/microcatalog/utils/MicroserviceBuilderTest.java b/src/test/java/com/github/microcatalog/utils/MicroserviceBuilderTest.java new file mode 100644 index 0000000..0ad8ec6 --- /dev/null +++ b/src/test/java/com/github/microcatalog/utils/MicroserviceBuilderTest.java @@ -0,0 +1,16 @@ +package com.github.microcatalog.utils; + +import com.github.microcatalog.domain.Microservice; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class MicroserviceBuilderTest { + + @Test + void build() { + MicroserviceBuilder builder = new MicroserviceBuilder(); + Microservice microservice = builder.withId(1L).build(); + assertThat(microservice).isNotNull().extracting(Microservice::getId).isEqualTo(1L); + } +}