├── .babelrc ├── screenshot.png ├── src ├── main │ ├── resources │ │ ├── bootstrap.properties │ │ ├── application-active-directory.properties │ │ ├── banner.txt │ │ ├── sonar-config.properties │ │ ├── static │ │ │ ├── index.html │ │ │ └── style.css │ │ ├── application-basic-ldap.properties │ │ ├── logback.xml │ │ ├── application-development-h2.properties │ │ ├── application-development-mysql.properties │ │ ├── test-server.ldif │ │ ├── db │ │ │ └── changelog │ │ │ │ └── db.changelog-master.xml │ │ ├── application.properties │ │ └── data.sql │ ├── java │ │ └── de │ │ │ └── dm │ │ │ ├── personsearch │ │ │ ├── PersonRepository.java │ │ │ ├── Person.java │ │ │ ├── InMemoryPersonSearchService.java │ │ │ ├── PersonSearchConfiguration.java │ │ │ ├── PersonSearchController.java │ │ │ ├── basicldap │ │ │ │ └── PersonBasicLdapService.java │ │ │ └── activedirectory │ │ │ │ └── PersonActiveDirectoryService.java │ │ │ ├── frontendconfig │ │ │ ├── FrontendConfigDto.java │ │ │ └── FrontendConfigRestController.java │ │ │ ├── microservices │ │ │ ├── domain │ │ │ │ ├── ConsumeDto.java │ │ │ │ ├── HostDto.java │ │ │ │ ├── MicroserviceDto.java │ │ │ │ └── ServiceProperties.java │ │ │ ├── repository │ │ │ │ ├── InvalidStageNameException.java │ │ │ │ └── ServicePropertiesRepository.java │ │ │ ├── business │ │ │ │ ├── semanticexceptions │ │ │ │ │ ├── ServiceAlreadyExistsException.java │ │ │ │ │ ├── ServiceNotDeletableException.java │ │ │ │ │ └── ServiceNotFoundException.java │ │ │ │ ├── ServiceRegistryProperties.java │ │ │ │ ├── DefaultNodeContentFactory.java │ │ │ │ ├── ServiceRegistryContentProvider.java │ │ │ │ ├── PersistenceContentProvider.java │ │ │ │ ├── MicroserviceDtoFactory.java │ │ │ │ ├── MicroserviceContentProviderService.java │ │ │ │ ├── MicroserviceMergeService.java │ │ │ │ └── MicroserviceConditioningService.java │ │ │ └── controller │ │ │ │ └── ServiceController.java │ │ │ ├── bitbucket │ │ │ ├── domain │ │ │ │ ├── TopCommitterDto.java │ │ │ │ ├── BitbucketCommitterDto.java │ │ │ │ ├── BitbucketAuthorDto.java │ │ │ │ └── BitbucketCommitsDto.java │ │ │ └── controller │ │ │ │ └── BitbucketController.java │ │ │ ├── common │ │ │ ├── activedirectory │ │ │ │ ├── ActiveDirectoryProperties.java │ │ │ │ └── ActiveDirectoryAuthenticationProviderConfig.java │ │ │ ├── UserController.java │ │ │ ├── DefaultAuthenticationProviderConfig.java │ │ │ ├── HttpOkAuthenticationSuccessHandler.java │ │ │ ├── WebSecurityConfig.java │ │ │ └── basicldap │ │ │ │ └── BasicLdapAuthenticationProviderConfig.java │ │ │ └── SelaviApplication.java │ └── javascript │ │ ├── actions │ │ ├── stageSelectorActions.js │ │ ├── microserviceMindmapContextMenuActions.js │ │ ├── frontendConfigActions.js │ │ ├── loginDialogActions.js │ │ ├── addEditDialogActions.js │ │ ├── microserviceDeleteServiceDialogActions.js │ │ ├── microserviceFilderboxActions.js │ │ └── microserviceMindmapActions.js │ │ ├── shared │ │ ├── requiredPropertyUtil.js │ │ └── filterUtils.js │ │ ├── components │ │ ├── microserviceSnackbar.js │ │ ├── microserviceDocumentationLink.js │ │ ├── stageSelector.js │ │ ├── linkTextField.js │ │ ├── microserviceCountLabel.js │ │ ├── microserviceMindmapContextMenu.js │ │ ├── microserviceDeleteServiceDialog.js │ │ ├── loginDialog.js │ │ └── microserviceFilterbox.js │ │ └── app.js └── test │ ├── javascript │ ├── setup.js │ ├── components │ │ ├── microserviceDocumentationLink.spec.js │ │ ├── stageSelector.spec.js │ │ ├── microserviceCountLabel.spec.js │ │ ├── linkTextField.spec.js │ │ ├── microserviceDeleteServiceDialog.spec.js │ │ ├── microserviceFilterbox.spec.js │ │ └── loginDialog.spec.js │ ├── actions │ │ ├── microserviceMindmapContextMenuActions.spec.js │ │ ├── stageSelectorActions.spec.js │ │ ├── microserviceDeleteServiceDialogActions.spec.js │ │ ├── loginDialogActions.spec.js │ │ └── microserviceMindmapActions.spec.js │ ├── stores │ │ └── microserviceStore.spec.js │ └── shared │ │ ├── requiredPropertyUtil.spec.js │ │ └── filterUtils.spec.js │ └── java │ └── de │ └── dm │ ├── microservices │ ├── business │ │ ├── DefaultNodeContentFactoryTest.java │ │ ├── ServiceRegistryContentProviderTest.java │ │ ├── MicroserviceConditioningServiceTest.java │ │ ├── MicroserviceContentProviderServiceUnitTest.java │ │ ├── MicroserviceDtoFactoryUnitTest.java │ │ └── MicroserviceMergeServiceUnitTest.java │ └── repository │ │ └── ServiceRegistryRepositoryTest.java │ ├── common │ └── HttpOkAuthenticationSuccessHandlerTest.java │ ├── personsearch │ ├── basicldap │ │ └── PersonBasicLdapServiceTest.java │ ├── activedirectory │ │ └── PersonActiveDirectoryServiceTest.java │ └── PersonSearchControllerTest.java │ └── activedirectory │ └── business │ └── ActiveDirectoryServiceUnitTest.java ├── .gitignore ├── Dockerfile ├── MAINTAINERS.md ├── webpack.config.js ├── LICENSE.txt ├── docker_build_deploy.sh ├── package.json └── .travis.yml /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ["es2015", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dm-drogeriemarkt/selavi/HEAD/screenshot.png -------------------------------------------------------------------------------- /src/main/resources/bootstrap.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=@project.name@ 2 | spring.config.location=${catalina.base:}/conf/app/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | node_modules 4 | target 5 | src/main/resources/static/built/ 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings/ 10 | .nyc_output/ 11 | coverage/ 12 | 13 | -------------------------------------------------------------------------------- /src/main/resources/application-active-directory.properties: -------------------------------------------------------------------------------- 1 | selavi.ad.url=ldaps://localhost:636 2 | selavi.ad.password=secret 3 | selavi.ad.userDn=user 4 | selavi.ad.base=dc=springframework,dc=org 5 | selavi.ad.domain=DOMAIN 6 | -------------------------------------------------------------------------------- /src/main/java/de/dm/personsearch/PersonRepository.java: -------------------------------------------------------------------------------- 1 | package de.dm.personsearch; 2 | 3 | import java.util.List; 4 | 5 | public interface PersonRepository { 6 | 7 | List findByName(String name); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8 2 | 3 | COPY target/app.jar /srv/app.jar 4 | 5 | WORKDIR /srv/ 6 | 7 | EXPOSE 8080 8 | 9 | CMD [ "java", "-Xmx450m", "-jar", "app.jar", "--server.port=${PORT:8080}", "--spring.config.location=/srv/conf/" ] 10 | -------------------------------------------------------------------------------- /src/main/java/de/dm/frontendconfig/FrontendConfigDto.java: -------------------------------------------------------------------------------- 1 | package de.dm.frontendconfig; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | @Data 7 | @Builder 8 | public class FrontendConfigDto { 9 | private String documentationUrl; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,---. ,--. ,--. ,--.,--. 2 | ' .-' ,---. | | ,--,--.\ `.' / `--' 3 | `. `-. | .-. :| | ' ,-. | \ / ,--. 4 | .-' |\ --.| '--.\ '-' | \ / | | 5 | `-----' `----'`-----' `--`--' `-' `--' 6 | (Service Landscape Visualizer) -------------------------------------------------------------------------------- /src/main/resources/sonar-config.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=selavi 2 | sonar.sources=src/main/java,src/main/javascript 3 | sonar.jacoco.reportPath=target/jacoco.exec 4 | sonar.javascript.lcov.reportPath=target/lcov.info 5 | sonar.java.binaries=target/classes 6 | #sonar.exclusions= 7 | #sonar.coverage.exclusions= -------------------------------------------------------------------------------- /src/main/java/de/dm/microservices/domain/ConsumeDto.java: -------------------------------------------------------------------------------- 1 | package de.dm.microservices.domain; 2 | 3 | import lombok.*; 4 | 5 | @AllArgsConstructor 6 | @NoArgsConstructor 7 | @Getter 8 | @Setter 9 | @ToString 10 | public class ConsumeDto { 11 | private String target; 12 | private String type; 13 | private String label; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/de/dm/personsearch/Person.java: -------------------------------------------------------------------------------- 1 | package de.dm.personsearch; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | @Data 7 | @Builder 8 | public class Person { 9 | 10 | private String id; 11 | private String displayName; 12 | private String eMail; 13 | private byte[] thumbnailPhoto; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Service Landscape Visualizer 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/de/dm/bitbucket/domain/TopCommitterDto.java: -------------------------------------------------------------------------------- 1 | package de.dm.bitbucket.domain; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | 6 | @Data 7 | @Builder 8 | public class TopCommitterDto { 9 | 10 | private Long id; 11 | private String name; 12 | private String emailAddress; 13 | private Long numberOfCommits; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/application-basic-ldap.properties: -------------------------------------------------------------------------------- 1 | spring.ldap.embedded.ldif=classpath:test-server.ldif 2 | spring.ldap.embedded.base-dn=dc=springframework,dc=org 3 | spring.ldap.embedded.port=8389 4 | spring.ldap.embedded.credential.username=uid=bob,ou=people 5 | spring.ldap.embedded.credential.password=bobspassword 6 | spring.ldap.embedded.validation.enabled=true 7 | -------------------------------------------------------------------------------- /src/main/java/de/dm/bitbucket/domain/BitbucketCommitterDto.java: -------------------------------------------------------------------------------- 1 | package de.dm.bitbucket.domain; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @Builder 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class BitbucketCommitterDto { 13 | private BitbucketAuthorDto author; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/de/dm/bitbucket/domain/BitbucketAuthorDto.java: -------------------------------------------------------------------------------- 1 | package de.dm.bitbucket.domain; 2 | 3 | import lombok.*; 4 | 5 | @Data 6 | @Builder 7 | @AllArgsConstructor 8 | @NoArgsConstructor 9 | @EqualsAndHashCode(of = {"id"}) 10 | public class BitbucketAuthorDto { 11 | 12 | private String name; 13 | private String emailAddress; 14 | private Long id; 15 | private String displayName; 16 | } 17 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | Sven Müller (https://github.com/wicked539) 2 | Bastian Dupps (https://github.com/dupps) 3 | David Gonzales-Casin(https://github.com/Gonzo17) 4 | Rudi Forsch(https://github.com/rudifordm) 5 | Arnold Franke (https://github.com/indyarni) 6 | Jochen Löhl 7 | Erik Altmann 8 | -------------------------------------------------------------------------------- /src/main/java/de/dm/personsearch/InMemoryPersonSearchService.java: -------------------------------------------------------------------------------- 1 | package de.dm.personsearch; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | 6 | public class InMemoryPersonSearchService implements PersonRepository { 7 | 8 | 9 | @Override 10 | public List findByName(String name) { 11 | 12 | return Collections.singletonList(Person.builder().displayName("user").build()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/de/dm/microservices/repository/InvalidStageNameException.java: -------------------------------------------------------------------------------- 1 | package de.dm.microservices.repository; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.BAD_REQUEST) 7 | public class InvalidStageNameException extends RuntimeException { 8 | public InvalidStageNameException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/de/dm/microservices/business/semanticexceptions/ServiceAlreadyExistsException.java: -------------------------------------------------------------------------------- 1 | package de.dm.microservices.business.semanticexceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY, reason = "The service could not be created because it already exists.") 7 | public class ServiceAlreadyExistsException extends RuntimeException { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/de/dm/microservices/domain/HostDto.java: -------------------------------------------------------------------------------- 1 | package de.dm.microservices.domain; 2 | 3 | import lombok.*; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | @Getter 11 | @Setter 12 | @ToString 13 | public class HostDto { 14 | private String hostName; 15 | private String ipAddr; 16 | private String homePageUrl; 17 | private List ports = new ArrayList<>(); 18 | 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/de/dm/microservices/repository/ServicePropertiesRepository.java: -------------------------------------------------------------------------------- 1 | package de.dm.microservices.repository; 2 | 3 | import de.dm.microservices.domain.ServiceProperties; 4 | import org.springframework.data.repository.CrudRepository; 5 | 6 | import java.util.List; 7 | 8 | public interface ServicePropertiesRepository extends CrudRepository { 9 | 10 | List findByPkStage(String stage); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/de/dm/microservices/business/ServiceRegistryProperties.java: -------------------------------------------------------------------------------- 1 | package de.dm.microservices.business; 2 | 3 | import lombok.Getter; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | import java.util.LinkedHashMap; 7 | import java.util.Map; 8 | 9 | @ConfigurationProperties(prefix = "selavi.registry") 10 | public class ServiceRegistryProperties { 11 | 12 | @Getter 13 | private Map url = new LinkedHashMap<>(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/de/dm/common/activedirectory/ActiveDirectoryProperties.java: -------------------------------------------------------------------------------- 1 | package de.dm.common.activedirectory; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | @Data 7 | @ConfigurationProperties(prefix = "selavi.ad") 8 | public class ActiveDirectoryProperties { 9 | 10 | private String url; 11 | private String userDn; 12 | private String password; 13 | private String base; 14 | private String domain; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | [%d{ISO8601}] [%t] %-5p %c - %m%n 8 | utf8 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/java/de/dm/microservices/business/semanticexceptions/ServiceNotDeletableException.java: -------------------------------------------------------------------------------- 1 | package de.dm.microservices.business.semanticexceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY, reason = "The service could not be deleted because it is a service from the registry.") 7 | public class ServiceNotDeletableException extends RuntimeException { 8 | 9 | public ServiceNotDeletableException() { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/de/dm/microservices/business/semanticexceptions/ServiceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package de.dm.microservices.business.semanticexceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY, reason = "The service could not be deleted because it could not be found in the repository.") 7 | public class ServiceNotFoundException extends RuntimeException { 8 | 9 | public ServiceNotFoundException() { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/test/javascript/setup.js: -------------------------------------------------------------------------------- 1 | require('babel-register')(); 2 | const jsdom = require('jsdom').jsdom; 3 | 4 | // setup jsdom 5 | global.document = jsdom('', { 6 | url: "http://localhost" 7 | }); 8 | global.window = document.defaultView; 9 | global.navigator = window.navigator; 10 | 11 | // react-tap-event-plugin must not be called twice 12 | if (!global.touchTapSetup) { 13 | require('react-tap-event-plugin')(); 14 | global.touchTapSetup = true; 15 | } 16 | 17 | require('url-search-params-polyfill'); 18 | -------------------------------------------------------------------------------- /src/main/javascript/actions/stageSelectorActions.js: -------------------------------------------------------------------------------- 1 | import errorCode from 'rest/interceptor/errorCode'; 2 | import rest from 'rest'; 3 | import mime from 'rest/interceptor/mime'; 4 | 5 | export function onStageSelected(stage) { 6 | return function (dispatch) { 7 | let client = rest.wrap(mime).wrap(errorCode); 8 | client({ path: '/selavi/services/' + stage }).then(response => { 9 | dispatch({ 10 | type: 'FETCH_MICROSERVICES_SUCCESS', 11 | stage: stage, 12 | response: response 13 | }); 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/application-development-h2.properties: -------------------------------------------------------------------------------- 1 | ## db credentials - h2 2 | spring.h2.console.path=/h2-console 3 | spring.h2.console.enabled=true 4 | spring.datasource.url=jdbc:h2:file:${java.io.tmpdir}/selavi;AUTO_SERVER=TRUE;DB_CLOSE_ON_EXIT=FALSE 5 | spring.datasource.username=sa 6 | spring.datasource.password= 7 | spring.datasource.initialize=true 8 | # liquibase 9 | liquibase.enabled=false 10 | # the app is deployed as /selavi on the gateway, so we set tomcat's deployment path to the same value 11 | server.context-path=/selavi 12 | # dont query the service registry (we are probably offline, anyway) 13 | development.offline-mode=true 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/de/dm/bitbucket/domain/BitbucketCommitsDto.java: -------------------------------------------------------------------------------- 1 | package de.dm.bitbucket.domain; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | @Data 12 | @Builder 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | public class BitbucketCommitsDto { 16 | private Integer size; 17 | private boolean lastPage; 18 | private Integer start; 19 | private Integer limit; 20 | private Integer nextPageStart; 21 | private List values = new ArrayList<>(); 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/application-development-mysql.properties: -------------------------------------------------------------------------------- 1 | ## db credentials - mysql 2 | #spring.jpa.hibernate.ddl-auto=update 3 | spring.datasource.initialize=false 4 | spring.datasource.url=jdbc:mysql://localhost:3306/selavi 5 | spring.datasource.username=selavi 6 | spring.datasource.password=supersicher 7 | spring.datasource.driver-class-name=com.mysql.jdbc.Driver 8 | # liquibase 9 | liquibase.change-log=classpath:/db/changelog/db.changelog-master.xml 10 | liquibase.url=jdbc:mysql://localhost:3306/selavi 11 | liquibase.user=selavi 12 | liquibase.password=supersicher 13 | # the app is deployed as /selavi on the gateway, so we set tomcat's deployment path to the same value 14 | server.context-path=/selavi -------------------------------------------------------------------------------- /src/main/java/de/dm/personsearch/PersonSearchConfiguration.java: -------------------------------------------------------------------------------- 1 | package de.dm.personsearch; 2 | 3 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.core.Ordered; 7 | import org.springframework.core.annotation.Order; 8 | 9 | @Configuration 10 | @Order(Ordered.HIGHEST_PRECEDENCE) 11 | public class PersonSearchConfiguration { 12 | 13 | @Bean 14 | @ConditionalOnMissingBean(PersonRepository.class) 15 | public PersonRepository inMemoryPersonRepository() { 16 | return new InMemoryPersonSearchService(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/javascript/actions/microserviceMindmapContextMenuActions.js: -------------------------------------------------------------------------------- 1 | export function onAddProperty() { 2 | return { 3 | type: 'EDIT_SERVICE' 4 | }; 5 | } 6 | 7 | export function onDeleteService() { 8 | return { 9 | type: 'DELETE_SERVICE' 10 | }; 11 | } 12 | 13 | export function onDeleteLink() { 14 | return { 15 | type: 'DELETE_LINK' 16 | }; 17 | } 18 | 19 | export function onEditLink() { 20 | return { 21 | type: 'EDIT_LINK' 22 | }; 23 | } 24 | 25 | export function onShowService() { 26 | return { 27 | type: 'SHOW_SERVICE' 28 | }; 29 | } 30 | 31 | export function onHideService() { 32 | return { 33 | type: 'HIDE_SERVICE' 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/main/javascript/shared/requiredPropertyUtil.js: -------------------------------------------------------------------------------- 1 | export function getRequiredPropertyNames(inputTabsArray) { 2 | const flatObj = inputTabsArray.map((tab) => tab.inputFields) 3 | .reduce((acc, el) => Object.assign(acc, el), {}); 4 | 5 | let result = []; 6 | 7 | for (let key in flatObj) { 8 | if (flatObj[key].required) { 9 | result.push(key); 10 | } 11 | } 12 | 13 | return result; 14 | } 15 | 16 | export function hasAllRequiredProperties(service, fieldNameArray) { 17 | for (let i = 0; i < fieldNameArray.length; i++) { 18 | if (!service[fieldNameArray[i]]) { 19 | return false; 20 | } 21 | } 22 | 23 | return true; 24 | } -------------------------------------------------------------------------------- /src/main/java/de/dm/frontendconfig/FrontendConfigRestController.java: -------------------------------------------------------------------------------- 1 | package de.dm.frontendconfig; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | import javax.annotation.security.PermitAll; 8 | 9 | @RestController 10 | public class FrontendConfigRestController { 11 | 12 | @Value("${selavi.frontend.documentationUrl}") 13 | private String documentationUrl; 14 | 15 | @GetMapping("/frontendconfig") 16 | @PermitAll 17 | public FrontendConfigDto frontendConfig() { 18 | return FrontendConfigDto.builder().documentationUrl(documentationUrl).build(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/main/javascript/app.js', 5 | devtool: 'sourcemaps', 6 | cache: true, 7 | debug: true, 8 | output: { 9 | path: __dirname, 10 | filename: './target/classes/static/bundle.js' 11 | }, 12 | module: { 13 | loaders: [ 14 | { 15 | test: path.join(__dirname, '.'), 16 | exclude: /(node_modules)/, 17 | loader: 'babel', 18 | query: { 19 | cacheDirectory: true, 20 | presets: ['es2015', 'react'], 21 | plugins: ['transform-object-assign'] 22 | } 23 | } 24 | ] 25 | } 26 | }; -------------------------------------------------------------------------------- /src/main/resources/test-server.ldif: -------------------------------------------------------------------------------- 1 | dn: dc=springframework,dc=org 2 | objectclass: top 3 | objectclass: domain 4 | objectclass: extensibleObject 5 | dc: springframework 6 | 7 | dn: ou=groups,dc=springframework,dc=org 8 | objectclass: top 9 | objectclass: organizationalUnit 10 | ou: groups 11 | 12 | dn: ou=people,dc=springframework,dc=org 13 | objectclass: top 14 | objectclass: organizationalUnit 15 | ou: people 16 | 17 | dn: uid=ben,ou=people,dc=springframework,dc=org 18 | objectclass: top 19 | objectclass: person 20 | objectclass: organizationalPerson 21 | objectclass: inetOrgPerson 22 | cn: Ben Alex 23 | sn: Alex 24 | uid: ben 25 | userPassword: benspassword 26 | 27 | dn: uid=bob,ou=people,dc=springframework,dc=org 28 | objectclass: top 29 | objectclass: person 30 | objectclass: organizationalPerson 31 | objectclass: inetOrgPerson 32 | cn: Bob Hamilton 33 | sn: Hamilton 34 | uid: bob 35 | userPassword: bobspassword 36 | -------------------------------------------------------------------------------- /src/main/java/de/dm/personsearch/PersonSearchController.java: -------------------------------------------------------------------------------- 1 | package de.dm.personsearch; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestParam; 8 | 9 | import java.util.List; 10 | 11 | @Controller 12 | public class PersonSearchController { 13 | 14 | private final PersonRepository personRepository; 15 | 16 | public PersonSearchController(PersonRepository personRepository) { 17 | this.personRepository = personRepository; 18 | } 19 | 20 | @GetMapping("/person/search") 21 | public ResponseEntity> searchForPersons(@RequestParam String searchQuery) { 22 | return new ResponseEntity<>(this.personRepository.findByName(searchQuery), HttpStatus.OK); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/de/dm/microservices/business/DefaultNodeContentFactory.java: -------------------------------------------------------------------------------- 1 | package de.dm.microservices.business; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.node.ObjectNode; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | @Service 9 | public class DefaultNodeContentFactory { 10 | private static final String KEY_ID = "id"; 11 | private static final String KEY_LABEL = "label"; 12 | 13 | private final ObjectMapper mapper; 14 | 15 | @Autowired 16 | public DefaultNodeContentFactory(ObjectMapper mapper) { 17 | this.mapper = mapper; 18 | } 19 | 20 | public ObjectNode create(String serviceName) { 21 | ObjectNode node = mapper.createObjectNode(); 22 | node.put(KEY_ID, serviceName); 23 | node.put(KEY_LABEL, serviceName); 24 | 25 | return node; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/javascript/components/microserviceDocumentationLink.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import chai from 'chai'; 3 | import {shallow} from 'enzyme'; 4 | import {MicroserviceDocumentationLink} from '../../../main/javascript/components/microserviceDocumentationLink'; 5 | import sinon from 'sinon'; 6 | 7 | 8 | describe('', function () { 9 | 10 | it('check documentation link', function () { 11 | const props = createProps(); 12 | 13 | const wrapper = shallow(); 14 | chai.expect(wrapper.text()).to.contains('Doku'); 15 | chai.expect(wrapper.find('a').props().href).to.equal(props.frontendConfig.documentationUrl); 16 | }); 17 | }); 18 | 19 | function createProps() { 20 | return { 21 | loadFrontendConfig: sinon.spy(), 22 | frontendConfig: { 23 | documentationUrl: 'https://example.com/docu' 24 | } 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/de/dm/microservices/business/ServiceRegistryContentProvider.java: -------------------------------------------------------------------------------- 1 | package de.dm.microservices.business; 2 | 3 | import de.dm.microservices.domain.MicroserviceDto; 4 | import de.dm.microservices.repository.ServiceRegistryRepository; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.util.Map; 8 | import java.util.Set; 9 | 10 | @Service 11 | public class ServiceRegistryContentProvider { 12 | 13 | private final ServiceRegistryRepository serviceRegistryRepository; 14 | 15 | public ServiceRegistryContentProvider(ServiceRegistryRepository serviceRegistryRepository) { 16 | this.serviceRegistryRepository = serviceRegistryRepository; 17 | } 18 | 19 | public Map getAllMicroservices(String stage) { 20 | return serviceRegistryRepository.findAllServices(stage); 21 | } 22 | 23 | public Set getAllStageNames() { 24 | return serviceRegistryRepository.getAllStageNames(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/de/dm/microservices/business/DefaultNodeContentFactoryTest.java: -------------------------------------------------------------------------------- 1 | package de.dm.microservices.business; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.node.ObjectNode; 5 | import de.dm.microservices.business.DefaultNodeContentFactory; 6 | import org.junit.Test; 7 | 8 | import static org.hamcrest.CoreMatchers.is; 9 | import static org.hamcrest.MatcherAssert.assertThat; 10 | 11 | public class DefaultNodeContentFactoryTest { 12 | 13 | private static final String SERVICE_NAME = "halo i bims der servis"; 14 | private ObjectMapper mapper = new ObjectMapper(); 15 | 16 | private DefaultNodeContentFactory factory = new DefaultNodeContentFactory(mapper); 17 | 18 | @Test 19 | public void create() { 20 | ObjectNode result = factory.create(SERVICE_NAME); 21 | 22 | assertThat(result.get("id").textValue(), is(SERVICE_NAME)); 23 | assertThat(result.get("label").textValue(), is(SERVICE_NAME)); 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/java/de/dm/bitbucket/controller/BitbucketController.java: -------------------------------------------------------------------------------- 1 | package de.dm.bitbucket.controller; 2 | 3 | import de.dm.bitbucket.business.BitbucketService; 4 | import de.dm.bitbucket.domain.TopCommitterDto; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.PathVariable; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import java.util.List; 10 | 11 | @RestController 12 | public class BitbucketController { 13 | 14 | private final BitbucketService bitbucketService; 15 | 16 | public BitbucketController(BitbucketService bitbucketService) { 17 | this.bitbucketService = bitbucketService; 18 | } 19 | 20 | @GetMapping("/bitbucket/{stage}/{microserviceId}") 21 | public List getBitbucketInformation(@PathVariable final String stage, @PathVariable final String microserviceId) { 22 | return bitbucketService.findNamedTopCommitters(stage, microserviceId); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/de/dm/common/UserController.java: -------------------------------------------------------------------------------- 1 | package de.dm.common; 2 | 3 | import de.dm.personsearch.Person; 4 | import de.dm.personsearch.PersonRepository; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | import java.security.Principal; 9 | import java.util.List; 10 | 11 | @RestController 12 | public class UserController { 13 | 14 | private final PersonRepository personRepository; 15 | 16 | public UserController(PersonRepository personRepository) { 17 | this.personRepository = personRepository; 18 | } 19 | 20 | @RequestMapping("/user") 21 | public Person getUserDetails(Principal principal) { 22 | 23 | List personList = personRepository.findByName(principal.getName()); 24 | 25 | if (personList.size() == 1) { 26 | return personList.get(0); 27 | } 28 | 29 | return Person.builder().displayName(principal.getName()).build(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/javascript/actions/frontendConfigActions.js: -------------------------------------------------------------------------------- 1 | import rest from 'rest'; 2 | import mime from 'rest/interceptor/mime'; 3 | import errorCode from 'rest/interceptor/errorCode'; 4 | 5 | export function loadFrontendConfig() { 6 | return function (dispatch) { 7 | let request = { 8 | headers: { 9 | 'Content-Type': 'application/json' 10 | }, 11 | path: '/selavi/frontendconfig', 12 | method: 'GET' 13 | }; 14 | 15 | let client = rest.wrap(mime).wrap(errorCode); 16 | client(request).then( 17 | response => { 18 | dispatch({ 19 | type: 'FETCH_FRONTENDCONFIG_SUCCESS', 20 | frontendConfig: response.entity 21 | }); 22 | }, error => { 23 | dispatch({ 24 | type: 'FETCH_FRONTENDCONFIG_FAILED', 25 | message: error.entity.message 26 | }); 27 | }); 28 | } 29 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 dm-drogerie markt GmbH & Co. KG, https://dm.de 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /src/main/java/de/dm/microservices/domain/MicroserviceDto.java: -------------------------------------------------------------------------------- 1 | package de.dm.microservices.domain; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import lombok.ToString; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | @Getter 16 | @Setter 17 | @ToString 18 | @Builder 19 | public class MicroserviceDto { 20 | private String id; 21 | private String label; 22 | private List hosts = new ArrayList<>(); 23 | private List consumes = new ArrayList<>(); 24 | private String description; 25 | private String bitbucketUrl; 26 | private String ignoredCommitters; 27 | private String fdOwner; 28 | private String tags; 29 | private String microserviceUrl; 30 | private String ipAddress; 31 | private String networkZone; 32 | private String documentationLink; 33 | private String buildMonitorLink; 34 | private String monitoringLink; 35 | private boolean external; 36 | private String version; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/javascript/actions/loginDialogActions.js: -------------------------------------------------------------------------------- 1 | import errorCode from 'rest/interceptor/errorCode'; 2 | import rest from 'rest'; 3 | import mime from 'rest/interceptor/mime'; 4 | 5 | export function onCancel() { 6 | return { 7 | type: 'CANCEL_MENU_ACTION', 8 | }; 9 | } 10 | 11 | export function onSubmit(params) { 12 | return function (dispatch) { 13 | 14 | dispatch({ 15 | type: 'LOGIN_REQUESTED', 16 | }); 17 | 18 | let request = { 19 | method: 'POST', 20 | path: '/selavi/login', 21 | headers: { 22 | 'Content-Type': 'application/x-www-form-urlencoded' 23 | }, 24 | entity: params.entity 25 | }; 26 | 27 | let client = rest.wrap(mime).wrap(errorCode); 28 | client(request).then(response => { 29 | dispatch({ 30 | type: 'LOGIN_SUCCESS', 31 | loggedInUser: response.entity 32 | }); 33 | }, response => { 34 | dispatch({ 35 | type: 'LOGIN_FAILED', 36 | message: response.entity.message 37 | }); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/de/dm/microservices/domain/ServiceProperties.java: -------------------------------------------------------------------------------- 1 | package de.dm.microservices.domain; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import javax.persistence.Column; 9 | import javax.persistence.EmbeddedId; 10 | import javax.persistence.Entity; 11 | import java.io.Serializable; 12 | 13 | @Entity(name = "service_properties") 14 | @NoArgsConstructor 15 | @Getter 16 | @Setter 17 | public class ServiceProperties implements Serializable { 18 | 19 | @EmbeddedId 20 | private ServicePropertiesPk pk; 21 | 22 | @Column(nullable = false, length = Integer.MAX_VALUE) 23 | private String content; 24 | 25 | public ServiceProperties(String id, String stage, String content) { 26 | pk = new ServicePropertiesPk(id, stage); 27 | this.content = content; 28 | } 29 | 30 | @AllArgsConstructor 31 | @NoArgsConstructor 32 | @Getter 33 | @Setter 34 | public static class ServicePropertiesPk implements Serializable { 35 | @Column(nullable = false) 36 | private String id; 37 | 38 | @Column(nullable = false) 39 | private String stage; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docker_build_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # set some environment variables 4 | export REPO=dmopensource/selavi 5 | export TAG=`if [ "${TRAVIS_BRANCH}" = "master" ]; then echo "latest"; else echo "${TRAVIS_BRANCH}" ; fi` 6 | 7 | # build and tag image 8 | docker build -f Dockerfile -t $REPO:$COMMIT . 9 | docker tag $REPO:$COMMIT $REPO:$TAG 10 | docker tag $REPO:$COMMIT $REPO:travis-$TRAVIS_BUILD_NUMBER 11 | 12 | # login and push image to docker.io 13 | docker login -u $DOCKER_USER -p $DOCKER_PASS 14 | docker push $REPO 15 | 16 | # tag image for heroku, login and push image to heroku 17 | docker tag $REPO:$COMMIT registry.heroku.com/selavi/web 18 | docker login --username=_ --password=$HEROKU_TOKEN registry.heroku.com 19 | docker push registry.heroku.com/selavi/web 20 | 21 | # release image on heroku 22 | export DOCKER_IMAGE_ID=$(docker inspect $REPO:$TAG --format={{.Id}}) 23 | curl -n -X PATCH https://api.heroku.com/apps/selavi/formation \ 24 | -d '{ 25 | "updates": [ 26 | { 27 | "type": "web", 28 | "docker_image": "'$DOCKER_IMAGE_ID'" 29 | } 30 | ] 31 | }' \ 32 | -H "Content-Type: application/json" \ 33 | -H "Accept: application/vnd.heroku+json; version=3.docker-releases" \ 34 | -H "Authorization: Bearer $HEROKU_TOKEN" 35 | -------------------------------------------------------------------------------- /src/main/javascript/actions/addEditDialogActions.js: -------------------------------------------------------------------------------- 1 | import rest from 'rest'; 2 | import mime from 'rest/interceptor/mime'; 3 | import errorCode from 'rest/interceptor/errorCode'; 4 | 5 | export function onSubmit(entity, path, method, stage) { 6 | return function (dispatch) { 7 | let request = { 8 | entity: entity, 9 | headers: { 10 | 'Content-Type': 'application/json' 11 | }, 12 | path: path, 13 | method: method 14 | }; 15 | 16 | let client = rest.wrap(mime).wrap(errorCode); 17 | client(request).then(() => { 18 | client({ path: '/selavi/services/' + stage }).then(response => { 19 | dispatch({ 20 | type: 'FETCH_MICROSERVICES_SUCCESS', 21 | response: response 22 | }); 23 | }); 24 | }, response => { 25 | dispatch({ 26 | type: 'ADD_EDIT_FAILED', 27 | message: response.entity.message 28 | }); 29 | }); 30 | } 31 | } 32 | 33 | export function onCancel() { 34 | return function (dispatch) { 35 | dispatch({ 36 | type: 'CANCEL_MENU_ACTION' 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/de/dm/common/HttpOkAuthenticationSuccessHandlerTest.java: -------------------------------------------------------------------------------- 1 | package de.dm.common; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.springframework.mock.web.MockHttpServletRequest; 6 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; 7 | 8 | import javax.servlet.ServletException; 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.io.IOException; 11 | 12 | import static org.mockito.Mockito.mock; 13 | import static org.mockito.Mockito.verify; 14 | 15 | public class HttpOkAuthenticationSuccessHandlerTest { 16 | 17 | private HttpOkAuthenticationSuccessHandler httpOkAuthenticationSuccessHandler; 18 | 19 | @Before 20 | public void setup() { 21 | httpOkAuthenticationSuccessHandler = new HttpOkAuthenticationSuccessHandler(); 22 | } 23 | 24 | @Test 25 | public void onAuthenticationSuccessRedirect() throws IOException, ServletException { 26 | MockHttpServletRequest request = MockMvcRequestBuilders.post("login").buildRequest(null); 27 | 28 | HttpServletResponse response = mock(HttpServletResponse.class); 29 | 30 | httpOkAuthenticationSuccessHandler.onAuthenticationSuccess(request, response, null); 31 | 32 | verify(response).sendRedirect("user"); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/javascript/actions/microserviceMindmapContextMenuActions.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import { 3 | onAddProperty, 4 | onDeleteLink, 5 | onDeleteService, 6 | onEditLink, 7 | onShowService 8 | } from '../../../main/javascript/actions/microserviceMindmapContextMenuActions'; 9 | 10 | describe('microserviceMindmapContextMenuActions', function () { 11 | it('onAddProperty dispatches EDIT_SERVICE', function () { 12 | const result = onAddProperty(); 13 | 14 | chai.expect(result.type).to.equal('EDIT_SERVICE'); 15 | }); 16 | 17 | it('onDeleteService dispatches DELETE_SERVICE', function () { 18 | const result = onDeleteService(); 19 | 20 | chai.expect(result.type).to.equal('DELETE_SERVICE'); 21 | }); 22 | 23 | it('onDeleteLink dispatches DELETE_LINK', function () { 24 | const result = onDeleteLink(); 25 | 26 | chai.expect(result.type).to.equal('DELETE_LINK'); 27 | }); 28 | 29 | it('onEditLink dispatches EDIT_LINK', function () { 30 | const result = onEditLink(); 31 | 32 | chai.expect(result.type).to.equal('EDIT_LINK'); 33 | }); 34 | 35 | it('onShowService dispatches SHOW_SERVICE', function () { 36 | const result = onShowService(); 37 | 38 | chai.expect(result.type).to.equal('SHOW_SERVICE'); 39 | }); 40 | }); -------------------------------------------------------------------------------- /src/test/java/de/dm/microservices/business/ServiceRegistryContentProviderTest.java: -------------------------------------------------------------------------------- 1 | package de.dm.microservices.business; 2 | 3 | import de.dm.microservices.repository.ServiceRegistryRepository; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import java.util.Arrays; 8 | import java.util.HashSet; 9 | import java.util.Set; 10 | 11 | import static org.hamcrest.CoreMatchers.is; 12 | import static org.junit.Assert.assertThat; 13 | import static org.mockito.Mockito.mock; 14 | import static org.mockito.Mockito.when; 15 | 16 | public class ServiceRegistryContentProviderTest { 17 | 18 | ServiceRegistryContentProvider serviceRegistryContentProvider; 19 | 20 | ServiceRegistryRepository serviceRegistryRepository; 21 | 22 | @Before 23 | public void setup() { 24 | serviceRegistryRepository = mock(ServiceRegistryRepository.class); 25 | 26 | serviceRegistryContentProvider = new ServiceRegistryContentProvider(serviceRegistryRepository); 27 | } 28 | 29 | @Test 30 | public void getAllStageNames() throws Exception { 31 | HashSet stageNames = new HashSet<>(Arrays.asList("foo", "bar")); 32 | when(serviceRegistryRepository.getAllStageNames()).thenReturn(stageNames); 33 | 34 | Set allStageNames = serviceRegistryContentProvider.getAllStageNames(); 35 | 36 | assertThat(allStageNames, is(allStageNames)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/javascript/actions/microserviceDeleteServiceDialogActions.js: -------------------------------------------------------------------------------- 1 | import rest from 'rest'; 2 | import mime from 'rest/interceptor/mime'; 3 | import errorCode from 'rest/interceptor/errorCode'; 4 | 5 | export function onCancel() { 6 | return { 7 | type: 'CANCEL_MENU_ACTION', 8 | }; 9 | } 10 | 11 | export function onSubmit(params) { 12 | return function (dispatch) { 13 | let request = { 14 | method: 'DELETE' 15 | }; 16 | 17 | if (params.type === 'DELETE_SERVICE') { 18 | request.path = '/selavi/services/' + params.stage + '/' + params.deleteServiceId; 19 | } else if (params.type === 'DELETE_LINK') { 20 | request.path = '/selavi/services/' + params.stage + '/' + params.deleteLinkFromId + '/relations/' + params.deleteLinkToId; 21 | } 22 | 23 | let client = rest.wrap(mime).wrap(errorCode); 24 | client(request).then(() => { 25 | client({ path: '/selavi/services/' + params.stage }).then(response => { 26 | dispatch({ 27 | type: 'FETCH_MICROSERVICES_SUCCESS', 28 | response: response 29 | }); 30 | }); 31 | }, response => { 32 | dispatch({ 33 | type: 'DELETE_SERVICE_FAILED', 34 | message: response.entity.message 35 | }); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/javascript/components/microserviceSnackbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import Snackbar from 'material-ui/Snackbar'; 5 | 6 | const mapStateToProps = (state) => { 7 | return { 8 | menuMode: state.menuMode, 9 | globalErrorMessage: state.globalErrorMessage 10 | }; 11 | }; 12 | 13 | const mapDispatchToProps = (dispatch) => { 14 | return { 15 | onRequestClose: function () { 16 | dispatch({ 17 | type: 'CANCEL_MENU_ACTION' 18 | }); 19 | } 20 | }; 21 | }; 22 | 23 | class MicroserviceSnackbar extends React.Component { 24 | render() { 25 | 26 | let open = false; 27 | let message = ""; 28 | 29 | if (this.props.menuMode === 'ADD_LINK') { 30 | open = true; 31 | message = "Add link mode: draw connection between services!" 32 | } else if (this.props.globalErrorMessage) { 33 | open = true; 34 | message = this.props.globalErrorMessage 35 | } 36 | 37 | return ( 38 | 44 | ); 45 | } 46 | } 47 | 48 | export default connect(mapStateToProps, mapDispatchToProps)(MicroserviceSnackbar); -------------------------------------------------------------------------------- /src/main/javascript/components/microserviceDocumentationLink.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {connect} from 'react-redux'; 3 | import {loadFrontendConfig} from '../actions/frontendConfigActions'; 4 | 5 | const style = {color: '#f69805'}; 6 | 7 | const documentationStyle = { 8 | color: 'rgba(0, 0, 0, 0.4)', 9 | zIndex: 999, 10 | position: 'absolute', 11 | right: '0.5em', 12 | bottom: '0.5em' 13 | }; 14 | 15 | const mapStateToProps = (state) => { 16 | return { 17 | frontendConfig: state.frontendConfig, 18 | }; 19 | }; 20 | 21 | const mapDispatchToProps = { 22 | loadFrontendConfig 23 | }; 24 | 25 | export class MicroserviceDocumentationLink extends Component { 26 | 27 | constructor(props) { 28 | super(props); 29 | this.props.loadFrontendConfig(); 30 | } 31 | 32 | render() { 33 | 34 | if (this.props.frontendConfig === null) { 35 | return ; 36 | } 37 | 38 | return ( 39 | 40 | Doku 43 | 44 | ); 45 | } 46 | } 47 | 48 | export default connect(mapStateToProps, mapDispatchToProps)(MicroserviceDocumentationLink); -------------------------------------------------------------------------------- /src/test/javascript/components/stageSelector.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import React from 'react'; 3 | import chai from 'chai'; 4 | import { shallow } from 'enzyme'; 5 | import { StageSelector } from '../../../main/javascript/components/stageSelector'; 6 | 7 | describe('', function () { 8 | 9 | it('shows drop-down box with all available stages, pre-selects "stage" prop', function () { 10 | const props = createProps(); 11 | props.availableStages = ['foo', 'bar', 'baz']; 12 | props.stage = 'bar'; 13 | 14 | const wrapper = shallow(); 15 | 16 | chai.expect(wrapper.find('MenuItem').length).to.equal(3); 17 | chai.expect(wrapper.props().value).to.equal(1); 18 | }); 19 | 20 | it('calls onStageSelected with the name of the selected stage', function () { 21 | const props = createProps(); 22 | props.availableStages = ['foo', 'bar', 'baz']; 23 | props.stage = 'foo'; 24 | 25 | const wrapper = shallow(); 26 | 27 | wrapper.simulate('change', undefined, undefined, 2); 28 | 29 | sinon.assert.calledOnce(props.onStageSelected); 30 | sinon.assert.calledWith(props.onStageSelected, 'baz'); 31 | }); 32 | }); 33 | 34 | function createProps() { 35 | return { 36 | onStageSelected: sinon.spy(), 37 | stage: undefined, 38 | availableStages: undefined 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "selavi-frontend", 3 | "version": "0.0.1", 4 | "description": "Service Landscape Visualizer", 5 | "dependencies": { 6 | "material-ui": "^0.18.6", 7 | "react": "^15.4.2", 8 | "react-dom": "^15.3.2", 9 | "react-redux": "^4.4.6", 10 | "react-tap-event-plugin": "^2.0.1", 11 | "redux": "^3.6.0", 12 | "redux-thunk": "^2.1.0", 13 | "rest": "^1.3.1", 14 | "webpack": "^1.12.2", 15 | "babel-register": "latest" 16 | }, 17 | "scripts": { 18 | "watch": "webpack --watch -d", 19 | "test": "mocha -w src/test/javascript/setup.js src/test/javascript/**/*.spec.js", 20 | "testCI": "nyc mocha src/test/javascript/setup.js src/test/javascript/**/*.spec.js" 21 | }, 22 | "devDependencies": { 23 | "babel-core": "^6.18.2", 24 | "babel-loader": "^6.2.7", 25 | "babel-plugin-transform-object-assign": "^6.8.0", 26 | "babel-polyfill": "^6.16.0", 27 | "babel-preset-es2015": "^6.18.0", 28 | "babel-preset-react": "^6.16.0", 29 | "chai": "^3.5.0", 30 | "enzyme": "^2.7.1", 31 | "jsdom": "^9.12.0", 32 | "mocha": "^3.2.0", 33 | "nyc": "^10.1.2", 34 | "react-addons-test-utils": "^15.4.2", 35 | "sinon": "^1.17.6", 36 | "url-search-params-polyfill": "^2.0.0" 37 | }, 38 | "nyc": { 39 | "exclude": [ 40 | "src/**/*.spec.js", 41 | "src/test/javascript/setup.js" 42 | ], 43 | "reporter": [ 44 | "html", 45 | "text" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/resources/static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Roboto, sans-serif; 3 | margin: 0; 4 | } 5 | 6 | table { 7 | border-spacing: 1.0em 0.5em; 8 | } 9 | 10 | th { 11 | text-align: left; 12 | } 13 | 14 | .appcontainer { 15 | display: flex; 16 | flex-direction: column; 17 | height: 100vh; 18 | } 19 | 20 | .appheader { 21 | padding: 0.5em; 22 | } 23 | 24 | 25 | .appcontent { 26 | display: flex; 27 | flex-direction: row; 28 | height: 97vh; 29 | } 30 | 31 | .microserviceMindmap, .microserviceFilterbox { 32 | border-style: solid; 33 | border-radius: 0; 34 | border-width: 2px; 35 | border-color: #5793ec; 36 | margin: 0.5em; 37 | } 38 | 39 | .microserviceMindmap { 40 | flex-grow: 2; 41 | display: flex; 42 | position: relative; 43 | } 44 | 45 | .vizContainer { 46 | flex-grow: 5; 47 | } 48 | 49 | .debugContainer { 50 | overflow: scroll; 51 | max-height: 89%; 52 | width: 20em; 53 | } 54 | 55 | .microserviceFilterbox { 56 | padding: 0.5em; 57 | } 58 | 59 | .contextMenu { 60 | background-color: lightgrey; 61 | display: flex; 62 | flex-direction: column; 63 | border: darkgray; 64 | border-style: solid; 65 | border-width: 0.1em; 66 | padding: 0.5em; 67 | } 68 | 69 | .contextMenu button { 70 | margin-bottom: 0.5em; 71 | } 72 | 73 | .contextMenu button:last-child { 74 | margin-bottom: 0em; 75 | } 76 | 77 | .contextMenu[hidden] { 78 | display: none; 79 | } 80 | -------------------------------------------------------------------------------- /src/main/javascript/components/stageSelector.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import DropDownMenu from 'material-ui/DropDownMenu'; 4 | import MenuItem from 'material-ui/MenuItem'; 5 | 6 | import { onStageSelected } from './../actions/stageSelectorActions'; 7 | 8 | const mapStateToProps = (state) => { 9 | return { 10 | stage: state.stage, 11 | availableStages: state.availableStages 12 | }; 13 | }; 14 | 15 | const mapDispatchToProps = { 16 | onStageSelected 17 | }; 18 | 19 | export class StageSelector extends React.Component { 20 | 21 | onChangeHandler(event, key, value) { 22 | this.props.onStageSelected(this.props.availableStages[value]); 23 | } 24 | 25 | render() { 26 | 27 | const menuItems = this.props.availableStages.map((stage, index) => ); 29 | const selectedStageIndex = this.props.availableStages.indexOf(this.props.stage); 30 | 31 | return ( 32 | 34 | {menuItems} 35 | 36 | ); 37 | } 38 | } 39 | 40 | export default connect(mapStateToProps, mapDispatchToProps)(StageSelector); -------------------------------------------------------------------------------- /src/main/java/de/dm/microservices/business/PersistenceContentProvider.java: -------------------------------------------------------------------------------- 1 | package de.dm.microservices.business; 2 | 3 | import de.dm.microservices.domain.MicroserviceDto; 4 | import de.dm.microservices.domain.ServiceProperties; 5 | import de.dm.microservices.repository.ServicePropertiesRepository; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | @Service 12 | public class PersistenceContentProvider { 13 | 14 | private final ServicePropertiesRepository servicePropertiesRepository; 15 | private final MicroserviceDtoFactory microserviceDtoFactory; 16 | 17 | public PersistenceContentProvider(ServicePropertiesRepository servicePropertiesRepository, MicroserviceDtoFactory microserviceDtoFactory) { 18 | this.servicePropertiesRepository = servicePropertiesRepository; 19 | this.microserviceDtoFactory = microserviceDtoFactory; 20 | } 21 | 22 | Map getAllMicroservices(String stage) { 23 | final Map microserviceDtoMap = new HashMap<>(); 24 | final Iterable allServiceProperties = servicePropertiesRepository.findByPkStage(stage); 25 | allServiceProperties.forEach(serviceProperties -> { 26 | final MicroserviceDto microserviceDto = microserviceDtoFactory.getMicroserviceDtoFromJSON(serviceProperties.getContent()); 27 | if (microserviceDto != null) { 28 | microserviceDtoMap.put(serviceProperties.getPk().getId(), microserviceDto); 29 | } 30 | }); 31 | 32 | return microserviceDtoMap; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/de/dm/personsearch/basicldap/PersonBasicLdapServiceTest.java: -------------------------------------------------------------------------------- 1 | package de.dm.personsearch.basicldap; 2 | 3 | import de.dm.personsearch.Person; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.mockito.Mock; 8 | import org.mockito.runners.MockitoJUnitRunner; 9 | import org.springframework.ldap.core.AttributesMapper; 10 | import org.springframework.ldap.core.LdapTemplate; 11 | import org.springframework.ldap.query.ContainerCriteria; 12 | 13 | import java.util.Arrays; 14 | import java.util.List; 15 | 16 | import static org.hamcrest.core.Is.is; 17 | import static org.junit.Assert.assertNotNull; 18 | import static org.junit.Assert.assertThat; 19 | import static org.mockito.Matchers.any; 20 | import static org.mockito.Mockito.when; 21 | 22 | @RunWith(MockitoJUnitRunner.class) 23 | public class PersonBasicLdapServiceTest { 24 | 25 | private PersonBasicLdapService personBasicLdapService; 26 | 27 | @Mock 28 | private LdapTemplate ldapTemplate; 29 | 30 | @Before 31 | public void init() { 32 | personBasicLdapService = new PersonBasicLdapService(ldapTemplate); 33 | } 34 | 35 | @Test 36 | public void findByName() { 37 | final List persons = Arrays.asList(Person.builder().id("testId").displayName("bob").eMail("bob@bob.de").build()); 38 | when(ldapTemplate.search(any(ContainerCriteria.class), any(AttributesMapper.class))).thenReturn(persons); 39 | 40 | final List result = personBasicLdapService.findByName("Bob"); 41 | assertNotNull(result); 42 | assertThat(result.size(), is(1)); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/de/dm/personsearch/basicldap/PersonBasicLdapService.java: -------------------------------------------------------------------------------- 1 | package de.dm.personsearch.basicldap; 2 | 3 | import de.dm.personsearch.Person; 4 | import de.dm.personsearch.PersonRepository; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.ldap.core.AttributesMapper; 7 | import org.springframework.ldap.core.LdapTemplate; 8 | import org.springframework.ldap.query.ContainerCriteria; 9 | import org.springframework.ldap.query.LdapQueryBuilder; 10 | 11 | import java.util.List; 12 | 13 | @Slf4j 14 | public class PersonBasicLdapService implements PersonRepository { 15 | 16 | private final LdapTemplate ldapTemplate; 17 | 18 | public PersonBasicLdapService(LdapTemplate ldapTemplate) { 19 | this.ldapTemplate = ldapTemplate; 20 | } 21 | 22 | public List findByName(String name) { 23 | String ldapQuery = "*" + name + "*"; 24 | ldapQuery = ldapQuery.replace(' ', '*'); 25 | ContainerCriteria containerCriteria = LdapQueryBuilder.query().where("objectclass").is("person").and("uid").like(ldapQuery); 26 | AttributesMapper attributesMapper = (attrs) -> { 27 | Person.PersonBuilder builder = Person.builder().id((String) attrs.get("uid").get()).displayName((String) attrs.get("uid").get()); 28 | if (attrs.get("thumbnailphoto") != null) { 29 | builder.thumbnailPhoto((byte[]) ((byte[]) attrs.get("thumbnailphoto").get())); 30 | } 31 | 32 | return builder.build(); 33 | }; 34 | log.info("Search person for name {}", name); 35 | return this.ldapTemplate.search(containerCriteria, attributesMapper); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/de/dm/personsearch/activedirectory/PersonActiveDirectoryServiceTest.java: -------------------------------------------------------------------------------- 1 | package de.dm.personsearch.activedirectory; 2 | 3 | import de.dm.personsearch.Person; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.mockito.Mock; 8 | import org.mockito.runners.MockitoJUnitRunner; 9 | import org.springframework.ldap.core.AttributesMapper; 10 | import org.springframework.ldap.core.LdapTemplate; 11 | import org.springframework.ldap.query.ContainerCriteria; 12 | 13 | import java.util.Arrays; 14 | import java.util.List; 15 | 16 | import static org.hamcrest.core.Is.is; 17 | import static org.junit.Assert.assertNotNull; 18 | import static org.junit.Assert.assertThat; 19 | import static org.mockito.Matchers.any; 20 | import static org.mockito.Mockito.when; 21 | 22 | @RunWith(MockitoJUnitRunner.class) 23 | public class PersonActiveDirectoryServiceTest { 24 | 25 | private PersonActiveDirectoryService personActiveDirectoryService; 26 | 27 | @Mock 28 | private LdapTemplate ldapTemplate; 29 | 30 | @Before 31 | public void init() { 32 | personActiveDirectoryService = new PersonActiveDirectoryService(ldapTemplate); 33 | } 34 | 35 | @Test 36 | public void findByName() { 37 | final List persons = Arrays.asList(Person.builder().id("testId").displayName("bob").eMail("bob@bob.de").build()); 38 | when(ldapTemplate.search(any(ContainerCriteria.class), any(AttributesMapper.class))).thenReturn(persons); 39 | 40 | final List result = personActiveDirectoryService.findByName("Bob"); 41 | assertNotNull(result); 42 | assertThat(result.size(), is(1)); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/javascript/actions/microserviceFilderboxActions.js: -------------------------------------------------------------------------------- 1 | import rest from 'rest'; 2 | import mime from 'rest/interceptor/mime'; 3 | import errorCode from 'rest/interceptor/errorCode'; 4 | 5 | export function onType(event, value) { 6 | return { 7 | type: 'FILTERBOX_TYPE', 8 | filterString: value 9 | }; 10 | } 11 | 12 | export function onLogin() { 13 | return { 14 | type: 'LOGIN' 15 | }; 16 | } 17 | 18 | export function onLogout() { 19 | return function (dispatch) { 20 | let request = { 21 | method: 'POST', 22 | path: '/selavi/logout' 23 | }; 24 | 25 | let client = rest.wrap(mime).wrap(errorCode); 26 | client(request).then(() => { 27 | dispatch({ 28 | type: 'LOGOUT_SUCCESS' 29 | }); 30 | }, response => { 31 | dispatch({ 32 | type: 'LOGOUT_FAILED', 33 | message: response.entity.message 34 | }); 35 | }); 36 | } 37 | ; 38 | } 39 | 40 | export function onAddLink() { 41 | return { 42 | type: 'ADD_LINK', 43 | }; 44 | } 45 | 46 | export function onShowVersions() { 47 | return { 48 | type: 'SHOW_VERSIONS', 49 | }; 50 | } 51 | 52 | export function onHideVersions() { 53 | return { 54 | type: 'HIDE_VERSIONS', 55 | }; 56 | } 57 | 58 | export function onAddService() { 59 | return { 60 | type: 'ADD_SERVICE', 61 | }; 62 | } 63 | 64 | export function onCancel() { 65 | return { 66 | type: 'CANCEL_MENU_ACTION', 67 | }; 68 | } 69 | 70 | export function onUnhideServices() { 71 | return { 72 | type: 'UNHIDE_SERVICES', 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /src/main/javascript/components/linkTextField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextField from 'material-ui/TextField'; 3 | import IconButton from 'material-ui/IconButton'; 4 | import OpenInNew from 'material-ui/svg-icons/action/open-in-new'; 5 | import { grey300 } from 'material-ui/styles/colors'; 6 | 7 | class LinkTextField extends React.Component { 8 | 9 | constructor(props) { 10 | super(props); 11 | this.state = { linkActive: !!props.defaultValue }; 12 | } 13 | 14 | _onChange(event) { 15 | this.setState({ linkActive: !!event.target.value }); 16 | }; 17 | 18 | _onTouchTap() { 19 | let value = this.refs["textField"].getValue(); 20 | if (value) { 21 | window.open(value); 22 | } 23 | }; 24 | 25 | getValue() { 26 | return this.refs["textField"].getValue(); 27 | } 28 | 29 | render() { 30 | 31 | let textFieldProps = Object.assign({}, this.props, { 32 | ref: "textField" 33 | }); 34 | textFieldProps.style.width = "31em"; 35 | 36 | let buttonProps = {}; 37 | let iconProps = { 38 | onTouchTap: this._onTouchTap.bind(this) 39 | }; 40 | 41 | if (!this.state.linkActive) { 42 | iconProps.color = grey300; 43 | } else { 44 | buttonProps.tooltip = "Open link..."; 45 | } 46 | 47 | return ( 48 |
49 | 50 | 51 | 52 | 53 |
54 | ); 55 | } 56 | } 57 | 58 | export default LinkTextField; -------------------------------------------------------------------------------- /src/main/java/de/dm/microservices/business/MicroserviceDtoFactory.java: -------------------------------------------------------------------------------- 1 | package de.dm.microservices.business; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.DeserializationFeature; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import de.dm.microservices.domain.MicroserviceDto; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.io.IOException; 12 | import java.util.ArrayList; 13 | 14 | @Service 15 | public class MicroserviceDtoFactory { 16 | 17 | private static final Logger LOG = LoggerFactory.getLogger(MicroserviceDtoFactory.class); 18 | 19 | private final ObjectMapper mapper; 20 | 21 | public MicroserviceDtoFactory() { 22 | this.mapper = new ObjectMapper(); 23 | this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 24 | } 25 | 26 | public MicroserviceDto getMicroserviceDtoFromJSON(final String json) { 27 | 28 | try { 29 | final MicroserviceDto microserviceDto = mapper.readValue(json, MicroserviceDto.class); 30 | if (microserviceDto.getConsumes() == null) { 31 | microserviceDto.setConsumes(new ArrayList<>()); 32 | microserviceDto.setHosts(new ArrayList<>()); 33 | } 34 | return microserviceDto; 35 | } catch (IOException e) { 36 | LOG.error(e.getMessage(), e); 37 | } 38 | 39 | return null; 40 | } 41 | 42 | String getJsonFromMicroserviceDto(final MicroserviceDto dto) { 43 | try { 44 | return mapper.writeValueAsString(dto); 45 | } catch (JsonProcessingException e) { 46 | LOG.error(e.getMessage(), e); 47 | } 48 | 49 | return null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/de/dm/common/DefaultAuthenticationProviderConfig.java: -------------------------------------------------------------------------------- 1 | package de.dm.common; 2 | 3 | import de.dm.common.activedirectory.ActiveDirectoryAuthenticationProviderConfig; 4 | import de.dm.common.basicldap.BasicLdapAuthenticationProviderConfig; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Import; 10 | import org.springframework.security.authentication.AuthenticationProvider; 11 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 12 | import org.springframework.security.core.userdetails.UserDetailsService; 13 | import org.springframework.security.provisioning.InMemoryUserDetailsManager; 14 | 15 | import java.util.Properties; 16 | 17 | @Configuration 18 | @Import({BasicLdapAuthenticationProviderConfig.class, ActiveDirectoryAuthenticationProviderConfig.class}) 19 | public class DefaultAuthenticationProviderConfig { 20 | 21 | @Value("${selavi.security.userRole}") 22 | private String userRole; 23 | 24 | @Bean 25 | @ConditionalOnMissingBean 26 | public AuthenticationProvider authenticationProvider() { 27 | DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); 28 | daoAuthenticationProvider.setUserDetailsService(createInMemoryUserDetailsManager()); 29 | return daoAuthenticationProvider; 30 | } 31 | 32 | private UserDetailsService createInMemoryUserDetailsManager() { 33 | final Properties users = new Properties(); 34 | users.put("user", "password," + userRole + ",enabled"); //add whatever other user you need 35 | return new InMemoryUserDetailsManager(users); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/javascript/shared/filterUtils.js: -------------------------------------------------------------------------------- 1 | // these are the data fields that we want to search 2 | const _fields = [ 3 | 'label', 4 | 'tags' 5 | ]; 6 | 7 | export function shouldFilterOut(microservice, filterString) { 8 | 9 | if (!filterString) { 10 | // filter string empty, no need to filter anything 11 | return false; 12 | } 13 | 14 | for (let i = 0; i < _fields.length; i++) { 15 | if (_match(microservice[_fields[i]], filterString)) { 16 | return false; 17 | } 18 | } 19 | 20 | // also search for edges 21 | let consumes = microservice.consumes; 22 | if(consumes !== undefined) { 23 | for(let i = 0; i < consumes.length; i++) { 24 | if(_match(consumes[i].type, filterString)) { 25 | return false; 26 | } 27 | } 28 | } 29 | 30 | // filter string does not match any of the fields we search 31 | return true; 32 | } 33 | 34 | export function isFilterHit(fieldName, fieldValue, filterString) { 35 | 36 | if (!filterString) { 37 | // filter string empty, no hit 38 | return false; 39 | } 40 | 41 | let isSearchedField = false; 42 | 43 | for (let i = 0; i < _fields.length; i++) { 44 | if (fieldName === _fields[i]) { 45 | isSearchedField = true; 46 | break; 47 | } 48 | } 49 | 50 | if (!isSearchedField) { 51 | // fieldName is not one of the fields we want to search 52 | return false; 53 | } 54 | 55 | return _match(fieldValue, filterString); 56 | } 57 | 58 | function _match(fieldValue, filterString) { 59 | 60 | if (typeof fieldValue !== "string") { 61 | // fieldValue is not a string, cant be a hit 62 | return false; 63 | } 64 | 65 | return (fieldValue.toLowerCase().indexOf(filterString.toLowerCase()) !== -1); 66 | } 67 | 68 | -------------------------------------------------------------------------------- /src/main/java/de/dm/common/HttpOkAuthenticationSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package de.dm.common; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; 5 | import org.springframework.security.web.savedrequest.HttpSessionRequestCache; 6 | import org.springframework.security.web.savedrequest.RequestCache; 7 | import org.springframework.security.web.savedrequest.SavedRequest; 8 | import org.springframework.util.StringUtils; 9 | 10 | import javax.servlet.ServletException; 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | import java.io.IOException; 14 | 15 | public class HttpOkAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { 16 | 17 | private RequestCache requestCache = new HttpSessionRequestCache(); 18 | 19 | @Override 20 | public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { 21 | 22 | // redirect to PersonSearchController.getCurrentUser() to fetch user details 23 | response.sendRedirect("user"); 24 | 25 | SavedRequest savedRequest = requestCache.getRequest(request, response); 26 | 27 | if (savedRequest == null) { 28 | clearAuthenticationAttributes(request); 29 | return; 30 | } 31 | 32 | String targetUrlParam = getTargetUrlParameter(); 33 | 34 | if (isAlwaysUseDefaultTargetUrl() 35 | || (targetUrlParam != null 36 | && StringUtils.hasText(request.getParameter(targetUrlParam)))) { 37 | requestCache.removeRequest(request, response); 38 | clearAuthenticationAttributes(request); 39 | return; 40 | } 41 | 42 | clearAuthenticationAttributes(request); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/javascript/components/microserviceCountLabel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import { hasAllRequiredProperties } from './../shared/requiredPropertyUtil'; 5 | 6 | const mapStateToProps = (state) => { 7 | return { 8 | microservices: state.microservices, 9 | hiddenMicroServices: state.hiddenMicroServices 10 | }; 11 | }; 12 | 13 | export class MicroserviceCountLabel extends React.Component { 14 | render() { 15 | 16 | const externalCount = this.props.microservices.filter((microservice) => microservice.external).length; 17 | const internalCount = this.props.microservices.length - externalCount; 18 | 19 | const internalLabel = (internalCount === 1 ? "microservice" : "microservices"); 20 | 21 | const servicesMissingReqPropsCount = this.props.microservices.filter((microservice) => !hasAllRequiredProperties(microservice, this.props.serviceRequiredProperties)).length; 22 | const servicesMissingReqPropsLabel = (servicesMissingReqPropsCount === 1 ? "service" : "services") + " missing req. props"; 23 | 24 | const hiddenServicesLabel = this.props.hiddenMicroServices.length > 0 ? " | " + this.props.hiddenMicroServices.length + " services hidden" : ""; 25 | 26 | return ( 27 | 28 | {internalCount} {internalLabel} | {externalCount} external 29 | {servicesMissingReqPropsCount > 0 && | {servicesMissingReqPropsCount} {servicesMissingReqPropsLabel}} 30 | {hiddenServicesLabel} 31 | 32 | ); 33 | } 34 | } 35 | 36 | export default connect(mapStateToProps)(MicroserviceCountLabel); 37 | -------------------------------------------------------------------------------- /src/main/javascript/actions/microserviceMindmapActions.js: -------------------------------------------------------------------------------- 1 | import rest from 'rest'; 2 | import mime from 'rest/interceptor/mime'; 3 | 4 | export function onSelectMicroserviceNode(params) { 5 | return function (dispatch) { 6 | let client = rest.wrap(mime); 7 | client({ 8 | path: '/selavi/bitbucket/' + params.stage + '/' + params.nodes[0], 9 | method: 'GET', 10 | }).then(response => { 11 | dispatch({ 12 | type: 'MICROSERVICE_NODE_SELECTED', 13 | selectedServiceId: params.nodes[0], 14 | response: response 15 | }); 16 | }); 17 | } 18 | } 19 | 20 | export function onContextMenuOpen(params) { 21 | if (params.nodeId) { 22 | return { 23 | type: 'CONTEXT_MENU_OPEN', 24 | top: params.top, 25 | left: params.left, 26 | contextMenuServiceId: params.nodeId, 27 | contextMenuFromId: undefined, 28 | contextMenuToId: undefined 29 | }; 30 | } else if (params.edgeFromId && params.edgeToId) { 31 | return { 32 | type: 'CONTEXT_MENU_OPEN', 33 | top: params.top, 34 | left: params.left, 35 | contextMenuServiceId: undefined, 36 | contextMenuFromId: params.edgeFromId, 37 | contextMenuToId: params.edgeToId 38 | }; 39 | } else { 40 | return { 41 | type: 'CONTEXT_MENU_OPEN', 42 | top: -1, 43 | left: -1, 44 | contextMenuServiceId: undefined, 45 | contextMenuFromId: undefined, 46 | contextMenuToId: undefined 47 | }; 48 | } 49 | } 50 | 51 | export function onAddLink(edgeData) { 52 | return function (dispatch) { 53 | dispatch({ 54 | type: 'ADD_RELATION', 55 | consumerId: edgeData.from, 56 | consumedServiceId: edgeData.to 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/javascript/actions/stageSelectorActions.spec.js: -------------------------------------------------------------------------------- 1 | import rest from 'rest'; 2 | import sinon from 'sinon'; 3 | import { onStageSelected } from '../../../main/javascript/actions/stageSelectorActions'; 4 | 5 | describe('stageSelectorActions', function () { 6 | describe('onStageSelected', function () { 7 | let thenHandler, errorHandler, clientStub; 8 | 9 | before(function () { 10 | const thenSpy = function (handlerParam, errorHandlerParam) { 11 | thenHandler = handlerParam; 12 | errorHandler = errorHandlerParam; 13 | }; 14 | clientStub = sinon.stub().returns({ 15 | then: sinon.spy(thenSpy) 16 | }); 17 | const wrapStub = { 18 | wrap: sinon.stub().returns(clientStub) 19 | }; 20 | 21 | sinon.stub(rest, 'wrap').returns(wrapStub); 22 | }); 23 | 24 | afterEach(function () { 25 | clientStub.reset(); 26 | 27 | // cleanup, make sure tests don't interfere with each other 28 | thenHandler = undefined; 29 | errorHandler = undefined; 30 | rest.wrap.reset() 31 | }); 32 | 33 | after(function () { 34 | rest.wrap.restore(); 35 | }); 36 | 37 | it('requests services for given stage, and passes stage to dispatched action', function () { 38 | const dispatchSpy = sinon.spy(); 39 | 40 | const submitFn = onStageSelected('dev'); 41 | submitFn(dispatchSpy); 42 | 43 | sinon.assert.calledOnce(clientStub); 44 | sinon.assert.calledWith(clientStub, { path: "/selavi/services/dev" }); 45 | 46 | thenHandler('response_to_get_services'); 47 | 48 | sinon.assert.calledOnce(dispatchSpy); 49 | sinon.assert.calledWith(dispatchSpy, { 50 | response: 'response_to_get_services', 51 | type: "FETCH_MICROSERVICES_SUCCESS", 52 | stage: "dev" 53 | }); 54 | }); 55 | }); 56 | }); -------------------------------------------------------------------------------- /src/test/javascript/components/microserviceCountLabel.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import chai from 'chai'; 3 | import { shallow } from 'enzyme'; 4 | import { MicroserviceCountLabel } from '../../../main/javascript/components/microserviceCountLabel'; 5 | 6 | describe('', function () { 7 | 8 | it('displays count of (external) services', function () { 9 | const props = createProps(); 10 | 11 | const wrapper = shallow(); 12 | chai.expect(wrapper.text()).to.equal('1 microservice ✪ | 1 external ✪'); 13 | }); 14 | 15 | it('displays count of services that are missing required properties', function () { 16 | const props = createProps(); 17 | props.serviceRequiredProperties.push('consumes'); 18 | 19 | const wrapper = shallow(); 20 | chai.expect(wrapper.text()).to.equal('1 microservice ✪ | 1 external ✪ | 1 service missing req. props'); 21 | }); 22 | 23 | it('displays count of hidden services', function () { 24 | const props = createProps(); 25 | 26 | const wrapper = shallow(); 27 | chai.expect(wrapper.text()).to.equal('1 microservice ✪ | 1 external ✪'); 28 | 29 | wrapper.setProps({ hiddenMicroServices: [ 'this is a service' ] }); 30 | chai.expect(wrapper.text()).to.equal('1 microservice ✪ | 1 external ✪ | 1 services hidden'); 31 | }); 32 | 33 | }); 34 | 35 | function createProps() { 36 | return { 37 | microservices: [ 38 | { 39 | id: "foo-service", 40 | label: "foo-service" 41 | }, 42 | { 43 | id: "bar-consumer", 44 | label: "bar-consumer", 45 | external: true, 46 | consumes: [ 47 | { "target": "foo-service", "type": "REST" } 48 | ] 49 | } 50 | ], 51 | serviceRequiredProperties: [], 52 | hiddenMicroServices: [] 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/de/dm/personsearch/activedirectory/PersonActiveDirectoryService.java: -------------------------------------------------------------------------------- 1 | package de.dm.personsearch.activedirectory; 2 | 3 | import de.dm.personsearch.Person; 4 | import de.dm.personsearch.PersonRepository; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.ldap.core.AttributesMapper; 7 | import org.springframework.ldap.core.LdapTemplate; 8 | import org.springframework.ldap.query.ContainerCriteria; 9 | 10 | import java.util.List; 11 | 12 | import static org.springframework.ldap.query.LdapQueryBuilder.query; 13 | 14 | @Slf4j 15 | public class PersonActiveDirectoryService implements PersonRepository { 16 | 17 | private final LdapTemplate ldapTemplate; 18 | 19 | public PersonActiveDirectoryService(LdapTemplate ldapTemplate) { 20 | this.ldapTemplate = ldapTemplate; 21 | } 22 | 23 | @Override 24 | public List findByName(String name) { 25 | String ldapQuery = "*" + name + "*"; 26 | ldapQuery = ldapQuery.replace(' ', '*'); 27 | 28 | final ContainerCriteria containerCriteria = query() 29 | .where("objectclass").is("user") 30 | .and("objectclass").not().is("computer") 31 | .and("sAMAccountName").isPresent() 32 | .and("sAMAccountName").not().like("*Admin*") 33 | .and("mail").isPresent() 34 | .and("name").like(ldapQuery); 35 | 36 | final AttributesMapper attributesMapper = attrs -> { 37 | Person.PersonBuilder builder = Person.builder() 38 | .id((String) attrs.get("sAMAccountName").get()) 39 | .displayName((String) attrs.get("displayname").get()) 40 | .eMail((String) attrs.get("mail").get()); 41 | 42 | if (attrs.get("thumbnailphoto") != null) { 43 | builder.thumbnailPhoto((byte[]) attrs.get("thumbnailphoto").get()); 44 | } 45 | 46 | return builder.build(); 47 | 48 | }; 49 | 50 | log.info("Search person for name {}", name); 51 | return ldapTemplate.search(containerCriteria, attributesMapper); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/de/dm/personsearch/PersonSearchControllerTest.java: -------------------------------------------------------------------------------- 1 | package de.dm.personsearch; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import javax.naming.InvalidNameException; 7 | import java.security.Principal; 8 | import java.util.Arrays; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | import static org.hamcrest.CoreMatchers.is; 13 | import static org.junit.Assert.assertThat; 14 | import static org.mockito.Mockito.mock; 15 | import static org.mockito.Mockito.when; 16 | 17 | public class PersonSearchControllerTest { 18 | 19 | private PersonSearchController personSearchController; 20 | 21 | private PersonRepository personRepository; 22 | 23 | @Before 24 | public void setup() { 25 | 26 | personRepository = mock(InMemoryPersonSearchService.class); 27 | personSearchController = new PersonSearchController(personRepository); 28 | } 29 | 30 | @Test 31 | public void getCurrentUser() throws InvalidNameException { 32 | Principal principal = () -> "foobar"; 33 | Person user = Person.builder().build(); 34 | 35 | when(personRepository.findByName("foobar")).thenReturn(Arrays.asList(user)); 36 | 37 | Person result = personSearchController.searchForPersons("foobar").getBody().get(0); 38 | 39 | assertThat(result, is(user)); 40 | } 41 | 42 | @Test 43 | public void getCurrentUserNoUniqueResult() throws InvalidNameException { 44 | Principal principal = () -> "foobar"; 45 | 46 | when(personRepository.findByName("foobar")).thenReturn(Collections.emptyList()); 47 | 48 | List result = personSearchController.searchForPersons("foobar").getBody(); 49 | 50 | assertThat(result.size(), is(0)); 51 | } 52 | 53 | @Test 54 | public void searchForPersons() throws Exception { 55 | Person person = mock(Person.class); 56 | String searchQuery = "gimmegimmegimmeapersonaftermidnight"; 57 | when(personRepository.findByName(searchQuery)).thenReturn(Arrays.asList(person)); 58 | 59 | List result = personSearchController.searchForPersons(searchQuery).getBody(); 60 | 61 | assertThat(result.size(), is(1)); 62 | assertThat(result.get(0), is(person)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/resources/db/changelog/db.changelog-master.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/test/java/de/dm/activedirectory/business/ActiveDirectoryServiceUnitTest.java: -------------------------------------------------------------------------------- 1 | package de.dm.activedirectory.business; 2 | 3 | import de.dm.personsearch.InMemoryPersonSearchService; 4 | import de.dm.personsearch.Person; 5 | import de.dm.personsearch.PersonRepository; 6 | import org.junit.Ignore; 7 | import org.junit.Test; 8 | import org.mockito.ArgumentCaptor; 9 | import org.springframework.ldap.core.AttributesMapper; 10 | import org.springframework.ldap.core.LdapTemplate; 11 | import org.springframework.ldap.query.LdapQuery; 12 | 13 | import javax.naming.directory.Attributes; 14 | import javax.naming.directory.BasicAttributes; 15 | 16 | import static org.hamcrest.CoreMatchers.is; 17 | import static org.hamcrest.MatcherAssert.assertThat; 18 | import static org.mockito.Mockito.mock; 19 | import static org.mockito.Mockito.verify; 20 | 21 | public class ActiveDirectoryServiceUnitTest { 22 | 23 | 24 | private LdapTemplate ldapTemplate = mock(LdapTemplate.class); 25 | 26 | @Test 27 | @Ignore 28 | public void getAllPersonNames() throws Exception { 29 | PersonRepository activeDirectoryService = new InMemoryPersonSearchService(); 30 | activeDirectoryService.findByName("Alt, foo"); 31 | 32 | ArgumentCaptor queryArgumentCaptor = ArgumentCaptor.forClass(LdapQuery.class); 33 | ArgumentCaptor mapperArgumentCaptor = ArgumentCaptor.forClass(AttributesMapper.class); 34 | 35 | verify(ldapTemplate).search(queryArgumentCaptor.capture(), mapperArgumentCaptor.capture()); 36 | 37 | assertThat(queryArgumentCaptor.getValue().filter().toString(), 38 | is("(&(objectclass=user)(!(objectclass=computer))(sAMAccountName=*)(!(sAMAccountName=*Admin*))(mail=*)(name=*Alt,*foo*))")); 39 | 40 | Attributes attributes = new BasicAttributes(); 41 | attributes.put("sAMAccountName", "fbc"); 42 | attributes.put("displayname", "Altmann, Erik"); 43 | attributes.put("mail", "john.doe@example.com"); 44 | byte[] selfie = {}; 45 | attributes.put("thumbnailphoto", selfie); 46 | 47 | Person person = (Person) mapperArgumentCaptor.getValue().mapFromAttributes(attributes); 48 | 49 | assertThat(person.getId(), is("fbc")); 50 | assertThat(person.getDisplayName(), is("Altmann, Erik")); 51 | assertThat(person.getEMail(), is("john.doe@example.com")); 52 | assertThat(person.getThumbnailPhoto(), is(selfie)); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/javascript/components/microserviceMindmapContextMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from 'react-redux'; 3 | import { 4 | onAddProperty, 5 | onDeleteLink, 6 | onDeleteService, 7 | onEditLink, 8 | onHideService, 9 | onShowService 10 | } from './../actions/microserviceMindmapContextMenuActions'; 11 | 12 | const mapStateToProps = (state) => { 13 | return { 14 | top: state.contextMenuTop, 15 | left: state.contextMenuLeft, 16 | contextMenuServiceId: state.contextMenuServiceId, 17 | contextMenuFromId: state.contextMenuFromId, 18 | contextMenuToId: state.contextMenuToId, 19 | loggedInUser: state.loggedInUser 20 | }; 21 | }; 22 | 23 | 24 | const mapDispatchToProps = { 25 | onAddProperty, 26 | onDeleteService, 27 | onDeleteLink, 28 | onEditLink, 29 | onShowService, 30 | onHideService 31 | }; 32 | 33 | export class MicroserviceMindmapContextMenu extends React.Component { 34 | 35 | render() { 36 | const {top, left} = this.props; 37 | const style = {position: 'fixed', top, left, zIndex: 999}; 38 | 39 | if (this.props.loggedInUser) { 40 | if (this.props.contextMenuServiceId) { 41 | return ( 42 | 47 | ); 48 | } else if (this.props.contextMenuFromId && this.props.contextMenuToId) { 49 | return ( 50 | 54 | ); 55 | } 56 | } else if (this.props.contextMenuServiceId) { 57 | return ( 58 | 62 | ); 63 | } 64 | 65 | return