1
0
mirror of https://github.com/kochetkov-ma/allure-server.git synced 2024-11-21 16:46:43 +02:00

Feature/tms (#89)

* add: delete old format support
add: fix build
add: new plugin system
add: spring boot 3, update allure, update all libs version,
update vaadin, update gradle, update nodejs

* add: fix test

* add: fix test

* add: fix test

* add: fix test

* add: fix docker image platforms

* add: fix docker image platforms

* add: enabled/disabled plugins
This commit is contained in:
Maksim Kochetkov 2024-07-01 20:26:58 +03:00 committed by GitHub
parent 650449cc83
commit 0d7342b9f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
159 changed files with 20538 additions and 26032 deletions

View File

@ -1,7 +1,6 @@
node_modules/
.helm/
.github/
build/
.idea/
.gradle/
tmp/
tmp/

19
.editorconfig Normal file
View File

@ -0,0 +1,19 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
max_line_length = 200
[*.java]
max_line_length = 180
[*.yml]
indent_size = 2
[*.yaml]
indent_size = 2

View File

@ -3,7 +3,8 @@ name: Build / Test / Check
on: [push, pull_request]
env:
NODE_VERSION: 12.x
NODE_VERSION: 20.13.1
GRADLE_VERSION: 8.8
jobs:
build:
@ -11,19 +12,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Fast checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v1
uses: actions/setup-java@v4
with:
java-version: '11'
java-package: jdk
architecture: x64
java-version: '21'
distribution: 'corretto'
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v1
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Build with Gradle
uses: eskatos/gradle-command-action@v1
uses: eskatos/gradle-command-action@v3
with:
gradle-version: 6.9.2
gradle-version: ${{ env.GRADLE_VERSION }}
arguments: '--stacktrace --info build'

View File

@ -9,36 +9,50 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
## Build prepare ##
- name: Fast checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Set RELEASE_VERSION
run: echo ::set-env name=RELEASE_VERSION::${GITHUB_REF:11}
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
- name: Set up JDK
uses: actions/setup-java@v1
uses: actions/setup-java@v4
with:
java-version: '11'
java-package: jdk
architecture: x64
java-version: '21'
distribution: 'corretto'
- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v1
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
## Build Image ##
- name: Build with Gradle
uses: eskatos/gradle-command-action@v1
uses: eskatos/gradle-command-action@v3
with:
gradle-version: 6.9.2
gradle-version: ${{ env.GRADLE_VERSION }}
arguments: ' -Pversion=${{ env.RELEASE_VERSION }} --stacktrace bootJar'
env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
- name: Archive code coverage results
uses: actions/upload-artifact@v1
with:
name: allure-server-${{ env.RELEASE_VERSION }}.jar
path: build/libs/allure-server-${{ env.RELEASE_VERSION }}.jar
## Release in DockerHub ##
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@v5
env:
RELEASE_VERSION: ${{ env.RELEASE_VERSION }}
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
with:
platforms: linux/amd64,linux/arm64
name: kochetkovma/allure-server
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
tags: "latest,${{ env.RELEASE_VERSION }}"
buildargs: RELEASE_VERSION
## Release in GitHub ##
- name: Create Release
id: create_release
uses: actions/create-release@latest
@ -64,14 +78,3 @@ jobs:
asset_path: build/libs/allure-server-${{ env.RELEASE_VERSION }}.jar
asset_name: allure-server.jar
asset_content_type: application/jar
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@v5
env:
RELEASE_VERSION: ${{ env.RELEASE_VERSION }}
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
with:
name: kochetkovma/allure-server
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
tags: "latest,${{ env.RELEASE_VERSION }}"
buildargs: RELEASE_VERSION

9
.gitignore vendored
View File

@ -1,6 +1,5 @@
/.gradle
/.idea
**/wrapper/gradle-wrapper.jar
/build
/keys
/allure
@ -14,7 +13,6 @@ webpack.config.js
webpack.generated.js
# Gradle
gradlew
gradlew.bat
# Compiled class file
@ -30,7 +28,6 @@ gradlew.bat
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
@ -52,4 +49,8 @@ allure-server-store/
tmp/
pg-secret.yaml
CA.pem
CA.pem
**/generated/**
*generated*
*-private*

View File

@ -4,7 +4,7 @@ image:
repository: kochetkovma/allure-server
pullPolicy: IfNotPresent
## Don't use 'latest' ;)
tag: 2.12.0
tag: 2.13.4
## Add 'key: value collection' and delete '{ }' if need
## Uncomment and remove '{ }' if need

View File

@ -1,42 +0,0 @@
/**
* NOTICE: this is an auto-generated file
*
* This file has been generated for `pnpm install` task.
* It is used to pin client side dependencies.
* This file will be overwritten on every run.
*/
const fs = require('fs');
const versionsFile = require('path').resolve(__dirname, 'build/frontend/versions.json');
if (!fs.existsSync(versionsFile)) {
return;
}
const versions = JSON.parse(fs.readFileSync(versionsFile, 'utf-8'));
module.exports = {
hooks: {
readPackage
}
};
function readPackage(pkg) {
const { dependencies } = pkg;
if (dependencies) {
for (let k in versions) {
if (dependencies[k] && dependencies[k] !== versions[k]) {
pkg.dependencies[k] = versions[k];
}
}
}
// Forcing chokidar version for now until new babel version is available
// check out https://github.com/babel/babel/issues/11488
if (pkg.dependencies.chokidar) {
pkg.dependencies.chokidar = '^3.4.0';
}
return pkg;
}

View File

@ -1,12 +1,7 @@
FROM gradle:6.9.2-jdk11 as build
COPY . .
ARG RELEASE_VERSION=${RELEASE_VERSION:-0.0.0}
RUN gradle -Pversion=docker -i -s --no-daemon bootJar
FROM openjdk:11.0.15-jre-slim-bullseye as production
COPY --from=build /home/gradle/build/libs/allure-server-docker.jar /allure-server-docker.jar
FROM amazoncorretto:21-alpine
COPY build/libs/*.jar /allure-server-docker.jar
# Set port
EXPOSE ${PORT:-8080}
# Run application
ENV JAVA_OPTS="-Xms256m -Xmx2048m"
ENTRYPOINT ["java", "-Dloader.path=/ext", "-cp", "allure-server-docker.jar", "-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE:default}", "org.springframework.boot.loader.PropertiesLauncher"]
ENTRYPOINT ["java", "-Dloader.path=/ext", "-jar", "allure-server-docker.jar", "-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE:default}"]

View File

@ -2,16 +2,18 @@ Allure Portal (Allure Report Server)
=================================
![Build / Test / Check](https://github.com/kochetkov-ma/allure-server/workflows/Build%20/%20Test%20/%20Check/badge.svg?branch=master)
[![jdk11](https://camo.githubusercontent.com/f3886a668d85acf93f6fec0beadcbb40a5446014/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6a646b2d31312d7265642e737667)](https://www.oracle.com/java/technologies/javase-jdk11-downloads.html)
[![gradle](https://camo.githubusercontent.com/f7b6b0146f2ee4c36d3da9fa18d709301d91f811/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f746f6f6c2d677261646c652d626c75652e737667)](https://gradle.org/)
[![junit](https://camo.githubusercontent.com/d2ba89c41121d7c6223c1ad926380235cf95ef82/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6a756e69742d706c6174666f726d2d627269676874677265656e2e737667)](https://github.com/junit-team/junit4/blob/master/doc/ReleaseNotes4.13.md)
![Static Badge](https://img.shields.io/badge/java-21-brightgreen)
![Static Badge](https://img.shields.io/badge/gradle-8.8-brightgreen)
![Docker Image Version](https://img.shields.io/docker/v/kochetkovma/allure-server?label=DockerHub)
![Docker Pulls](https://img.shields.io/docker/pulls/kochetkovma/allure-server?link=https)
[![checkstyle](https://img.shields.io/badge/checkstyle-google-blue)](https://github.com/checkstyle/checkstyle)
[![pmd](https://img.shields.io/badge/pmd-passed-green)](https://github.com/pmd/pmd)
[![spotbugs](https://img.shields.io/badge/spotbugs-passed-green)](https://github.com/spotbugs/spotbugs)
## About
https://allurereport.org/docs
Allure server for store / aggregate / manage Allure results and generate / manage Allure Reports.
There is simple API with Swagger(OpenAPI) Description.

View File

@ -1,26 +1,22 @@
import java.util.regex.Pattern
plugins {
id 'java'
id 'jacoco'
id 'idea'
id 'pmd'
id 'checkstyle'
id 'com.github.spotbugs' version '4.6.0'
id 'io.freefair.lombok' version '5.3.3.3'
id 'com.github.ben-manes.versions' version '0.42.0'
id 'io.freefair.lombok' version '8.6'
id 'com.github.ben-manes.versions' version '0.51.0'
// https://docs.spring.io/spring-boot/docs/current/reference/html/dependency-versions.html
id 'org.springframework.boot' version '2.7.1'
id 'io.spring.dependency-management' version '1.0.12.RELEASE'
id 'org.springframework.boot' version '3.3.1'
id 'io.spring.dependency-management' version '1.1.5'
id "com.vaadin" version "23.1.3"
id "com.vaadin" version "24.4.4"
id "org.openapi.generator" version '7.6.0'
}
apply from: './gradle/dependencies.gradle'
apply from: './gradle/checking.gradle'
apply from: './gradle/testing.gradle'
generateLombokConfig.enabled = false
group = theGroup
archivesBaseName = theArchivesBaseName
idea {
@ -33,20 +29,22 @@ compileJava {
options.encoding = 'UTF-8'
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
wrapper {
distributionType = Wrapper.DistributionType.ALL
gradleVersion = '6.9.2'
gradleVersion = '8.8'
doLast {
delete "$projectDir/gradlew.bat", "$projectDir/gradlew"
}
}
vaadin {
nodeVersion = 'v16.15.0'
pnpmEnable = false
nodeAutoUpdate = false
nodeVersion = 'v20.13.1'
pnpmEnable = true
productionMode = true
forceProductionBuild
}
classes {
doLast {
@ -54,6 +52,84 @@ classes {
def releaseVersion = System.env.RELEASE_VERSION as String
if (releaseVersion) {
new File(resourcesDir, "version.info").text = releaseVersion
} else {
new File(resourcesDir, "version.info").text = version
}
}
}
}
springBoot {
mainClass = "ru.iopump.qa.allure.Application"
}
tasks.named("bootJar") {
manifest {
attributes 'Main-Class': 'org.springframework.boot.loader.launch.PropertiesLauncher'
}
}
//// OPENAPI ////
// https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator-gradle-plugin
// https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators.md
// https://openapi-generator.tech/docs/generators/spring/
openApiGenerate {
generatorName = "spring"
library = "spring-boot"
inputSpec = "$rootDir/src/test/resources/tms/openapi-youtrack.json"
outputDir = "$projectDir/build/generated"
apiPackage = "org.brewcode.api.youtrack"
invokerPackage = "org.brewcode.api.youtrack.invoker"
modelPackage = "org.brewcode.api.youtrack.model"
modelNameSuffix = "Dto"
importMappings = [
SavedQueryDto: "org.brewcode.api.youtrack.model.SavedQueryDto",
]
configOptions = [
useBeanValidation : "false",
useJakartaEe : "true",
serializationLibrary : "jackson",
annotationLibrary : "swagger2",
generatedConstructorWithRequiredArgs: "true",
dateLibrary : "java8",
useSpringBoot3 : "true",
interfaceOnly : "true",
openApiNullable : "false",
useResponseEntity : "false", // Не использовать ResponseEntity<Е>, а сразу вернуть Е
skipDefaultInterface : "true" // Не добавлять в интерфейс default реализацию
]
}
tasks.named("openApiGenerate") {
doLast {
def directory = file("build/generated/src/main/java/org/brewcode/api/youtrack/model")
directory.eachFile {
def pattern = Pattern.compile('Type\\(value = (.+).class')
def matcher = pattern.matcher(it.text)
if (matcher.find())
it.text = matcher.replaceAll { match -> 'Type(value = org.brewcode.api.youtrack.model.%s.class'.formatted(match.group(1)) }
if (it.name == 'BaseBundleDto.java') {
it.text = it.text.readLines().withIndex().findAll { line, index -> index < 72 || index > 98 }.collect { it[0] }.join("\n")
}
}
}
}
compileJava.dependsOn tasks.openApiGenerate
compileTestJava.dependsOn tasks.openApiGenerate
sourceSets.main.java.srcDirs += tasks.openApiGenerate
bootJar {
manifest {
attributes 'Main-Class': 'org.springframework.boot.loader.launch.PropertiesLauncher'
}
}
wrapper {
gradleVersion = '8.8'
distributionType = Wrapper.DistributionType.ALL
}

View File

@ -3,11 +3,11 @@ services:
allure-server:
# For local debug #
# build: .
image: kochetkovma/allure-server:2.12.0
image: kochetkovma/allure-server:2.13.5
ports:
- 8080:8080
volumes:
- ./tmp/allure:/allure/:rw
environment:
SPRING_PROFILES_ACTIVE: oauth
# BASIC_AUTH_ENABLE: true
# BASIC_AUTH_ENABLE: true

View File

@ -3,7 +3,7 @@ services:
allure-server:
# For local debug #
# build: .
image: kochetkovma/allure-server:2.12.0
image: kochetkovma/allure-server:2.13.5
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/allure
SPRING_DATASOURCE_USERNAME: postgres
@ -26,4 +26,4 @@ services:
POSTGRES_USER: postgres
POSTGRES_DB: allure
ports:
- 5432:5432
- 5432:5432

View File

@ -1,25 +0,0 @@
import '@polymer/iron-icon/iron-icon.js';
import '@polymer/iron-iconset-svg/iron-iconset-svg.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
const template = html`<iron-iconset-svg name="icomoon" size="1024">
<svg>
<defs>
<g id="linkedin">
<path fill="#0077b5" style="fill: var(--color1, #0077b5)" class="path1"
d="M872.405 872.619h-151.637v-237.611c0-56.661-1.152-129.579-79.019-129.579-79.061 0-91.136 61.653-91.136 125.397v241.792h-151.637v-488.619h145.664v66.603h1.963c20.352-38.4 69.845-78.933 143.787-78.933 153.643 0 182.059 101.12 182.059 232.747zM227.712 317.141c-48.811 0-88.021-39.509-88.021-88.107 0-48.555 39.253-88.021 88.021-88.021 48.64 0 88.064 39.467 88.064 88.021 0 48.597-39.467 88.107-88.064 88.107zM303.744 872.619h-152.064v-488.619h152.064zM948.267 0h-872.704c-41.771 0-75.563 33.024-75.563 73.771v876.459c0 40.789 33.792 73.771 75.563 73.771h872.576c41.728 0 75.861-32.981 75.861-73.771v-876.459c0-40.747-34.133-73.771-75.861-73.771z"></path>
</g>
<g id="github">
<path class="path1"
d="M512 12.672c-282.88 0-512 229.248-512 512 0 226.261 146.688 418.133 350.080 485.76 25.6 4.821 34.987-11.008 34.987-24.619 0-12.16-0.427-44.373-0.64-87.040-142.421 30.891-172.459-68.693-172.459-68.693-23.296-59.093-56.96-74.88-56.96-74.88-46.379-31.744 3.584-31.104 3.584-31.104 51.413 3.584 78.421 52.736 78.421 52.736 45.653 78.293 119.851 55.68 149.12 42.581 4.608-33.109 17.792-55.68 32.427-68.48-113.707-12.8-233.216-56.832-233.216-253.013 0-55.893 19.84-101.547 52.693-137.387-5.76-12.928-23.040-64.981 4.48-135.509 0 0 42.88-13.739 140.8 52.48 40.96-11.392 84.48-17.024 128-17.28 43.52 0.256 87.040 5.888 128 17.28 97.28-66.219 140.16-52.48 140.16-52.48 27.52 70.528 10.24 122.581 5.12 135.509 32.64 35.84 52.48 81.493 52.48 137.387 0 196.693-119.68 240-233.6 252.587 17.92 15.36 34.56 46.763 34.56 94.72 0 68.523-0.64 123.563-0.64 140.203 0 13.44 8.96 29.44 35.2 24.32 204.843-67.157 351.403-259.157 351.403-485.077 0-282.752-229.248-512-512-512z"></path>
</g>
<g id="docker">
<path fill="#1488c6" style="fill: var(--color2, #1488c6)" class="path1"
d="M205.653 737.067c-29.184 0-55.637-23.893-55.637-52.907s23.893-53.035 55.68-53.035c31.915 0 55.893 23.893 55.893 52.992s-26.539 52.907-55.936 52.949zM888.832 448.512c-5.76-42.325-32-76.8-66.56-103.253l-13.44-10.667-10.837 13.227c-21.077 23.893-29.44 66.261-26.88 97.92 2.56 23.979 10.24 47.787 23.637 66.304-10.837 5.547-24.235 10.667-34.56 16.085-24.32 7.979-47.957 10.667-71.68 10.667h-684.373l-2.56 15.787c-5.12 50.432 2.56 103.253 23.979 151.040l10.411 18.56v2.56c64 105.941 177.92 153.6 301.995 153.6 238.677 0 434.432-103.253 527.232-325.675 60.8 2.645 122.197-13.227 151.040-71.509l7.68-13.227-12.8-7.979c-34.56-21.077-81.92-23.893-121.6-13.227zM547.157 406.187h-103.595v103.253h103.68v-103.339zM547.157 276.352h-103.595v103.253h103.68v-103.125zM547.157 143.915h-103.595v103.253h103.68v-103.253zM673.877 406.187h-102.997v103.253h103.253v-103.339zM289.963 406.187h-102.955v103.253h103.339v-103.339zM419.243 406.187h-102.4v103.253h102.997v-103.339zM161.963 406.187h-102.229v103.253h103.595v-103.339zM419.243 276.352h-102.4v103.253h102.997v-103.125zM289.323 276.352h-102.144v103.253h102.955v-103.125z"></path>
</g>
</defs>
</svg>
</iron-iconset-svg>`;
document.head.appendChild(template.content);

View File

@ -1,32 +0,0 @@
/******************************************************************************
* This file is auto-generated by Vaadin.
* If you want to customize the entry point, you can copy this file or create
* your own `index.ts` in your frontend directory.
* By default, the `index.ts` file should be in `./frontend/` folder.
*
* NOTE:
* - You need to restart the dev-server after adding the new `index.ts` file.
* After that, all modifications to `index.ts` are recompiled automatically.
* - `index.js` is also supported if you don't want to use TypeScript.
******************************************************************************/
// import Vaadin client-router to handle client-side and server-side navigation
import { Router } from '@vaadin/router';
// import Flow module to enable navigation to Vaadin server-side views
import { Flow } from '@vaadin/flow-frontend/Flow';
const { serverSideRoutes } = new Flow({
imports: () => import('../../build/frontend/generated-flow-imports')
});
const routes = [
// for client-side, place routes below (more info https://vaadin.com/docs/v15/flow/typescript/creating-routes.html)
// for server-side, the next magic line sends all unmatched routes:
...serverSideRoutes // IMPORTANT: this must be the last entry in the array
];
// Vaadin router needs an outlet in the index.html page to display views
const router = new Router(document.querySelector('#outlet'));
router.setRoutes(routes);

View File

@ -1,10 +0,0 @@
// @ts-nocheck
window.Vaadin = window.Vaadin || {};
window.Vaadin.featureFlags = window.Vaadin.featureFlags || {};
window.Vaadin.featureFlags.exampleFeatureFlag = false;
window.Vaadin.featureFlags.viteForFrontendBuild = false;
window.Vaadin.featureFlags.mapComponent = false;
window.Vaadin.featureFlags.spreadsheetComponent = false;
window.Vaadin.featureFlags.hillaPush = false;
window.Vaadin.featureFlags.newLicenseChecker = false;
window.Vaadin.featureFlags.collaborationEngineBackend = false;

View File

@ -1,3 +0,0 @@
import './vaadin-featureflags.ts';
import './index';

View File

@ -3,10 +3,10 @@
This file is auto-generated by Vaadin.
-->
<html lang="en">
<html>
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body, #outlet {
height: 100vh;
@ -17,7 +17,7 @@ This file is auto-generated by Vaadin.
<!-- index.ts is included here automatically (either by the dev server or during the build) -->
</head>
<body>
<!-- This outlet div is where the views are rendered -->
<div id="outlet"></div>
<!-- This outlet div is where the views are rendered -->
<div id="outlet"></div>
</body>
</html>

View File

@ -5,4 +5,4 @@ theArchivesBaseName=allure-server
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx2024m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx2024m -Dfile.encoding=UTF-8

View File

@ -1,33 +0,0 @@
////// checkstyle //////
checkstyle {
toolVersion = '8.30'
}
////// pmd //////
pmd {
toolVersion = "6.21.0"
rulePriority = 4 // 5 is default
ruleSetFiles "$rootDir/config/pmd/pmd-rules.xml"
ruleSets = []
}
pmdTest {
rulePriority = 1
}
////// spotbugs //////
spotbugs {
ignoreFailures = false
effort = 'default'
reportLevel = 'high'
excludeFilter = file("$rootDir/config/spotbug/spotbugs-exclude.xml")
}
spotbugsTest {
reports {
xml.enabled false
html.enabled true
}
}
spotbugsMain {
reports {
xml.enabled false
html.enabled true
}
}

View File

@ -3,17 +3,11 @@ repositories {
}
ext {
qaLibVersion = '1.2.0'
vaadinVersion = '23.1.3'
guavaVersion = '31.1-jre'
apacheIoVersion = '2.11.0'
openApiVersion = '1.6.9'
allureVersion = '2.18.1'
}
configurations {
developmentOnly
runtimeClasspath {
extendsFrom developmentOnly
}
vaadinVersion = '24.4.4'
guavaVersion = '33.2.1-jre'
apacheIoVersion = '2.16.1'
openApiVersion = '1.8.0'
allureVersion = '2.29.0'
}
dependencyManagement {
imports {
@ -30,7 +24,8 @@ dependencies {
/* Guava */
implementation "com.google.guava:guava:$guavaVersion"
/* Open API */
implementation "org.springdoc:springdoc-openapi-ui:$openApiVersion"
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.2'
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0"
/* OAuth2 */
implementation 'org.springframework.boot:spring-boot-starter-security'
@ -41,10 +36,10 @@ dependencies {
implementation('org.springframework.boot:spring-boot-starter-web') { exclude group: 'ch.qos.logback' }
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
implementation 'org.hibernate:hibernate-validator:6.2.3.Final'
implementation 'org.hibernate:hibernate-validator:8.0.1.Final'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.jooq:joor:0.9.14'
implementation 'org.jooq:joor:0.9.15'
implementation('com.vaadin:vaadin-spring-boot-starter') {
@ -55,8 +50,7 @@ dependencies {
"org.webjars.bowergithub.vaadin",
"org.webjars.bowergithub.webcomponents"].forEach { group -> exclude(group: group) }
['vaadin-dev-server',
'vaadin-accordion-flow',
['vaadin-accordion-flow',
'vaadin-avatar-flow',
'vaadin-date-picker-flow',
'vaadin-date-time-picker-flow',
@ -75,21 +69,18 @@ dependencies {
'flow-dnd',
'android-json'].forEach { module -> exclude(module: module) }
}
developmentOnly 'org.springframework.boot:spring-boot-devtools'
implementation "commons-io:commons-io:$apacheIoVersion"
/* Logging API*/
implementation 'org.slf4j:slf4j-api'
/* Logging IMPL*/
runtimeOnly 'ch.qos.logback:logback-classic'
runtimeOnly 'com.h2database:h2:1.4.200'
runtimeOnly 'com.h2database:h2:2.2.224'
runtimeOnly 'org.postgresql:postgresql'
/* Testing */
testImplementation('junit:junit') { transitive = false }
testImplementation 'org.assertj:assertj-core'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage'
exclude group: 'org.junit.jupiter'
exclude group: 'ch.qos.logback'
}
}
testImplementation('org.springframework.boot:spring-boot-starter-test')
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-loader-tools'
}

View File

@ -0,0 +1,2 @@
#This file is generated by updateDaemonJvm
toolchainVersion=21

View File

@ -1,11 +1,13 @@
////// junit //////
test {
useJUnitPlatform()
minHeapSize = "256m"
maxHeapSize = "2G"
reports {
junitXml.enabled = true
html.enabled = true
}
// reports {
// junitXml.enabled = true
// html.enabled = true
// }
testLogging {
showCauses true
showStackTraces true
@ -13,26 +15,4 @@ test {
/* events "started", "skipped", "failed" */
exceptionFormat "full"
}
jacoco {
enabled = true
}
finalizedBy jacocoTestReport
}
////// jacoco //////
jacocoTestReport {
reports {
xml.enabled true
csv.enabled false
html.enabled true
}
}
jacocoTestCoverageVerification {
mustRunAfter jacocoTestReport
violationRules {
rule {
limit {
minimum = 0.7
}
}
}
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

249
gradlew vendored Executable file
View File

@ -0,0 +1,249 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

25071
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,15 +11,17 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
import ru.iopump.qa.allure.properties.AllureProperties;
import ru.iopump.qa.allure.properties.BasicProperties;
import ru.iopump.qa.allure.properties.CleanUpProperties;
import ru.iopump.qa.allure.properties.TmsProperties;
// @ImportAutoConfiguration({FeignAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class})
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class, ErrorMvcAutoConfiguration.class})
@EnableCaching
@EnableTransactionManagement
@EnableConfigurationProperties({AllureProperties.class, CleanUpProperties.class, BasicProperties.class})
@EnableConfigurationProperties({AllureProperties.class, CleanUpProperties.class, BasicProperties.class, TmsProperties.class})
@EnableVaadin
public class Application { //NOPMD
public static void main(String[] args) { //NOPMD
SpringApplication.run(Application.class, args);
}
}
}

View File

@ -0,0 +1,49 @@
package ru.iopump.qa.allure.api;
import com.fasterxml.jackson.annotation.JsonInclude;
import feign.RequestInterceptor;
import feign.Retryer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ru.iopump.qa.allure.properties.TmsProperties;
import static org.springframework.cloud.openfeign.security.OAuth2AccessTokenInterceptor.AUTHORIZATION;
import static org.springframework.cloud.openfeign.security.OAuth2AccessTokenInterceptor.BEARER;
@Slf4j
@Configuration
@RequiredArgsConstructor
@EnableFeignClients(basePackages = {"ru.iopump.qa.allure.api"})
@ImportAutoConfiguration({FeignAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class})
public class FeignConfiguration {
@Bean
public RequestInterceptor feignRequestInterceptor(TmsProperties props) {
return requestTemplate -> {
var token = props.getToken();
var hasAuthorization = requestTemplate.headers().containsKey(AUTHORIZATION) && requestTemplate.headers().get(AUTHORIZATION).contains(token);
if (!hasAuthorization) {
requestTemplate.removeHeader(AUTHORIZATION);
requestTemplate.header(AUTHORIZATION, BEARER + " " + token);
}
};
}
@Bean
public Retryer retryer() {
return new Retryer.Default(100, 1000, 2);
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
return builder -> builder.serializationInclusion(JsonInclude.Include.NON_NULL);
}
}

View File

@ -0,0 +1,7 @@
package ru.iopump.qa.allure.api.youtrack;
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient(name = "youtrack-issues", url = "${tms.api-base-url}")
public interface IssuesClient extends org.brewcode.api.youtrack.IssuesApi {
}

View File

@ -1,5 +1,7 @@
package ru.iopump.qa.allure.config;
import jakarta.annotation.Nonnull;
import jakarta.servlet.http.HttpServletRequest;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@ -14,8 +16,6 @@ import org.springframework.web.servlet.resource.PathResourceResolver;
import org.springframework.web.servlet.resource.ResourceResolverChain;
import ru.iopump.qa.allure.properties.AllureProperties;
import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@ -51,30 +51,30 @@ public class RedirectConfiguration implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry
.addResourceHandler(join("/", cfg.reports().dir(), "**"))
.addResourceLocations("file:" + cfg.reports().dir())
.resourceChain(true)
.addResolver(new PathResourceResolver() {
.addResourceHandler(join("/", cfg.reports().dir(), "**"))
.addResourceLocations("file:" + cfg.reports().dir())
.resourceChain(true)
.addResolver(new PathResourceResolver() {
@Override
public Resource resolveResource(HttpServletRequest request,
@Nonnull String requestPath,
@Nonnull List<? extends Resource> locations,
@Nonnull ResourceResolverChain chain) {
return super.resolveResource(request, requestPath, locations, chain);
@Override
public Resource resolveResource(HttpServletRequest request,
@Nonnull String requestPath,
@Nonnull List<? extends Resource> locations,
@Nonnull ResourceResolverChain chain) {
return super.resolveResource(request, requestPath, locations, chain);
}
@Override
protected Resource getResource(@Nonnull String resourcePath,
@Nonnull Resource location) throws IOException {
var res = super.getResource(resourcePath, location);
if (res == null) {
return getIndexHtml(resourcePath, location);
}
return res;
@Override
protected Resource getResource(@Nonnull String resourcePath,
@Nonnull Resource location) throws IOException {
var res = super.getResource(resourcePath, location);
if (res == null) {
return getIndexHtml(resourcePath, location);
}
return res;
}
});
}
});
}
@SneakyThrows
@ -83,11 +83,11 @@ public class RedirectConfiguration implements WebMvcConfigurer {
final Path thisResource = location.getFile().toPath().resolve(resourcePath);
if (Files.exists(thisResource) && Files.isDirectory(thisResource)) {
return Files.walk(thisResource, 1)
.skip(1)
.filter(i -> "index.html".equals(i.getFileName().toString()))
.map(i -> new FileSystemResource(i.toFile()))
.findFirst()
.orElse(null);
.skip(1)
.filter(i -> "index.html".equals(i.getFileName().toString()))
.map(i -> new FileSystemResource(i.toFile()))
.findFirst()
.orElse(null);
}
return null;
}

View File

@ -0,0 +1,25 @@
package ru.iopump.qa.allure.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import ru.iopump.qa.allure.helper.plugin.AllureServerPlugin;
import ru.iopump.qa.util.ReflectionUtil;
import java.util.Collection;
@Slf4j
@Configuration
public class SpringConfiguration {
@Bean
public Collection<AllureServerPlugin> allureServerPlugins() {
var plugins = ReflectionUtil.createImplementations(AllureServerPlugin.class, null);
log.info("[ALLURE SERVER CONFIGURATION] Allure server plugins loaded: {}", plugins.stream().map(SpringConfiguration::name).toList());
return plugins;
}
private static String name(AllureServerPlugin plugin) {
return plugin.getClass() + ":" + plugin.getName();
}
}

View File

@ -5,6 +5,9 @@ import io.qameta.allure.entity.ExecutorInfo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Valid;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
@ -14,7 +17,16 @@ import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import ru.iopump.qa.allure.entity.ReportEntity;
import ru.iopump.qa.allure.model.ReportGenerateRequest;
@ -24,9 +36,6 @@ import ru.iopump.qa.allure.service.JpaReportService;
import ru.iopump.qa.allure.service.ResultService;
import ru.iopump.qa.util.StreamUtil;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import javax.validation.Valid;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;

View File

@ -4,6 +4,10 @@ import com.google.common.base.Preconditions;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@ -13,7 +17,15 @@ import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import ru.iopump.qa.allure.model.ResultResponse;
import ru.iopump.qa.allure.model.UploadResponse;
@ -21,10 +33,6 @@ import ru.iopump.qa.allure.service.PathUtil;
import ru.iopump.qa.allure.service.ResultService;
import ru.iopump.qa.util.StreamUtil;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

View File

@ -1,6 +1,16 @@
package ru.iopump.qa.allure.entity;
import jakarta.annotation.Nullable;
import jakarta.persistence.Access;
import jakarta.persistence.AccessType;
import jakarta.persistence.Basic;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PositiveOrZero;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -8,11 +18,6 @@ import lombok.NoArgsConstructor;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.ObjectUtils;
import javax.annotation.Nullable;
import javax.persistence.*;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.PositiveOrZero;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;

View File

@ -2,10 +2,9 @@ package ru.iopump.qa.allure.gui;
import com.vaadin.flow.component.applayout.AppLayout;
import com.vaadin.flow.component.applayout.DrawerToggle;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.html.Anchor;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.icon.IronIcon;
import com.vaadin.flow.component.icon.SvgIcon;
import com.vaadin.flow.component.orderedlayout.FlexComponent;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
@ -13,16 +12,19 @@ import com.vaadin.flow.component.tabs.Tab;
import com.vaadin.flow.component.tabs.Tabs;
import com.vaadin.flow.router.HighlightConditions;
import com.vaadin.flow.router.RouterLink;
import com.vaadin.flow.server.StreamResource;
import ru.iopump.qa.allure.gui.view.AboutView;
import ru.iopump.qa.allure.gui.view.ReportsView;
import ru.iopump.qa.allure.gui.view.ResultsView;
import ru.iopump.qa.allure.gui.view.SwaggerView;
@JsModule("./brands.js")
import java.io.Serial;
public class MainLayout extends AppLayout {
public static final String ALLURE_SERVER = "Allure Server";
@Serial
private static final long serialVersionUID = 2881152775131362224L;
public MainLayout() {
@ -57,13 +59,13 @@ public class MainLayout extends AppLayout {
tabs.setSizeFull();
var github = new Anchor("https://github.com/kochetkov-ma/allure-server",
new IronIcon("icomoon", "github"));
new SvgIcon(new StreamResource("github.svg", () -> getClass().getResourceAsStream("/icons/github.svg"))));
github.setTarget("_blank");
var dockerHub = new Anchor("https://hub.docker.com/r/kochetkovma/allure-server",
new IronIcon("icomoon", "docker"));
new SvgIcon(new StreamResource("docker.svg", () -> getClass().getResourceAsStream("/icons/docker.svg"))));
dockerHub.setTarget("_blank");
var linkedIn = new Anchor("https://www.linkedin.com/in/maxim-kochetkov-75178215a/",
new IronIcon("icomoon", "linkedin"));
new SvgIcon(new StreamResource("linkedin.svg", () -> getClass().getResourceAsStream("/icons/linkedin.svg"))));
linkedIn.setTarget("_blank");
var footer = new HorizontalLayout(github, dockerHub, linkedIn);

View File

@ -124,7 +124,7 @@ public class FilteredGrid<T> {
grid.addClassName(GRID_CLASS);
grid.setDataProvider(dataProvider);
grid.removeAllColumns();
grid.setHeightByRows(true);
// grid.setHeightByRows(true); // deprecated
grid.setSelectionMode(Grid.SelectionMode.MULTI);
final List<Grid.Column<T>> cols = columnSpecList.stream()

View File

@ -7,7 +7,7 @@ import com.vaadin.flow.component.checkbox.Checkbox;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.html.Label;
import com.vaadin.flow.component.html.NativeLabel;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.BeanValidationBinder;
@ -32,8 +32,8 @@ public class ReportGenerateDialog extends Dialog {
private final AllureReportController allureReportController;
private final Label info = new Label();
private final Label error = new Label();
private final NativeLabel info = new NativeLabel();
private final NativeLabel error = new NativeLabel();
@Getter
private final FormPayload payload;
private final Button generate = new Button("Generate", e -> onClickGenerate());

View File

@ -6,16 +6,16 @@ import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.dialog.Dialog;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.H3;
import com.vaadin.flow.component.html.Label;
import com.vaadin.flow.component.html.NativeLabel;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.upload.Upload;
import com.vaadin.flow.component.upload.receivers.MemoryBuffer;
import jakarta.annotation.Nonnull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Nonnull;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@ -33,16 +33,16 @@ public class ResultUploadDialog extends Dialog { //NOPMD
private final Button close = new Button("Ok", e -> onClickCloseAndDiscard());
public ResultUploadDialog(
Function<MemoryBuffer, Object> uploadConsumer,
int maxFileSizeBytes,
String type) {
Function<MemoryBuffer, Object> uploadConsumer,
int maxFileSizeBytes,
String type) {
this.buffer = new MemoryBuffer();
this.infoContainer = new Div();
this.upload = new Upload(buffer);
upload.setMaxFiles(1);
upload.setDropLabel(new Label(format("Upload allure {} as Zip archive (.zip)", type)));
upload.setDropLabel(new NativeLabel(format("Upload allure {} as Zip archive (.zip)", type)));
upload.setAcceptedFileTypes(".zip");
upload.setMaxFileSize(maxFileSizeBytes);
@ -50,9 +50,9 @@ public class ResultUploadDialog extends Dialog { //NOPMD
try {
var uploadResponse = uploadConsumer.apply(buffer);
show(info(format(
"File '{}- {} bytes' loaded: {}",
event.getFileName(), event.getContentLength(), uploadResponse
)), false
"File '{}- {} bytes' loaded: {}",
event.getFileName(), event.getContentLength(), uploadResponse
)), false
);
} catch (Exception ex) { //NOPMD
show(error("Internal error: " + ex.getLocalizedMessage()), true);

View File

@ -1,8 +1,8 @@
package ru.iopump.qa.allure.gui.dto;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

View File

@ -11,6 +11,7 @@ import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.data.provider.ListDataProvider;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
import ru.iopump.qa.allure.controller.AllureReportController;
@ -23,7 +24,6 @@ import ru.iopump.qa.allure.gui.component.ResultUploadDialog;
import ru.iopump.qa.allure.properties.AllureProperties;
import ru.iopump.qa.allure.service.JpaReportService;
import javax.annotation.PostConstruct;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.List;
@ -61,34 +61,34 @@ public class ReportsView extends VerticalLayout {
this.dateTimeResolver.retrieve();
this.uploadDialog = new ResultUploadDialog(
(buffer) -> allureReportController.uploadReport("manual_uploaded", toMultiPartFile(buffer)),
(int) multipartProperties.getMaxFileSize().toBytes(),
"report"
(buffer) -> allureReportController.uploadReport("manual_uploaded", toMultiPartFile(buffer)),
(int) multipartProperties.getMaxFileSize().toBytes(),
"report"
);
this.reports = new FilteredGrid<>(
asProvider(jpaReportService),
cols()
asProvider(jpaReportService),
cols()
);
this.uploadButton = new Button("Upload report");
this.deleteSelection = new Button("Delete selection",
new Icon(VaadinIcon.CLOSE_CIRCLE),
event -> {
for (ReportEntity reportEntity : reports.getGrid().getSelectedItems()) {
UUID uuid = reportEntity.getUuid();
try {
jpaReportService.internalDeleteByUUID(uuid);
Notification.show("Delete success: " + uuid, 2000, Notification.Position.TOP_START);
} catch (Exception e) { //NOPMD
Notification.show("Deleting error: " + e.getLocalizedMessage(),
5000,
Notification.Position.TOP_START);
log.error("Deleting error", e);
}
new Icon(VaadinIcon.CLOSE_CIRCLE),
event -> {
for (ReportEntity reportEntity : reports.getGrid().getSelectedItems()) {
UUID uuid = reportEntity.getUuid();
try {
jpaReportService.internalDeleteByUUID(uuid);
Notification.show("Delete success: " + uuid, 2000, Notification.Position.TOP_START);
} catch (Exception e) { //NOPMD
Notification.show("Deleting error: " + e.getLocalizedMessage(),
5000,
Notification.Position.TOP_START);
log.error("Deleting error", e);
}
reports.getGrid().deselectAll();
reports.getGrid().getDataProvider().refreshAll();
});
}
reports.getGrid().deselectAll();
reports.getGrid().getDataProvider().refreshAll();
});
deleteSelection.addThemeVariants(ButtonVariant.LUMO_ERROR);
this.dateTimeResolver.onClientReady(() -> reports.getGrid().getDataProvider().refreshAll());
@ -99,9 +99,9 @@ public class ReportsView extends VerticalLayout {
private static ListDataProvider<ReportEntity> asProvider(final JpaReportService jpaReportService) {
//noinspection unchecked
final Collection<ReportEntity> collection = (Collection<ReportEntity>) Proxy
.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{Collection.class},
(proxy, method, args) -> method.invoke(jpaReportService.getAll(), args));
.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{Collection.class},
(proxy, method, args) -> method.invoke(jpaReportService.getAll(), args));
return new ListDataProvider<>(collection);
}
@ -109,19 +109,19 @@ public class ReportsView extends VerticalLayout {
//// PRIVATE ////
private List<Col<ReportEntity>> cols() {
return ImmutableList.<Col<ReportEntity>>builder()
.add(Col.<ReportEntity>with().name("Uuid").value(prop("uuid")).build())
.add(
Col.<ReportEntity>with()
.name("Created")
.value(e -> dateTimeResolver.printDate(e.getCreatedDateTime()))
.build()
)
.add(Col.<ReportEntity>with().name("Url").value(this::displayUrl).type(LINK).build())
.add(Col.<ReportEntity>with().name("Path").value(prop("path")).build())
.add(Col.<ReportEntity>with().name("Active").value(prop("active")).build())
.add(Col.<ReportEntity>with().name("Size KB").value(prop("size")).type(NUMBER).build())
.add(Col.<ReportEntity>with().name("Build").value(this::buildUrl).type(LINK).build())
.build();
.add(Col.<ReportEntity>with().name("Uuid").value(prop("uuid")).build())
.add(
Col.<ReportEntity>with()
.name("Created")
.value(e -> dateTimeResolver.printDate(e.getCreatedDateTime()))
.build()
)
.add(Col.<ReportEntity>with().name("Url").value(this::displayUrl).type(LINK).build())
.add(Col.<ReportEntity>with().name("Path").value(prop("path")).build())
.add(Col.<ReportEntity>with().name("Active").value(prop("active")).build())
.add(Col.<ReportEntity>with().name("Size KB").value(prop("size")).type(NUMBER).build())
.add(Col.<ReportEntity>with().name("Build").value(this::buildUrl).type(LINK).build())
.build();
}
private String buildUrl(ReportEntity e) {

View File

@ -12,6 +12,7 @@ import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.data.provider.ListDataProvider;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
import ru.iopump.qa.allure.controller.AllureReportController;
@ -26,7 +27,6 @@ import ru.iopump.qa.allure.gui.dto.GenerateDto;
import ru.iopump.qa.allure.model.ResultResponse;
import ru.iopump.qa.util.StreamUtil;
import javax.annotation.PostConstruct;
import java.lang.reflect.Proxy;
import java.util.Collection;
import java.util.List;

View File

@ -5,10 +5,11 @@ import com.vaadin.flow.component.html.IFrame;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
import jakarta.servlet.ServletContext;
import lombok.extern.slf4j.Slf4j;
import ru.iopump.qa.allure.gui.MainLayout;
import javax.servlet.ServletContext;
import java.io.Serial;
import static ru.iopump.qa.allure.gui.MainLayout.ALLURE_SERVER;
import static ru.iopump.qa.allure.helper.Util.concatParts;
@ -19,6 +20,7 @@ import static ru.iopump.qa.allure.helper.Util.concatParts;
@Slf4j
public class SwaggerView extends VerticalLayout {
@Serial
private static final long serialVersionUID = 5822077036734476962L;
public SwaggerView(ServletContext context) {

View File

@ -1,100 +1,62 @@
package ru.iopump.qa.allure.helper; //NOPMD
import io.qameta.allure.Aggregator2;
import io.qameta.allure.ConfigurationBuilder;
import io.qameta.allure.ReportGenerator;
import io.qameta.allure.allure1.Allure1Plugin;
import io.qameta.allure.allure2.Allure2Plugin;
import io.qameta.allure.category.CategoriesPlugin;
import io.qameta.allure.category.CategoriesTrendPlugin;
import io.qameta.allure.context.FreemarkerContext;
import io.qameta.allure.context.JacksonContext;
import io.qameta.allure.context.MarkdownContext;
import io.qameta.allure.context.RandomUidContext;
import io.qameta.allure.core.AttachmentsPlugin;
import io.qameta.allure.ReportStorage;
import io.qameta.allure.core.Configuration;
import io.qameta.allure.core.MarkdownDescriptionsPlugin;
import io.qameta.allure.core.LaunchResults;
import io.qameta.allure.core.Plugin;
import io.qameta.allure.core.ReportWebPlugin;
import io.qameta.allure.core.TestsResultsPlugin;
import io.qameta.allure.duration.DurationPlugin;
import io.qameta.allure.duration.DurationTrendPlugin;
import io.qameta.allure.environment.Allure1EnvironmentPlugin;
import io.qameta.allure.history.HistoryPlugin;
import io.qameta.allure.history.HistoryTrendPlugin;
import io.qameta.allure.launch.LaunchPlugin;
import io.qameta.allure.mail.MailPlugin;
import io.qameta.allure.owner.OwnerPlugin;
import io.qameta.allure.plugin.DefaultPluginLoader;
import io.qameta.allure.retry.RetryPlugin;
import io.qameta.allure.retry.RetryTrendPlugin;
import io.qameta.allure.severity.SeverityPlugin;
import io.qameta.allure.status.StatusChartPlugin;
import io.qameta.allure.suites.SuitesPlugin;
import io.qameta.allure.summary.SummaryPlugin;
import io.qameta.allure.tags.TagsPlugin;
import io.qameta.allure.timeline.TimelinePlugin;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Component;
import ru.iopump.qa.allure.helper.plugin.AllureServerPlugin;
import ru.iopump.qa.allure.properties.AllureProperties;
import ru.iopump.qa.allure.properties.TmsProperties;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Component
@Slf4j
public final class AllureReportGenerator {
private final Collection<AllureServerPlugin> listeners;
private final AllureProperties allureProperties;
private final TmsProperties tmsProperties;
private final ReportGenerator delegate;
private final BeanFactory beanFactory;
private final AggregatorGrabber aggregatorGrabber = new AggregatorGrabber();
public AllureReportGenerator() {
public AllureReportGenerator(@NonNull Collection<AllureServerPlugin> listeners, AllureProperties allureProperties, TmsProperties tmsProperties, BeanFactory beanFactory) {
this.listeners = listeners;
this.allureProperties = allureProperties;
this.tmsProperties = tmsProperties;
this.beanFactory = beanFactory;
this.delegate = new ReportGenerator(configuration());
}
private static Configuration configuration() {
return new ConfigurationBuilder()
.fromPlugins(loadPlugins())
.fromExtensions(
Arrays.asList(
new JacksonContext(),
new MarkdownContext(),
new FreemarkerContext(),
new RandomUidContext(),
new MarkdownDescriptionsPlugin(),
new RetryPlugin(),
new RetryTrendPlugin(),
new TagsPlugin(),
new SeverityPlugin(),
new OwnerPlugin(),
new HistoryPlugin(),
new HistoryTrendPlugin(),
new CategoriesPlugin(),
new CategoriesTrendPlugin(),
new DurationPlugin(),
new DurationTrendPlugin(),
new StatusChartPlugin(),
new TimelinePlugin(),
new SuitesPlugin(),
new ReportWebPlugin(),
new TestsResultsPlugin(),
new AttachmentsPlugin(),
new MailPlugin(),
new SummaryPlugin(),
new ExecutorCiPlugin(),
new LaunchPlugin(),
new Allure2Plugin(),
new Allure1EnvironmentPlugin(),
new Allure1Plugin()
)
).build();
private Configuration configuration() {
return ConfigurationBuilder
.bundled()
.withPlugins(loadPlugins())
.withExtensions(List.of(aggregatorGrabber))
.build();
}
///// PRIVATE /////
@ -138,8 +100,75 @@ public final class AllureReportGenerator {
}
}
public Path generate(Path outputDirectory, List<Path> resultsDirectories) throws IOException {
delegate.generate(outputDirectory, resultsDirectories);
public Path generate(Path outputDirectory, List<Path> resultsDirectories, String reportUrl) {
var ctx = new PluginContext(reportUrl);
var effectiveListeners = listeners.stream()
.filter(it -> {
if (it.isEnabled(ctx))
return true;
log.info("[PLUGIN] Plugin '{} : {}' is disabled", it.getName(), it.getClass().getName());
return false;
})
.toList();
effectiveListeners.parallelStream()
.forEach(listener -> evaluateListener(() -> listener.onGenerationStart(resultsDirectories, ctx), listener.getName(), "before generation"));
final Collection<LaunchResults> launchesResults;
synchronized (aggregatorGrabber) {
delegate.generate(outputDirectory, resultsDirectories);
launchesResults = aggregatorGrabber.launchesResults();
}
effectiveListeners.parallelStream()
.forEach(listener -> evaluateListener(() -> listener.onGenerationFinish(outputDirectory, launchesResults, ctx), listener.getName(), "after generation"));
return outputDirectory;
}
@Getter
@RequiredArgsConstructor
private class PluginContext implements AllureServerPlugin.Context {
private final String reportUrl;
@Override
public AllureProperties getAllureProperties() {
return allureProperties;
}
@Override
public TmsProperties tmsProperties() {
return tmsProperties;
}
@Override
public BeanFactory beanFactory() {
return beanFactory;
}
}
private static void evaluateListener(Runnable runnable, String name, String stage) {
try {
runnable.run();
log.info("Listener '{}' {} executed successfully", name, stage);
} catch (Exception ex) {
log.error("Error in listener '{}'", name, ex);
}
}
private static class AggregatorGrabber implements Aggregator2 {
private Collection<LaunchResults> launchesResults = Collections.emptyList();
private Collection<LaunchResults> launchesResults() {
return launchesResults.stream().toList();
}
@Override
public void aggregate(Configuration configuration, List<LaunchResults> launchesResults, ReportStorage storage) {
this.launchesResults = launchesResults;
}
}
}

View File

@ -1,102 +0,0 @@
package ru.iopump.qa.allure.helper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import ru.iopump.qa.allure.entity.ReportEntity;
import ru.iopump.qa.allure.properties.AllureProperties;
import ru.iopump.qa.allure.service.PathUtil;
import ru.iopump.qa.util.Str;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import static ru.iopump.qa.allure.gui.DateTimeResolver.zeroZone;
@Slf4j
public class OldReportsFormatConverterHelper {
private final Path reportsDir;
private final String reportsPath;
public OldReportsFormatConverterHelper(AllureProperties cfg) {
this(Paths.get(cfg.reports().dir()), cfg.reports().path());
}
OldReportsFormatConverterHelper(Path reportsDir, String reportsPath) {
this.reportsDir = reportsDir;
this.reportsPath = reportsPath;
}
public Collection<ReportEntity> convertOldFormat() throws IOException {
if (hasOldFormatReports()) {
final Collection<Path> oldReports = Files.walk(reportsDir)
.parallel()
.filter(p -> "index.html".equalsIgnoreCase(p.getFileName().toString()))
.map(Path::getParent)
.filter(this::isOldFormat)
.collect(Collectors.toList());
log.info("Found '{}' old reports: {}", oldReports.size(), oldReports);
return oldReports.stream()
.map(dir -> {
final UUID uuid = UUID.randomUUID();
final File destination = reportsDir.resolve(uuid.toString()).toFile();
final String thisReportPath = reportsDir.relativize(dir).toString();
try {
FileUtils.moveDirectory(dir.toFile(), destination);
FileUtils.deleteDirectory(dir.toFile());
log.info("Report moved from '{}' to '{}'", dir, destination);
return ReportEntity.builder()
.uuid(uuid)
.path(thisReportPath)
.createdDateTime(LocalDateTime.now(zeroZone()))
.url(reportsPath + thisReportPath)
.level(0)
.active(true)
.build();
} catch (IOException e) {
log.error(Str.frm("Error moving report '{}' to '{}'", dir, destination), e);
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toUnmodifiableList());
}
return Collections.emptyList();
}
/**
* Fast check report directory.
*/
protected boolean hasOldFormatReports() throws IOException {
return !Files.walk(reportsDir, 1)
.skip(1)
.parallel()
.allMatch(this::isNewFormat);
}
protected boolean isNewFormat(Path p) {
return Files.isDirectory(p) &&
(p.getFileName().toString().matches(PathUtil.UUID_PATTERN)
|| p.getFileName().toString().equalsIgnoreCase("history"));
}
private boolean isOldFormat(Path p) {
return !isNewFormat(p);
}
}

View File

@ -1,6 +1,7 @@
package ru.iopump.qa.allure.helper;
import com.google.common.collect.Maps;
import jakarta.servlet.http.HttpServletRequest;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -11,7 +12,6 @@ import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.RedirectView;
import ru.iopump.qa.allure.properties.AllureProperties;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import static ru.iopump.qa.allure.helper.Util.concatParts;

View File

@ -1,5 +1,7 @@
package ru.iopump.qa.allure.helper;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Nullable;
import lombok.experimental.UtilityClass;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
@ -7,13 +9,14 @@ import ru.iopump.qa.allure.properties.AllureProperties;
import ru.iopump.qa.util.Str;
import ru.iopump.qa.util.StreamUtil;
import javax.annotation.Nullable;
import java.util.Optional;
import java.util.stream.Collectors;
@SuppressWarnings("RedundantModifiersUtilityClassLombok")
@UtilityClass
public class Util {
public static final ObjectMapper MAPPER = new ObjectMapper();
public static String url(AllureProperties allureProperties) {
if (StringUtils.isBlank(allureProperties.serverBaseUrl())) {
return ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString() + "/";
@ -24,15 +27,15 @@ public class Util {
public static String concatParts(@Nullable String... part) {
return StreamUtil.stream(part)
.collect(Collectors.joining("/"))
.replaceAll("/{2,}", "/");
.collect(Collectors.joining("/"))
.replaceAll("/{2,}", "/");
}
public static String join(@Nullable Object... part) {
return StreamUtil.stream(part)
.map(Str::toStr)
.map(s -> StringUtils.strip(s, "/"))
.collect(Collectors.joining("/"));
.map(Str::toStr)
.map(s -> StringUtils.strip(s, "/"))
.collect(Collectors.joining("/"));
}
public static String shortUrl(@Nullable String str) {

View File

@ -0,0 +1,33 @@
package ru.iopump.qa.allure.helper.plugin;
import io.qameta.allure.core.LaunchResults;
import org.springframework.beans.factory.BeanFactory;
import ru.iopump.qa.allure.properties.AllureProperties;
import ru.iopump.qa.allure.properties.TmsProperties;
import java.nio.file.Path;
import java.util.Collection;
public interface AllureServerPlugin {
void onGenerationStart(Collection<Path> resultsDirectories, Context context);
void onGenerationFinish(Path reportDirectory, Collection<LaunchResults> launchResults, Context context);
String getName();
default boolean isEnabled(Context context) {
return true;
}
interface Context {
AllureProperties getAllureProperties();
TmsProperties tmsProperties();
BeanFactory beanFactory();
String getReportUrl();
}
}

View File

@ -0,0 +1,81 @@
package ru.iopump.qa.allure.helper.plugin;
import io.qameta.allure.core.LaunchResults;
import io.qameta.allure.summary.SummaryData;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Objects;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardOpenOption.CREATE_NEW;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static ru.iopump.qa.allure.helper.Util.MAPPER;
@Slf4j
public class CustomReportMetaPlugin implements AllureServerPlugin {
private final static String LOGO_FILE = "logo.png";
private final static String LOGO_DIR = "plugin/custom-logo";
private final static String LOGO_STYLE_FILE = "styles.css";
private final static String SUMMARY_DIR = "widgets/summary.json";
@Override
public void onGenerationStart(Collection<Path> resultsDirectories, Context context) {
}
@SneakyThrows
@Override
public void onGenerationFinish(Path reportDirectory, Collection<LaunchResults> launchResults, Context context) {
var logo = context.getAllureProperties().logo();
if (logo != null) {
var bytes = logo.isReadable() ? logo.getContentAsByteArray() : null;
if (bytes != null) {
//noinspection resource
var customLogoDirectory = Files
.find(reportDirectory, 3, (path, basicFileAttributes) -> basicFileAttributes.isDirectory() && path.toString().endsWith(LOGO_DIR))
.findFirst()
.orElseThrow(() -> new InternalError("Custom logo plugin directory not found..."));
var logoName = Objects.requireNonNullElse(logo.getFilename(), LOGO_FILE);
var customLogoPath = customLogoDirectory.resolve(logoName);
var customLogoCssPath = customLogoDirectory.resolve(LOGO_STYLE_FILE);
Files.write(customLogoPath, bytes);
String cssForNewLogo = new String(new ClassPathResource("static/" + LOGO_STYLE_FILE).getContentAsByteArray(), UTF_8)
.replace("img.png", logoName);
Files.writeString(customLogoCssPath, cssForNewLogo, UTF_8, Files.exists(customLogoCssPath) ? TRUNCATE_EXISTING : CREATE_NEW);
log.info("{}: {} copied to {}", getName(), logoName, customLogoPath);
}
}
var title = context.getAllureProperties().title();
if (title != null) {
//noinspection resource
var summaryPath = Files
.find(reportDirectory, 3, (path, basicFileAttributes) -> basicFileAttributes.isRegularFile() && path.toString().endsWith(SUMMARY_DIR))
.findFirst()
.orElseThrow(() -> new InternalError("Summary file not found..."));
var summaryData = MAPPER.readValue(summaryPath.toFile(), SummaryData.class);
summaryData.setReportName(title);
var newSummary = MAPPER.writeValueAsString(summaryData);
Files.writeString(summaryPath, newSummary, UTF_8, TRUNCATE_EXISTING);
log.info("{}: Summary file updated with new title: {}", getName(), title);
}
}
@Override
public String getName() {
return "Logo Plugin";
}
}

View File

@ -0,0 +1,200 @@
package ru.iopump.qa.allure.helper.plugin;
import io.qameta.allure.core.LaunchResults;
import io.qameta.allure.entity.Link;
import io.qameta.allure.entity.Status;
import io.qameta.allure.entity.TestResult;
import jakarta.annotation.Nullable;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.brewcode.api.youtrack.model.IssueCommentDto;
import ru.iopump.qa.allure.api.youtrack.IssuesClient;
import ru.iopump.qa.allure.helper.plugin.youtrack.MarkdownStatisticModel;
import ru.iopump.qa.allure.helper.plugin.youtrack.MarkdownStatisticModel.Row.Statistic;
import ru.iopump.qa.allure.helper.plugin.youtrack.MarkdownStatisticModel.Total;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static io.qameta.allure.entity.Status.SKIPPED;
import static io.qameta.allure.entity.Status.UNKNOWN;
import static java.util.stream.Collectors.joining;
import static org.apache.commons.lang3.StringUtils.substringBefore;
import static ru.iopump.qa.allure.helper.Util.join;
import static ru.iopump.qa.allure.helper.Util.url;
import static ru.iopump.qa.allure.helper.plugin.youtrack.MarkdownStatisticModel.Row.Statistic.none;
// https://www.jetbrains.com/help/youtrack/devportal/youtrack-rest-api.html
@Slf4j
@RequiredArgsConstructor
public class YouTrackPlugin implements AllureServerPlugin {
private static final String COMMENT_TITLE = "E2E testing scenarios";
private static final String ALLURE_SERVER = "Allure Server";
private final boolean dryRun;
public YouTrackPlugin() {
this.dryRun = false;
}
@Override
public boolean isEnabled(Context context) {
return context.tmsProperties().isEnabled();
}
@Override
public void onGenerationStart(Collection<Path> resultsDirectories, Context context) {
}
@Override
public void onGenerationFinish(Path reportDirectory, Collection<LaunchResults> launchResults, Context context) {
final var effectiveDryRun = context.tmsProperties().isDryRun() || dryRun;
final var uuid = reportDirectory.getFileName().toString();
final var tmsHost = context.tmsProperties().getHost();
final var issueKeyPattern = context.tmsProperties().getIssueKeyPattern();
final var allureServerBaseUrl = url(context.getAllureProperties());
var launchResultsGroupByIssueTags = launchResultsGroupByIssueTags(launchResults, tmsHost, issueKeyPattern);
log.info("[PLUGIN {}] Statistic\n{}",
getName(),
launchResultsGroupByIssueTags.entrySet().stream()
.map(e ->
"%-20s : %s".formatted(
e.getKey(),
e.getValue().stream()
.map(tr -> "name: %s | full-name: %s | status: %s | %s ".formatted(substringBefore(tr.getName(), '\n'), tr.getFullName(), tr.getStatus(), tr.getTime()))
.collect(joining("\n" + StringUtils.repeat(' ', 23)))
)
)
.collect(joining("\n"))
);
launchResultsGroupByIssueTags
.entrySet()
.parallelStream()
.forEach(e -> {
var issueKey = e.getKey();
var results = e.getValue();
try {
var newScenarioModel = launchResultsToMarkdownStatisticModel(results, allureServerBaseUrl, context.getReportUrl());
if (newScenarioModel != null) {
sendComment(newScenarioModel, issueKey, context, effectiveDryRun);
log.info("[PLUGIN {}] Comment with executed scenarios created for issue '{}' for report '{}'\n{}", getName(), issueKey, uuid, newScenarioModel.toMarkdown());
} else
log.error("[PLUGIN {}] Server Internal Error. Failed to create comment for issue '{}'. Cannot build scenario model for results. Report '{}'", getName(), issueKey, uuid);
} catch (Throwable err) {
log.error("[PLUGIN %s] Failed to create comment for issue '%s'. Report '%s'".formatted(getName(), issueKey, uuid), err);
}
});
}
@Override
public String getName() {
return "YouTrack integration";
}
static void sendComment(MarkdownStatisticModel newScenarioModel, String issueKey, Context context, boolean dryRun) {
var client = context.beanFactory().getBean(IssuesClient.class);
List<IssueCommentDto> issueCommentDtos = !dryRun ? client.issuesIdCommentsGet(issueKey, "id,text,author", null, null) : Collections.emptyList();
Optional<IssueCommentDto> commentWithScenarios = issueCommentDtos.stream().filter(it -> it.getText().contains(COMMENT_TITLE)).findFirst();
final var commentToCreateOrUpdate = new IssueCommentDto();
if (commentWithScenarios.isEmpty()) {
commentToCreateOrUpdate.setText(newScenarioModel.toMarkdown());
if (!dryRun)
client.issuesIdCommentsPost(issueKey, null, true, null, commentToCreateOrUpdate);
} else {
var previousMarkdownText = commentWithScenarios.get().getText();
var previousModel = MarkdownStatisticModel.toModel(previousMarkdownText);
var mergedModel = previousModel.merge(newScenarioModel);
commentToCreateOrUpdate.setText(mergedModel.toMarkdown());
if (!dryRun)
client.issuesIdCommentsIssueCommentIdPost(issueKey, commentWithScenarios.get().getId(), true, null, commentToCreateOrUpdate);
}
}
@NonNull
static Map<String, List<TestResult>> launchResultsGroupByIssueTags(Collection<LaunchResults> launchResults, String tmsHost, Pattern issuePattern) {
return launchResults
.stream()
.flatMap(lr -> lr.getResults().stream()
.flatMap(r -> r.getLinks().stream()
.map(link -> Map.entry(link, r))))
.filter(e -> isIssueKey(e.getKey(), tmsHost, issuePattern))
.map(e -> Map.entry(extractIssueKey(e.getKey(), issuePattern), e.getValue()))
.collect(Collectors.groupingBy(
Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())
));
}
@Nullable
static MarkdownStatisticModel launchResultsToMarkdownStatisticModel(Collection<TestResult> launchResults, String baseLink, String reportLink) {
var rows = launchResults.stream()
.filter(scenario -> scenario.getStatus() != SKIPPED && scenario.getStatus() != UNKNOWN)
.map(scenario ->
new MarkdownStatisticModel.Row(
substringBefore(scenario.getName(), '\n'),
!isPassed(scenario) ? new Statistic(1, LocalDate.now().toString(), join(reportLink, "#suites", scenario.getUid())) : none,
isPassed(scenario) ? new Statistic(1, LocalDate.now().toString(), join(reportLink, "#suites", scenario.getUid())) : none
)
).collect(
Collectors.toMap(
MarkdownStatisticModel.Row::scenario,
it -> it,
MarkdownStatisticModel.Row::merge
)
)
.values()
.stream()
.sorted(Comparator.comparing(MarkdownStatisticModel.Row::scenario))
.toList();
if (!rows.isEmpty())
return new MarkdownStatisticModel(
COMMENT_TITLE,
rows,
new Total(rows.size()),
new MarkdownStatisticModel.Footer(ALLURE_SERVER, baseLink)
);
else
return null;
}
private static boolean isPassed(TestResult result) {
return result.getStatus() == Status.PASSED || result.getStatus() == SKIPPED;
}
private static boolean isIssueKey(Link allureLink, String tmsHost, Pattern issuePattern) {
var isTms = StringUtils.substringAfter(allureLink.getUrl(), "//").startsWith(tmsHost);
var hasKey = issuePattern.matcher(allureLink.getUrl()).find() || issuePattern.matcher(allureLink.getName()).find();
return isTms && hasKey;
}
private static String extractIssueKey(Link allureLink, Pattern issuePattern) {
Matcher matcher = issuePattern.matcher(allureLink.getName());
if (matcher.find())
return matcher.group(0);
matcher = Pattern.compile("(" + issuePattern.pattern() + ")").matcher(allureLink.getUrl());
if (matcher.find())
return matcher.group(1);
throw new IllegalArgumentException("Failed to extract issue key from link: " + allureLink);
}
}

View File

@ -0,0 +1,241 @@
package ru.iopump.qa.allure.helper.plugin.youtrack;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toMap;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public record MarkdownStatisticModel(
String title,
List<Row> scenarioStatisticRows,
Total total,
Footer footer
) {
public String toMarkdown() {
return """
### %s
| **Scenario** | `Failed` | `Passed` |
|--------------|-------------|--------------|
%s
%s
%s
""".formatted(
title,
scenarioStatisticRows.stream().sorted(comparing(Row::scenario)).map(Row::toMarkdown).collect(joining("\n")),
total.toMarkdown(),
footer.toMarkdown());
}
public static MarkdownStatisticModel toModel(String text) {
var lines = text.lines()
.filter(it -> !it.isBlank())
.toList();
var linesIterator = lines.iterator();
var title = linesIterator.hasNext() ? StringUtils.substringAfter(linesIterator.next(), "###").trim() : "parsing_error";
if (linesIterator.hasNext()) linesIterator.next();
if (linesIterator.hasNext()) linesIterator.next();
var rows = new ArrayList<Row>();
String rowText = null;
while (linesIterator.hasNext()) {
rowText = linesIterator.next();
var newRow = Row.toModel(rowText);
if (newRow == Row.error)
break;
rows.add(newRow);
rowText = null;
}
var total = rowText != null ? Total.toModel(rowText) : Total.error;
var footer = linesIterator.hasNext() ? Footer.toModel(linesIterator.next()) : Footer.error;
var model = new MarkdownStatisticModel(title, rows, total, footer);
return model.hasError()
? new MarkdownStatisticModel(title, rows, total, new Footer(footer.generatedBy + " **`with errors check logs`**", footer.link))
: model;
}
public MarkdownStatisticModel merge(MarkdownStatisticModel other) {
var rows = this.mergeRows(other.scenarioStatisticRows);
return new MarkdownStatisticModel(
this.title,
this.mergeRows(other.scenarioStatisticRows),
new Total(rows.size()),
this.footer.merge(other.footer)
);
}
private List<Row> mergeRows(List<Row> other) {
return Stream.concat(this.scenarioStatisticRows.stream(), other.stream())
.collect(toMap(
it -> it.scenario,
it -> it,
Row::merge,
LinkedHashMap::new))
.values()
.stream()
.sorted(comparing(Row::scenario))
.toList();
}
public boolean hasError() {
return scenarioStatisticRows.stream().anyMatch(Row::hasError) || total.hasError() || footer.hasError();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public record Row(
String scenario,
Statistic passed,
Statistic failed
) {
private static final Row error = new Row("parsing_error", Statistic.error, Statistic.error);
private static final Pattern pattern = Pattern.compile("\\| (.+?) \\| (.+?) \\| (.+?) \\|");
private static final String md_template = "| %s | %s | %s |";
public String toMarkdown() {
return String.format(md_template, scenario, passed.toMarkdown(), failed.toMarkdown());
}
public static Row toModel(String text) {
var matcher = pattern.matcher(text);
if (matcher.find())
return new Row(
matcher.group(1).trim(),
Statistic.toModel(matcher.group(2).trim()),
Statistic.toModel(matcher.group(3).trim())
);
else
return error;
}
public Row merge(Row other) {
if (other == error) return this;
return this == error ? other : new Row(
this.scenario,
this.passed.merge(other.passed),
this.failed.merge(other.failed)
);
}
public boolean hasError() {
return this == error || passed.hasError() || failed.hasError();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public record Statistic(
int count,
String latestDate,
String latestLink
) {
public static final Statistic none = new Statistic(0, "", "");
static final Statistic error = new Statistic(0, "parsing_error", "parsing_error");
private static final Pattern pattern = Pattern.compile("\\*\\*(\\d+)\\*\\* times \\[`latest` on (.+)]\\((.+)\\)");
private static final String md_template = "**%d** times [`latest` on %s](%s)";
public String toMarkdown() {
return String.format(md_template, count, latestDate, latestLink);
}
public static Statistic toModel(String text) {
var matcher = pattern.matcher(text);
if (matcher.find())
return new Statistic(
Integer.parseInt(matcher.group(1).trim()),
matcher.group(2).trim(),
matcher.group(3).trim()
);
else
return error;
}
public Statistic merge(Statistic other) {
if (other == error || other.count <= 0) return this;
return this == error ? other : new Statistic(
this.count + other.count,
isNotBlank(other.latestDate) ? other.latestDate : this.latestDate,
isNotBlank(other.latestLink) ? other.latestLink : this.latestLink
);
}
public boolean hasError() {
return this == error;
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public record Total(int total) {
private static final Total error = new Total(-1);
private static final Pattern pattern = Pattern.compile("Total test scenarios: \\*\\*(\\d+)\\*\\*");
private static final String md_template = "Total test scenarios: **%d**";
public String toMarkdown() {
return String.format(md_template, total);
}
public static Total toModel(String text) {
var matcher = pattern.matcher(text);
if (matcher.find())
return new Total(Integer.parseInt(matcher.group(1).trim()));
else
return error;
}
public boolean hasError() {
return this == error;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public record Footer(String generatedBy, String link) {
private static final Footer error = new Footer("parsing_error", "parsing_error");
private static final Pattern pattern = Pattern.compile("_Generated by \\[(.+)]\\((.+)\\)_");
private static final String md_template = "_Generated by [%s](%s)_";
public String toMarkdown() {
return String.format(md_template, generatedBy, link);
}
public static Footer toModel(String text) {
var matcher = pattern.matcher(text);
if (matcher.find())
return new Footer(
matcher.group(1),
matcher.group(2)
);
else
return error;
}
public Footer merge(Footer other) {
return new Footer(
other.generatedBy,
other.link
);
}
public boolean hasError() {
return this == error;
}
}
}

View File

@ -0,0 +1,10 @@
### E2E testing scenarios
| **Scenario** | ❌ `Failed` | ✅ `Passed` |
|--------------|--------------------------------------------|--------------------------------------------|
| Data 1 | **2** times [`latest` on 01.01.2024](link) | **3** times [`latest` on 01.01.2024](link) |
| Data 5 | **6** times [`latest` on 01.01.2023](link) | **7** times [`latest` on 01.01.2023](link) |
Total test scenarios: **2**
_Generated by [Allure Report Plugin - YouTrack Plugin](link)_

View File

@ -1,18 +1,19 @@
package ru.iopump.qa.allure.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import ru.iopump.qa.allure.service.PathUtil;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
public class ReportGenerateRequest {

View File

@ -3,7 +3,7 @@ package ru.iopump.qa.allure.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.base.Joiner;
import io.qameta.allure.entity.ExecutorInfo;
import javax.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import lombok.NoArgsConstructor;

View File

@ -1,13 +1,15 @@
package ru.iopump.qa.allure.properties;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.ToString;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.core.io.Resource;
import javax.annotation.PostConstruct;
import java.nio.file.Path;
import java.nio.file.Paths;
@ -16,29 +18,32 @@ import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
@ConfigurationProperties(prefix = "allure")
@Getter
@Accessors(fluent = true)
@ConstructorBinding
@Slf4j
@ToString
public class AllureProperties {
private final Reports reports;
private final String resultsDir;
private final boolean supportOldFormat;
private final String dateFormat;
private final String serverBaseUrl;
@Nullable
private final Resource logo;
private final String title;
public AllureProperties(Reports reports, String resultsDir, boolean supportOldFormat, String dateFormat, String serverBaseUrl) {
@ConstructorBinding
public AllureProperties(Reports reports, String resultsDir, String dateFormat, String serverBaseUrl, @Nullable Resource logo, String title) {
this.reports = defaultIfNull(reports, new Reports());
this.resultsDir = defaultIfNull(resultsDir, "allure/results/");
this.supportOldFormat = defaultIfNull(supportOldFormat, false);
this.dateFormat = defaultIfNull(dateFormat, "yy/MM/dd HH:mm:ss");
this.serverBaseUrl = defaultIfNull(serverBaseUrl, null);
this.logo = logo;
this.title = title;
}
@PostConstruct
void init() {
if (log.isInfoEnabled())
log.info("[ALLURE SERVER CONFIGURATION] Main AllureProperties parameters: " + this);
log.info("[ALLURE SERVER CONFIGURATION] Main AllureProperties parameters: {}", this);
}
@Getter

View File

@ -1,20 +1,19 @@
package ru.iopump.qa.allure.properties;
import jakarta.annotation.PostConstruct;
import lombok.Getter;
import lombok.ToString;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import javax.annotation.PostConstruct;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
@ConfigurationProperties(prefix = "basic.auth")
@Getter
@Accessors(fluent = true)
@ConstructorBinding
@Slf4j
@ToString(exclude = "password")
public class BasicProperties {
@ -23,6 +22,7 @@ public class BasicProperties {
private final String password;
private final boolean enable;
@ConstructorBinding
public BasicProperties(String username, String password, boolean enable) {
this.username = defaultIfNull(username, "admin");
this.password = defaultIfNull(password, "admin");

View File

@ -3,7 +3,7 @@ package ru.iopump.qa.allure.properties;
import lombok.Getter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConstructorBinding;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import java.time.LocalDateTime;
import java.time.LocalTime;

View File

@ -0,0 +1,19 @@
package ru.iopump.qa.allure.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.regex.Pattern;
@Data
@ConfigurationProperties(prefix = "tms")
public class TmsProperties {
private final boolean enabled;
private final String host;
private final String apiBaseUrl;
private final String project;
private final String token;
private final Pattern issueKeyPattern;
private final boolean dryRun;
}

View File

@ -1,66 +0,0 @@
package ru.iopump.qa.allure.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import ru.iopump.qa.allure.properties.BasicProperties;
@SuppressWarnings("deprecation")
@Configuration
@EnableWebSecurity
@Slf4j
public class BasicConfiguration extends WebSecurityConfigurerAdapter {
private final BasicProperties basicProperties;
private final boolean enableOAuth2;
private final boolean enableBasicAuth;
private final boolean enableAnyAuth;
BasicConfiguration(BasicProperties basicProperties, @Value("${app.security.enable-oauth2:false}") boolean enableOAuth2) {
super();
this.basicProperties = basicProperties;
this.enableBasicAuth = basicProperties.enable();
this.enableOAuth2 = enableOAuth2;
this.enableAnyAuth = enableBasicAuth || enableOAuth2;
log.info("[ALLURE SERVER SECURITY] Basic Auth: {} | OAuth2: {}", enableBasicAuth, enableOAuth2);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
if (enableBasicAuth)
auth.inMemoryAuthentication()
.withUser(basicProperties.username())
.password(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode(basicProperties.password()))
.roles("USER", "ADMIN");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers().frameOptions().sameOrigin()
.and()
.csrf().disable()
.requestCache().requestCache(new CustomRequestCache());
if (enableAnyAuth)
http
.authorizeRequests()
.requestMatchers(SecurityUtils::isFrameworkInternalRequest).permitAll()
.anyRequest().authenticated();
if (enableOAuth2)
http
.oauth2Login();
if (enableBasicAuth)
http
.httpBasic();
}
}

View File

@ -1,7 +1,7 @@
package ru.iopump.qa.allure.security;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
class CustomRequestCache extends HttpSessionRequestCache {

View File

@ -0,0 +1,74 @@
package ru.iopump.qa.allure.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import ru.iopump.qa.allure.properties.BasicProperties;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
@EnableWebSecurity
@Slf4j
public class SecurityConfiguration {
private final BasicProperties basicProperties;
private final boolean enableOAuth2;
private final boolean enableBasicAuth;
private final boolean enableAnyAuth;
public SecurityConfiguration(BasicProperties basicProperties, @Value("${app.security.enable-oauth2:false}") boolean enableOAuth2) {
this.basicProperties = basicProperties;
this.enableBasicAuth = basicProperties.enable();
this.enableOAuth2 = enableOAuth2;
this.enableAnyAuth = enableBasicAuth || enableOAuth2;
log.info("[ALLURE SERVER SECURITY] Basic Auth: {} | OAuth2: {}", enableBasicAuth, enableOAuth2);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.headers(it -> it.frameOptions(FrameOptionsConfig::sameOrigin))
.csrf(AbstractHttpConfigurer::disable)
.requestCache(it -> it.requestCache(new CustomRequestCache()));
if (enableAnyAuth)
http
.authorizeHttpRequests(it -> it
.requestMatchers(SecurityUtils::isFrameworkInternalRequest).permitAll()
.anyRequest().authenticated());
if (enableOAuth2)
http
.oauth2Login(withDefaults());
if (enableBasicAuth)
http
.httpBasic(withDefaults());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
UserDetails user = User.withUsername(basicProperties.username())
.password(encoder.encode(basicProperties.password()))
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}
}

View File

@ -2,9 +2,9 @@ package ru.iopump.qa.allure.security;
import com.vaadin.flow.server.HandlerHelper;
import com.vaadin.flow.shared.ApplicationConstants;
import jakarta.servlet.http.HttpServletRequest;
import lombok.experimental.UtilityClass;
import javax.servlet.http.HttpServletRequest;
import java.util.stream.Stream;
@UtilityClass

View File

@ -3,6 +3,7 @@ package ru.iopump.qa.allure.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
@ -16,11 +17,13 @@ import ru.iopump.qa.allure.properties.AllureProperties;
import ru.iopump.qa.allure.properties.CleanUpProperties;
import ru.iopump.qa.allure.repo.JpaReportRepository;
import javax.annotation.PostConstruct;
import java.io.File;
import java.time.*;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Collection;
import java.util.Date;
import java.util.Optional;
import java.util.stream.Collectors;
@ -41,15 +44,15 @@ public class CleanUpServiceConfiguration implements SchedulingConfigurer {
private static String print(Collection<Pair<ReportEntity, Boolean>> removedReports) {
return removedReports.stream().map(pair ->
format("CleanUpResult(id=%s, path=%s, create=%s, age=%sd, isDeleted=%s)",
pair.getKey().getUuid(),
pair.getKey().getPath(),
pair.getKey().getCreatedDateTime(),
Duration.between(
pair.getKey().getCreatedDateTime(),
LocalDateTime.now()
).toDays(),
pair.getValue())
format("CleanUpResult(id=%s, path=%s, create=%s, age=%sd, isDeleted=%s)",
pair.getKey().getUuid(),
pair.getKey().getPath(),
pair.getKey().getCreatedDateTime(),
Duration.between(
pair.getKey().getCreatedDateTime(),
LocalDateTime.now()
).toDays(),
pair.getValue())
).collect(Collectors.joining(", "));
}
@ -69,63 +72,63 @@ public class CleanUpServiceConfiguration implements SchedulingConfigurer {
}
final Collection<ReportEntity> candidatesCleanUp = repository
.findAllByCreatedDateTimeIsBefore(cleanUpProperties.getClosestEdgeDate());
.findAllByCreatedDateTimeIsBefore(cleanUpProperties.getClosestEdgeDate());
if (log.isDebugEnabled()) {
log.debug("CleanUp. All reports: " + repository.findAll().stream()
.map(e -> e.getUuid() + " " + e.getPath() + " " + e.getCreatedDateTime())
.collect(Collectors.joining(", ", "[", "]"))
.map(e -> e.getUuid() + " " + e.getPath() + " " + e.getCreatedDateTime())
.collect(Collectors.joining(", ", "[", "]"))
);
log.debug("CleanUp. Candidates to clean up: " + candidatesCleanUp.stream()
.map(e -> e.getUuid() + " " + e.getPath() + " " + e.getCreatedDateTime())
.collect(Collectors.joining(", ", "[", "]"))
.map(e -> e.getUuid() + " " + e.getPath() + " " + e.getCreatedDateTime())
.collect(Collectors.joining(", ", "[", "]"))
);
}
final Collection<Pair<ReportEntity, Boolean>> processedReports = candidatesCleanUp.stream()
.map(report ->
cleanUpProperties.getPaths().stream()
// Есть ли среди настроек paths для данного отчета
.filter(path -> report.getPath().equals(path.getPath())).findFirst()
// Если отчет подпадает под правила paths, то найти правило и использовать
.map(path -> {
if (report.getCreatedDateTime().isBefore(path.getEdgeDate()))
// Если отчет создан до крайней даты, то удалять
return delete(report);
else
// Оставить если младше
return Pair.of(report, false);
})
// Если отчет не подпадает под правила paths, то использовать общее правило ageDays
.orElseGet(() -> {
if (report.getCreatedDateTime().isBefore(cleanUpProperties.getEdgeDate()))
// Если отчет создан до крайней даты, то удалять
return delete(report);
else
// Оставить если младше
return Pair.of(report, false);
})
).collect(Collectors.toUnmodifiableList());
.map(report ->
cleanUpProperties.getPaths().stream()
// Есть ли среди настроек paths для данного отчета
.filter(path -> report.getPath().equals(path.getPath())).findFirst()
// Если отчет подпадает под правила paths, то найти правило и использовать
.map(path -> {
if (report.getCreatedDateTime().isBefore(path.getEdgeDate()))
// Если отчет создан до крайней даты, то удалять
return delete(report);
else
// Оставить если младше
return Pair.of(report, false);
})
// Если отчет не подпадает под правила paths, то использовать общее правило ageDays
.orElseGet(() -> {
if (report.getCreatedDateTime().isBefore(cleanUpProperties.getEdgeDate()))
// Если отчет создан до крайней даты, то удалять
return delete(report);
else
// Оставить если младше
return Pair.of(report, false);
})
).toList();
if (log.isInfoEnabled()) log.info("CleanUp finished with results: " + print(processedReports));
}, triggerContext -> {
final LocalDate nextDate = Optional.ofNullable(triggerContext.lastScheduledExecutionTime())
// Если триггер уже срабатывал, то прибавить день
.map(date -> date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate().plusDays(1))
// Если триггер не срабатывал, то взять текущий день
.orElse(LocalDate.now());
// Если триггер уже срабатывал, то прибавить день
.map(date -> date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate().plusDays(1))
// Если триггер не срабатывал, то взять текущий день
.orElse(LocalDate.now());
final LocalTime nextTime = cleanUpProperties.getTime();
// Следующее срабатывание из даты и времени из настроек
final LocalDateTime nextDateTime = LocalDateTime.of(nextDate, nextTime);
if (log.isInfoEnabled()) log.info("Next CleanUp scheduled at " + nextDateTime);
if (log.isInfoEnabled()) log.info("Next CleanUp scheduled at {}", nextDateTime);
return Date.from(nextDateTime.atZone(ZoneId.systemDefault()).toInstant());
return nextDateTime.atZone(ZoneId.systemDefault()).toInstant();
});
}

View File

@ -6,6 +6,10 @@ import com.fasterxml.jackson.databind.ObjectWriter;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import io.qameta.allure.entity.ExecutorInfo;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import jakarta.transaction.Transactional;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.NonNull;
import lombok.SneakyThrows;
@ -16,15 +20,10 @@ import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import ru.iopump.qa.allure.entity.ReportEntity;
import ru.iopump.qa.allure.helper.AllureReportGenerator;
import ru.iopump.qa.allure.helper.OldReportsFormatConverterHelper;
import ru.iopump.qa.allure.helper.ServeRedirectHelper;
import ru.iopump.qa.allure.properties.AllureProperties;
import ru.iopump.qa.allure.repo.JpaReportRepository;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.transaction.Transactional;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
@ -78,7 +77,7 @@ public class JpaReportService {
@PostConstruct
protected void initRedirection() {
repository.findByActiveTrue().forEach(
e -> redirection.mapRequestTo(join(cfg.reports().path(), e.getPath()), reportsDir.resolve(e.getUuid().toString()).toString())
e -> redirection.mapRequestTo(join(cfg.reports().path(), e.getPath()), reportsDir.resolve(e.getUuid().toString()).toString())
);
}
@ -88,11 +87,11 @@ public class JpaReportService {
// delete active history
entitiesActive
.forEach(e -> deleteQuietly(reportsDir.resolve(e.getUuid().toString()).resolve("history").toFile()));
.forEach(e -> deleteQuietly(reportsDir.resolve(e.getUuid().toString()).resolve("history").toFile()));
// delete active history
entitiesInactive
.forEach(e -> deleteQuietly(reportsDir.resolve(e.getUuid().toString()).toFile()));
.forEach(e -> deleteQuietly(reportsDir.resolve(e.getUuid().toString()).toFile()));
return entitiesInactive;
}
@ -132,45 +131,45 @@ public class JpaReportService {
final Path destination = reportUnzipService.unzipAndStore(archiveInputStream);
final UUID uuid = UUID.fromString(destination.getFileName().toString());
Preconditions.checkArgument(
Files.list(destination).anyMatch(path -> path.endsWith("index.html")),
"Uploaded archive is not an Allure Report"
Files.list(destination).anyMatch(path -> path.endsWith("index.html")),
"Uploaded archive is not an Allure Report"
);
// Find prev report if present
final Optional<ReportEntity> prevEntity = repository.findByPathOrderByCreatedDateTimeDesc(reportPath)
.stream()
.findFirst();
.stream()
.findFirst();
// Add CI executor information
var safeExecutorInfo = addExecutionInfo(
destination,
executorInfo,
baseUrl + str(reportsDir.resolve(uuid.toString())) + "/index.html",
uuid
destination,
executorInfo,
baseUrl + str(reportsDir.resolve(uuid.toString())) + "/index.html",
uuid
);
log.info("Report '{}' loaded", destination);
// New report entity
final ReportEntity newEntity = ReportEntity.builder()
.uuid(uuid)
.path(reportPath)
.createdDateTime(LocalDateTime.now(zeroZone()))
.url(join(baseUrl, cfg.reports().dir(), uuid.toString()) + "/")
.level(prevEntity.map(e -> e.getLevel() + 1).orElse(0L))
.active(true)
.size(ReportEntity.sizeKB(destination))
.buildUrl(
// Взять Build Url
ofNullable(safeExecutorInfo.getBuildUrl())
// Or Build Name
.or(() -> ofNullable(safeExecutorInfo.getBuildName()))
// Or Executor Name
.or(() -> ofNullable(safeExecutorInfo.getName()))
// Or Executor Type
.orElse(safeExecutorInfo.getType())
)
.build();
.uuid(uuid)
.path(reportPath)
.createdDateTime(LocalDateTime.now(zeroZone()))
.url(join(baseUrl, cfg.reports().dir(), uuid.toString()) + "/")
.level(prevEntity.map(e -> e.getLevel() + 1).orElse(0L))
.active(true)
.size(ReportEntity.sizeKB(destination))
.buildUrl(
// Взять Build Url
ofNullable(safeExecutorInfo.getBuildUrl())
// Or Build Name
.or(() -> ofNullable(safeExecutorInfo.getBuildName()))
// Or Executor Name
.or(() -> ofNullable(safeExecutorInfo.getName()))
// Or Executor Type
.orElse(safeExecutorInfo.getType())
)
.build();
// Add request mapping
redirection.mapRequestTo(newEntity.getPath(), reportsDir.resolve(uuid.toString()).toString());
@ -191,11 +190,6 @@ public class JpaReportService {
@Nullable ExecutorInfo executorInfo,
String baseUrl
) throws IOException {
if (cfg.supportOldFormat() && init.compareAndSet(false, true)) {
var old = new OldReportsFormatConverterHelper(cfg).convertOldFormat();
repository.saveAll(old);
old.forEach(e -> redirection.mapRequestTo(e.getPath(), reportsDir.resolve(e.getUuid().toString()).toString()));
}
// Preconditions
Preconditions.checkArgument(!resultDirs.isEmpty());
resultDirs.forEach(i -> Preconditions.checkArgument(Files.exists(i), "Result '%s' doesn't exist", i));
@ -205,33 +199,34 @@ public class JpaReportService {
// Find prev report if present
final Optional<ReportEntity> prevEntity = repository.findByPathOrderByCreatedDateTimeDesc(reportPath)
.stream()
.findFirst();
.stream()
.findFirst();
// New uuid directory
final Path destination = reportsDir.resolve(uuid.toString());
// Copy history from prev report
final Optional<Path> historyO = prevEntity
.flatMap(e -> copyHistory(reportsDir.resolve(e.getUuid().toString()), uuid.toString()))
.or(Optional::empty);
.flatMap(e -> copyHistory(reportsDir.resolve(e.getUuid().toString()), uuid.toString()))
.or(Optional::empty);
// Add CI executor information
var safeExecutorInfo = addExecutionInfo(
resultDirs.get(0),
executorInfo,
baseUrl + str(reportsDir.resolve(uuid.toString())) + "/index.html",
uuid
resultDirs.get(0),
executorInfo,
baseUrl + str(reportsDir.resolve(uuid.toString())) + "/index.html",
uuid
);
var reportUrl = join(baseUrl, cfg.reports().dir(), uuid.toString()) + "/";
try {
// Add history to results if exists
final List<Path> resultDirsToGenerate = historyO
.map(history -> (List<Path>) ImmutableList.<Path>builder().addAll(resultDirs).add(history).build())
.orElse(resultDirs);
.map(history -> (List<Path>) ImmutableList.<Path>builder().addAll(resultDirs).add(history).build())
.orElse(resultDirs);
// Generate new report with history
reportGenerator.generate(destination, resultDirsToGenerate);
reportGenerator.generate(destination, resultDirsToGenerate, reportUrl);
log.info("Report '{}' generated according to results '{}'", destination, resultDirsToGenerate);
} finally {
@ -245,24 +240,24 @@ public class JpaReportService {
// New report entity
final ReportEntity newEntity = ReportEntity.builder()
.uuid(uuid)
.path(reportPath)
.createdDateTime(LocalDateTime.now(zeroZone()))
.url(join(baseUrl, cfg.reports().dir(), uuid.toString()) + "/")
.level(prevEntity.map(e -> e.getLevel() + 1).orElse(0L))
.active(true)
.size(ReportEntity.sizeKB(destination))
.buildUrl(
// Взять Build Url
ofNullable(safeExecutorInfo.getBuildUrl())
// Or Build Name
.or(() -> ofNullable(safeExecutorInfo.getBuildName()))
// Or Executor Name
.or(() -> ofNullable(safeExecutorInfo.getName()))
// Or Executor Type
.orElse(safeExecutorInfo.getType())
)
.build();
.uuid(uuid)
.path(reportPath)
.createdDateTime(LocalDateTime.now(zeroZone()))
.url(reportUrl)
.level(prevEntity.map(e -> e.getLevel() + 1).orElse(0L))
.active(true)
.size(ReportEntity.sizeKB(destination))
.buildUrl(
// Взять Build Url
ofNullable(safeExecutorInfo.getBuildUrl())
// Or Build Name
.or(() -> ofNullable(safeExecutorInfo.getBuildName()))
// Or Executor Name
.or(() -> ofNullable(safeExecutorInfo.getName()))
// Or Executor Type
.orElse(safeExecutorInfo.getType())
)
.build();
// Add request mapping
redirection.mapRequestTo(newEntity.getPath(), reportsDir.resolve(uuid.toString()).toString());
@ -290,17 +285,17 @@ public class JpaReportService {
// If size more than max history
if (allReports.size() >= max) {
log.info("Current report count '{}' exceed max history report count '{}'",
allReports.size(),
max
allReports.size(),
max
);
// Delete last after max history
long deleted = allReports.stream()
.skip(max)
.peek(e -> log.info("Report '{}' will be deleted", e))
.peek(e -> deleteQuietly(reportsDir.resolve(e.getUuid().toString()).toFile()))
.peek(repository::delete)
.count();
.skip(max)
.peek(e -> log.info("Report '{}' will be deleted", e))
.peek(e -> deleteQuietly(reportsDir.resolve(e.getUuid().toString()).toFile()))
.peek(repository::delete)
.count();
// Update level (safety)
created.setLevel(Math.max(created.getLevel() - deleted, 0));

View File

@ -1,7 +1,8 @@
package ru.iopump.qa.allure.service;
import java.nio.file.Path;
import javax.annotation.Nullable;
import jakarta.annotation.Nullable;
import lombok.experimental.UtilityClass;
@SuppressWarnings("RedundantModifiersUtilityClassLombok")

View File

@ -1,25 +0,0 @@
import '@polymer/iron-icon/iron-icon.js';
import '@polymer/iron-iconset-svg/iron-iconset-svg.js';
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
const template = html`<iron-iconset-svg name="icomoon" size="1024">
<svg>
<defs>
<g id="linkedin">
<path fill="#0077b5" style="fill: var(--color1, #0077b5)" class="path1"
d="M872.405 872.619h-151.637v-237.611c0-56.661-1.152-129.579-79.019-129.579-79.061 0-91.136 61.653-91.136 125.397v241.792h-151.637v-488.619h145.664v66.603h1.963c20.352-38.4 69.845-78.933 143.787-78.933 153.643 0 182.059 101.12 182.059 232.747zM227.712 317.141c-48.811 0-88.021-39.509-88.021-88.107 0-48.555 39.253-88.021 88.021-88.021 48.64 0 88.064 39.467 88.064 88.021 0 48.597-39.467 88.107-88.064 88.107zM303.744 872.619h-152.064v-488.619h152.064zM948.267 0h-872.704c-41.771 0-75.563 33.024-75.563 73.771v876.459c0 40.789 33.792 73.771 75.563 73.771h872.576c41.728 0 75.861-32.981 75.861-73.771v-876.459c0-40.747-34.133-73.771-75.861-73.771z"></path>
</g>
<g id="github">
<path class="path1"
d="M512 12.672c-282.88 0-512 229.248-512 512 0 226.261 146.688 418.133 350.080 485.76 25.6 4.821 34.987-11.008 34.987-24.619 0-12.16-0.427-44.373-0.64-87.040-142.421 30.891-172.459-68.693-172.459-68.693-23.296-59.093-56.96-74.88-56.96-74.88-46.379-31.744 3.584-31.104 3.584-31.104 51.413 3.584 78.421 52.736 78.421 52.736 45.653 78.293 119.851 55.68 149.12 42.581 4.608-33.109 17.792-55.68 32.427-68.48-113.707-12.8-233.216-56.832-233.216-253.013 0-55.893 19.84-101.547 52.693-137.387-5.76-12.928-23.040-64.981 4.48-135.509 0 0 42.88-13.739 140.8 52.48 40.96-11.392 84.48-17.024 128-17.28 43.52 0.256 87.040 5.888 128 17.28 97.28-66.219 140.16-52.48 140.16-52.48 27.52 70.528 10.24 122.581 5.12 135.509 32.64 35.84 52.48 81.493 52.48 137.387 0 196.693-119.68 240-233.6 252.587 17.92 15.36 34.56 46.763 34.56 94.72 0 68.523-0.64 123.563-0.64 140.203 0 13.44 8.96 29.44 35.2 24.32 204.843-67.157 351.403-259.157 351.403-485.077 0-282.752-229.248-512-512-512z"></path>
</g>
<g id="docker">
<path fill="#1488c6" style="fill: var(--color2, #1488c6)" class="path1"
d="M205.653 737.067c-29.184 0-55.637-23.893-55.637-52.907s23.893-53.035 55.68-53.035c31.915 0 55.893 23.893 55.893 52.992s-26.539 52.907-55.936 52.949zM888.832 448.512c-5.76-42.325-32-76.8-66.56-103.253l-13.44-10.667-10.837 13.227c-21.077 23.893-29.44 66.261-26.88 97.92 2.56 23.979 10.24 47.787 23.637 66.304-10.837 5.547-24.235 10.667-34.56 16.085-24.32 7.979-47.957 10.667-71.68 10.667h-684.373l-2.56 15.787c-5.12 50.432 2.56 103.253 23.979 151.040l10.411 18.56v2.56c64 105.941 177.92 153.6 301.995 153.6 238.677 0 434.432-103.253 527.232-325.675 60.8 2.645 122.197-13.227 151.040-71.509l7.68-13.227-12.8-7.979c-34.56-21.077-81.92-23.893-121.6-13.227zM547.157 406.187h-103.595v103.253h103.68v-103.339zM547.157 276.352h-103.595v103.253h103.68v-103.125zM547.157 143.915h-103.595v103.253h103.68v-103.253zM673.877 406.187h-102.997v103.253h103.253v-103.339zM289.963 406.187h-102.955v103.253h103.339v-103.339zM419.243 406.187h-102.4v103.253h102.997v-103.339zM161.963 406.187h-102.229v103.253h103.595v-103.339zM419.243 276.352h-102.4v103.253h102.997v-103.125zM289.323 276.352h-102.144v103.253h102.955v-103.125z"></path>
</g>
</defs>
</svg>
</iron-iconset-svg>`;
document.head.appendChild(template.content);

View File

@ -16,18 +16,27 @@ spring:
database: H2
show-sql: false
hibernate.ddl-auto: update
cloud:
openfeign:
client:
config:
default:
loggerLevel: basic
vaadin.urlMapping: "/ui/*"
server.port: ${PORT:8080}
### Security
basic.auth:
username: admin
password: admin
enable: false
basic:
auth:
username: admin
password: admin
enable: false
### App Configuration
springdoc.swagger-ui.path: /swagger-ui.html
springdoc:
swagger-ui:
path: /swagger-ui.html
# #################
# Deprecated format. Not supported!
@ -40,8 +49,11 @@ springdoc.swagger-ui.path: /swagger-ui.html
# history.level: 20
# support.old.format: false
# date.format: "yy/MM/dd HH:mm:ss"
allure:
title: "BrewCode | Allure Report"
# FROM URL: https://avatars.githubusercontent.com/u/16944358?v=4
# FROM FILE: file:/images/logo.png
logo: ""
resultsDir: allure/results/
reports:
dir: allure/reports/
@ -64,4 +76,13 @@ logging:
org.springframework: INFO
org.springframework.core: WARN
org.springframework.beans.factory.support: WARN
ru.iopump.qa:allure: INFO # Allure Server Logs
ru.iopump.qa.allure: INFO # Allure Server Logs
ru.iopump.qa.allure.api: DEBUG
tms:
enabled: false
host: tms.localhost
api-base-url: https://${tms.host}/api
token: "my-token"
issue-key-pattern: "[A-Za-z]+-\\d+"
dry-run: false

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M349.9 236.3h-66.1v-59.4h66.1v59.4zm0-204.3h-66.1v60.7h66.1V32zm78.2 144.8H362v59.4h66.1v-59.4zm-156.3-72.1h-66.1v60.1h66.1v-60.1zm78.1 0h-66.1v60.1h66.1v-60.1zm276.8 100c-14.4-9.7-47.6-13.2-73.1-8.4-3.3-24-16.7-44.9-41.1-63.7l-14-9.3-9.3 14c-18.4 27.8-23.4 73.6-3.7 103.8-8.7 4.7-25.8 11.1-48.4 10.7H2.4c-8.7 50.8 5.8 116.8 44 162.1 37.1 43.9 92.7 66.2 165.4 66.2 157.4 0 273.9-72.5 328.4-204.2 21.4 .4 67.6 .1 91.3-45.2 1.5-2.5 6.6-13.2 8.5-17.1l-13.3-8.9zm-511.1-27.9h-66v59.4h66.1v-59.4zm78.1 0h-66.1v59.4h66.1v-59.4zm78.1 0h-66.1v59.4h66.1v-59.4zm-78.1-72.1h-66.1v60.1h66.1v-60.1z"/></svg>

After

Width:  |  Height:  |  Size: 816 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"/></svg>

After

Width:  |  Height:  |  Size: 655 B

View File

@ -0,0 +1 @@
loader.path=build/tmp/jar

View File

@ -56,6 +56,20 @@ allure.api.addTranslation('de', {
}
});
allure.api.addTranslation('nl', {
tab: {
behaviors: {
name: 'Functionaliteit'
}
},
widget: {
behaviors: {
name: 'Features en story’s',
showAll: 'Toon alle'
}
}
});
allure.api.addTranslation('he', {
tab: {
behaviors: {
@ -75,10 +89,10 @@ allure.api.addTranslation('br', {
behaviors: {
name: 'Comportamentos'
}
},
},
widget: {
behaviors: {
name: 'Funcionalidades por história',
name: 'Funcionalidades por história',
showAll: 'Mostrar tudo'
}
}
@ -126,6 +140,76 @@ allure.api.addTranslation('kr', {
}
});
allure.api.addTranslation('fr', {
tab: {
behaviors: {
name: 'Comportements'
}
},
widget: {
behaviors: {
name: 'Thèmes par histoires',
showAll: 'Montrer tout'
}
}
});
allure.api.addTranslation('pl', {
tab: {
behaviors: {
name: 'Zachowania'
}
},
widget: {
behaviors: {
name: 'Funkcje według historii',
showAll: 'pokaż wszystko'
}
}
});
allure.api.addTranslation('az', {
tab: {
behaviors: {
name: 'Davranışlar'
}
},
widget: {
behaviors: {
name: 'Hekayələr üzrə xüsusiyyətlər',
showAll: 'hamısını göstər'
}
}
});
allure.api.addTranslation('sv', {
tab: {
behaviors: {
name: 'Beteenden'
}
},
widget: {
behaviors: {
name: 'Funktioner efter user stories',
showAll: 'visa allt'
}
}
});
allure.api.addTranslation('isv', {
tab: {
behaviors: {
name: 'Funkcionalnost',
}
},
widget: {
behaviors: {
name: 'Funkcionalnost',
showAll: 'pokaži vsěčto',
}
}
});
allure.api.addTab('behaviors', {
title: 'tab.behaviors.name', icon: 'fa fa-list',
route: 'behaviors(/)(:testGroup)(/)(:testResult)(/)(:testResultTab)(/)',
@ -147,4 +231,4 @@ allure.api.addWidget('widgets', 'behaviors', allure.components.WidgetStatusView.
title: 'widget.behaviors.name',
baseUrl: 'behaviors',
showLinks: true
}));
}));

View File

@ -1,24 +1 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
<svg xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 128 128" version="1.1" viewBox="0 0 128 128" xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"><g id="Layer_1"><rect fill="#F4F5F5" height="1520" opacity="0" width="727.938" x="-59.984" y="-351"/></g>
<g id="Layer_2"><g><circle cx="64" cy="64" fill="#6E9583" r="64"/><g><defs><circle cx="64" cy="64" id="SVGID_3_" r="64"/></defs>
<clipPath id="SVGID_2_"><use overflow="visible" xlink:href="#SVGID_3_"/></clipPath>
<polygon clip-path="url(#SVGID_2_)" fill="#648778" points="93.572,29.677 128,64 128,128 54.36,128 33.341,106.906 "/></g><path
d="M84.044,20H36.018C33.579,20,32,22.11,32,24.549v78.903c0,2.439,1.579,4.549,4.018,4.549h55.989 c2.439,0,4.018-2.11,4.018-4.549V32.143L84.044,20z"
fill="#F1F1F1"/><g><defs><path d="M84.044,20H36.018C33.579,20,32,22.11,32,24.549v78.903c0,2.439,1.579,4.549,4.018,4.549h55.989 c2.439,0,4.018-2.11,4.018-4.549V32.143L84.044,20z" id="SVGID_5_"/></defs>
<clipPath id="SVGID_4_"><use overflow="visible" xlink:href="#SVGID_5_"/></clipPath>
<g clip-path="url(#SVGID_4_)"><polygon fill="#DDE1F1" points="50.948,67.621 65.539,82.042 42.971,83.087 49.777,90 42.971,91.087 49.277,97.555 42.971,99.087 53.027,109.305 97.684,109.305 97.684,75.707 97.075,54.055 81.059,37.758 70.97,44.918 62.684,35.107 "/></g></g><path
d="M88.186,32.138l7.839,0.005L84.044,20v7.96C84.044,30.398,85.769,32.138,88.186,32.138z" fill="#C2DFC9"/><path
d="M84,83.5H44c-0.828,0-1.5-0.672-1.5-1.5s0.672-1.5,1.5-1.5h40c0.828,0,1.5,0.672,1.5,1.5 S84.828,83.5,84,83.5z"
fill="#495260"/><path
d="M84,91.5H44c-0.828,0-1.5-0.672-1.5-1.5s0.672-1.5,1.5-1.5h40c0.828,0,1.5,0.672,1.5,1.5 S84.828,91.5,84,91.5z"
fill="#495260"/><path
d="M84,99.5H44c-0.828,0-1.5-0.672-1.5-1.5s0.672-1.5,1.5-1.5h40c0.828,0,1.5,0.672,1.5,1.5 S84.828,99.5,84,99.5z"
fill="#495260"/><g><path d="M69.568,31.844l-1.319,11.303c2.314,0.88,4.242,2.728,5.132,5.245c0.573,1.619,0.631,3.292,0.274,4.851 l10.257,4.895c0.527,0.252,1.155-0.023,1.329-0.581c1.308-4.188,1.323-8.819-0.253-13.273 c-2.379-6.723-7.827-11.477-14.212-13.254C70.21,30.872,69.636,31.26,69.568,31.844z" fill="#0E9CD9"/>
<path d="M66.68,59.901c-3.653,0.668-7.398-1.12-9.176-4.38c-1.094-2.006-1.312-4.174-0.858-6.157L46.39,44.469 c-0.527-0.251-1.155,0.023-1.329,0.58c-1.286,4.118-1.322,8.663,0.175,13.049c3.701,10.842,15.624,16.783,26.503,13.191 c4.655-1.537,8.399-4.531,10.911-8.3c0.324-0.486,0.141-1.147-0.385-1.398l-10.257-4.896 C70.751,58.296,68.929,59.49,66.68,59.901z"
fill="#E95037"/>
<path d="M62.239,43.074c0.734-0.26,1.479-0.405,2.22-0.464l1.316-11.275c0.067-0.576-0.389-1.08-0.968-1.071 c-2.218,0.035-4.469,0.421-6.676,1.202c-4.455,1.576-8.045,4.5-10.479,8.151c-0.324,0.486-0.142,1.147,0.385,1.399l10.257,4.895 C59.282,44.654,60.62,43.647,62.239,43.074z"
fill="#69B32D"/>
<g><defs><path d="M69.695,30.76l-1.446,12.387c2.314,0.88,4.242,2.728,5.132,5.245c0.573,1.619,0.631,3.292,0.274,4.851 l10.257,4.895c0.527,0.252,1.155-0.023,1.329-0.581c1.308-4.188,1.323-8.819-0.253-13.273 C82.476,37.185,76.541,32.281,69.695,30.76z M66.68,59.901c-3.653,0.668-7.398-1.12-9.176-4.38 c-1.094-2.006-1.312-4.174-0.858-6.157L46.39,44.469c-0.527-0.251-1.155,0.023-1.329,0.58 c-1.286,4.118-1.322,8.663,0.175,13.049c3.701,10.842,15.624,16.783,26.503,13.191c4.655-1.537,8.399-4.531,10.911-8.3 c0.324-0.486,0.141-1.147-0.385-1.398l-10.257-4.896C70.751,58.296,68.929,59.49,66.68,59.901z M62.239,43.074 c0.734-0.26,1.479-0.405,2.22-0.464l1.316-11.275c0.067-0.576-0.389-1.08-0.968-1.071c-2.218,0.035-4.469,0.421-6.676,1.202 c-4.455,1.576-8.045,4.5-10.479,8.151c-0.324,0.486-0.142,1.147,0.385,1.399l10.257,4.895 C59.282,44.654,60.62,43.647,62.239,43.074z" id="SVGID_7_"/></defs>
<clipPath id="SVGID_6_"><use overflow="visible" xlink:href="#SVGID_7_"/></clipPath>
<circle clip-path="url(#SVGID_6_)" cx="65.151" cy="51.304" fill="#FFFFFF" opacity="0.4" r="12.507"/></g></g></g></g></svg>
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 128 128" version="1.1" viewBox="0 0 128 128" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Layer_1"><rect fill="#F4F5F5" height="1520" opacity="0" width="727.938" x="-59.984" y="-351"/></g><g id="Layer_2"><g><circle cx="64" cy="64" fill="#6E9583" r="64"/><g><defs><circle cx="64" cy="64" id="SVGID_3_" r="64"/></defs><clipPath id="SVGID_2_"><use overflow="visible" xlink:href="#SVGID_3_"/></clipPath><polygon clip-path="url(#SVGID_2_)" fill="#648778" points="93.572,29.677 128,64 128,128 54.36,128 33.341,106.906 "/></g><path d="M84.044,20H36.018C33.579,20,32,22.11,32,24.549v78.903c0,2.439,1.579,4.549,4.018,4.549h55.989 c2.439,0,4.018-2.11,4.018-4.549V32.143L84.044,20z" fill="#F1F1F1"/><g><defs><path d="M84.044,20H36.018C33.579,20,32,22.11,32,24.549v78.903c0,2.439,1.579,4.549,4.018,4.549h55.989 c2.439,0,4.018-2.11,4.018-4.549V32.143L84.044,20z" id="SVGID_5_"/></defs><clipPath id="SVGID_4_"><use overflow="visible" xlink:href="#SVGID_5_"/></clipPath><g clip-path="url(#SVGID_4_)"><polygon fill="#DDE1F1" points="50.948,67.621 65.539,82.042 42.971,83.087 49.777,90 42.971,91.087 49.277,97.555 42.971,99.087 53.027,109.305 97.684,109.305 97.684,75.707 97.075,54.055 81.059,37.758 70.97,44.918 62.684,35.107 "/></g></g><path d="M88.186,32.138l7.839,0.005L84.044,20v7.96C84.044,30.398,85.769,32.138,88.186,32.138z" fill="#C2DFC9"/><path d="M84,83.5H44c-0.828,0-1.5-0.672-1.5-1.5s0.672-1.5,1.5-1.5h40c0.828,0,1.5,0.672,1.5,1.5 S84.828,83.5,84,83.5z" fill="#495260"/><path d="M84,91.5H44c-0.828,0-1.5-0.672-1.5-1.5s0.672-1.5,1.5-1.5h40c0.828,0,1.5,0.672,1.5,1.5 S84.828,91.5,84,91.5z" fill="#495260"/><path d="M84,99.5H44c-0.828,0-1.5-0.672-1.5-1.5s0.672-1.5,1.5-1.5h40c0.828,0,1.5,0.672,1.5,1.5 S84.828,99.5,84,99.5z" fill="#495260"/><g><path d="M69.568,31.844l-1.319,11.303c2.314,0.88,4.242,2.728,5.132,5.245c0.573,1.619,0.631,3.292,0.274,4.851 l10.257,4.895c0.527,0.252,1.155-0.023,1.329-0.581c1.308-4.188,1.323-8.819-0.253-13.273 c-2.379-6.723-7.827-11.477-14.212-13.254C70.21,30.872,69.636,31.26,69.568,31.844z" fill="#0E9CD9"/><path d="M66.68,59.901c-3.653,0.668-7.398-1.12-9.176-4.38c-1.094-2.006-1.312-4.174-0.858-6.157L46.39,44.469 c-0.527-0.251-1.155,0.023-1.329,0.58c-1.286,4.118-1.322,8.663,0.175,13.049c3.701,10.842,15.624,16.783,26.503,13.191 c4.655-1.537,8.399-4.531,10.911-8.3c0.324-0.486,0.141-1.147-0.385-1.398l-10.257-4.896 C70.751,58.296,68.929,59.49,66.68,59.901z" fill="#E95037"/><path d="M62.239,43.074c0.734-0.26,1.479-0.405,2.22-0.464l1.316-11.275c0.067-0.576-0.389-1.08-0.968-1.071 c-2.218,0.035-4.469,0.421-6.676,1.202c-4.455,1.576-8.045,4.5-10.479,8.151c-0.324,0.486-0.142,1.147,0.385,1.399l10.257,4.895 C59.282,44.654,60.62,43.647,62.239,43.074z" fill="#69B32D"/><g><defs><path d="M69.695,30.76l-1.446,12.387c2.314,0.88,4.242,2.728,5.132,5.245c0.573,1.619,0.631,3.292,0.274,4.851 l10.257,4.895c0.527,0.252,1.155-0.023,1.329-0.581c1.308-4.188,1.323-8.819-0.253-13.273 C82.476,37.185,76.541,32.281,69.695,30.76z M66.68,59.901c-3.653,0.668-7.398-1.12-9.176-4.38 c-1.094-2.006-1.312-4.174-0.858-6.157L46.39,44.469c-0.527-0.251-1.155,0.023-1.329,0.58 c-1.286,4.118-1.322,8.663,0.175,13.049c3.701,10.842,15.624,16.783,26.503,13.191c4.655-1.537,8.399-4.531,10.911-8.3 c0.324-0.486,0.141-1.147-0.385-1.398l-10.257-4.896C70.751,58.296,68.929,59.49,66.68,59.901z M62.239,43.074 c0.734-0.26,1.479-0.405,2.22-0.464l1.316-11.275c0.067-0.576-0.389-1.08-0.968-1.071c-2.218,0.035-4.469,0.421-6.676,1.202 c-4.455,1.576-8.045,4.5-10.479,8.151c-0.324,0.486-0.142,1.147,0.385,1.399l10.257,4.895 C59.282,44.654,60.62,43.647,62.239,43.074z" id="SVGID_7_"/></defs><clipPath id="SVGID_6_"><use overflow="visible" xlink:href="#SVGID_7_"/></clipPath><circle clip-path="url(#SVGID_6_)" cx="65.151" cy="51.304" fill="#FFFFFF" opacity="0.4" r="12.507"/></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,4 +1,4 @@
.side-nav__brand {
background: url('custom-logo.svg') no-repeat left center !important;
margin-left: 10px;
}
}

Some files were not shown because too many files have changed in this diff Show More