diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml index 5b16d6c..f17e9f5 100644 --- a/.github/workflows/publish-docker-image.yml +++ b/.github/workflows/publish-docker-image.yml @@ -44,5 +44,5 @@ jobs: with: context: ./EcoSonar-API push: true - tags: ${{ steps.meta.outputs.tags }} + tags: type=raw,value=3.3 labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/API.md b/API.md index e89e8f5..980467d 100644 --- a/API.md +++ b/API.md @@ -3,10 +3,16 @@ New Postman Collection available with all endpoints : [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/9592977-29c7010f-0efd-4063-b76a-5b0f455b1829?action=collection%2Ffork&collection-url=entityId%3D9592977-29c7010f-0efd-4063-b76a-5b0f455b1829%26entityType%3Dcollection%26workspaceId%3Df7ed92ee-00aa-4dc1-95aa-9f7d2da44e68) + +Swagger User Interface available at the link : `[ECOSONAR-API-URL]/swagger/` + +Locally, available at this address : `http://localhost:3002/swagger/` + ---- **EcoSonar URL Configuration - GET URLs FROM PROJECT** ---- +![GET URLs FROM PROJECT](./images/get-urls-from-project.webp) * **URL** @@ -53,6 +59,7 @@ EcoSonar API is not able to request to the MongoDB Database : **EcoSonar URL Configuration - INSERT URLs IN PROJECT** ---- +![INSERT URLs IN PROJECT](./images/insert-urls-in-project.webp) * **URL** @@ -99,6 +106,8 @@ EcoSonar API is not able to request to the MongoDB Database : **EcoSonar URL Configuration - DELETE URL IN PROJECT** ---- +![DELETE URL IN PROJECT](./images/delete-url-in-project.webp) + You can delete one url at a time. * **URL** @@ -143,6 +152,7 @@ EcoSonar API is not able to request to the MongoDB Database : **EcoSonar URL Configuration - GET CRAWLER RESULT** ---- +![GET CRAWLER RESULT](./images/get-crawler-result.webp) * **URL** @@ -184,6 +194,8 @@ EcoSonar API failed to launch crawler: **EcoSonar LAUNCH ANALYSIS** ---- +![LAUNCH ANALYSIS](./images/launch-analysis.webp) + EcoSonar analysis is launched through this API call either directly with a curl command or Postman request or through a Sonarqube Analysis. API call is done asynchronously to avoid performance issue ( ~ 3 seconds to analyse one page) * **URL** @@ -212,6 +224,7 @@ EcoSonar analysis is launched through this API call either directly with a curl **EcoSonar ANALYSIS - RETRIEVE ANALYSIS PER PROJECT** ---- +![RETRIEVE ANALYSIS PER PROJECT](./images/retrieve-analysis-per-project.webp) * **URL** @@ -363,6 +376,7 @@ EcoSonar API is not able to request to the MongoDB Database : **EcoSonar ANALYSIS - RETRIEVE ANALYSIS PER URL** ---- +![RETRIEVE ANALYSIS PER URL](./images/retrieve-analysis-per-url.webp) * **URL** @@ -507,6 +521,8 @@ EcoSonar API is not able to request to the MongoDB Database : **EcoSonar ANALYSIS - GET PROJECT SCORES** ---- +![GET PROJECT SCORES](./images/get-project-scores.webp) + Retrieve current scores from EcoIndex, Lighthouse Performance and Accessibility and W3C Validator for the project * **URL** @@ -555,6 +571,141 @@ EcoSonar API is not able to request to the MongoDB Database : * **Code:** 500 Internal Server Error
+**EcoSonar ANALYSIS - GET AVERAGE OF ALL SCORES FOR PROJECTS REGISTERED IN ECOSONAR AT A DEFINED DATE** +---- +![GET AVERAGE OF ALL SCORES FOR PROJECTS REGISTERED IN ECOSONAR AT A DEFINED DATE](./images/get-average-all-scores-projects.webp) + +Retrieve all EcoSonar projects average for all scores (EcoIndex, Google Lighthouse and W3C Validator). You can retrieve the scores at a date defined or for last analysis made if no date defined + +* **URL** + `/api/ecosonar/info` + or + `/api/ecosonar/info?date=` + +* **Method:** + + `GET` + +* **URL Params** + + DATE is optional : if no date defined, will look at latest analysis otherwise search for the latest analysis made before that date. + + **Optional:** + + `DATE=[string]` with format YYYY-MM-DD + +* **Data Params** + + None + +* **Success Response:** + + * **Code:** 200
+ **Content:** ` + { + "nbProjects": 0, + "ecoIndex": 0, + "perfScore": 0, + "accessibilityScore": 0, + "w3cScore": 0 + }` + +* **Error Response:** + +If date format is wrong + + * **Code:** 400 BAD REQUEST
+ **Content:** `{ + "error": 'Bad date format: YYYY-MM-DD' +}` + + OR + +EcoSonar API is not able to request to the MongoDB Database or other internal error: + + * **Code:** 500 Internal Server Error
+ +**EcoSonar ANALYSIS - GET ALL PROJECTS SCORES FROM DATE DEFINED** +---- +![GET ALL PROJECTS SCORES FROM DATE DEFINED](./images/get-all-scores-projects.webp) + +Retrieve all EcoSonar projects and return the scores for each of them at the date defined, if date not filled it would be the latest analysis. + +* **URL** + `/api/project/all` + or + `/api/ecosonar/info?date=` + or + `/api/ecosonar/info?filterName=` + or + `/api/ecosonar/info?date=&filterName=` + + +* **Method:** + + `POST` + +* **URL Params** + + DATE is optional : if no date defined, will look at latest analysis otherwise search for the latest analysis made before that date. + FILTER-NAME is optional : retrieve projects whose name contains the string 'filterName' (case insensitive) if filled + + + **Optional:** + + `DATE=[string]` with format YYYY-MM-DD + `FILTER-NAME=[string]` + +* **Data Params** + + CATEGORY in "filterScore" can take the following enum : ecoIndex, perfScore, accessScore, w3cScore. + "score" is a value from 0 to 100, it will be the threshold for the CATEGORY. + "select" takes the value "upper" or "lower" according if you want only project whose scores have an average value higher than score or lower. + CATEGORY in "sortBy" can take the following enum : ecoIndex, perfScore, accessScore, w3cScore and name. + "order" can take the value "asc" or "desc" if you want to sort your projects according to the type. + + `{ + "filterScore" : { + "cat": "CATEGORY", + "score": 0, + "select": "upper" + }, + "sortBy": { + "type": "CATEGORY", + "order": "asc" + } + }` + +* **Success Response:** + + * **Code:** 200
+ **Content:** + `{ + "nbProjects": 0, + "projects": { + "PROJECT": { + "ecoIndex": 0, + "perfScore": 0, + "accessScore": 0, + "w3cScore": 0, + "nbUrl": 0 + },` + +* **Error Response:** + +If date format is wrong + + * **Code:** 400 BAD REQUEST
+ **Content:** `{ + "error": 'Bad date format: YYYY-MM-DD' +}` + + OR + +EcoSonar API is not able to request to the MongoDB Database or other internal error: + + * **Code:** 500 Internal Server Error
+ **EcoSonar ANALYSIS - RETRIEVE ECOSONAR AUDIT IN EXCEL FORMAT FOR PROJECT** ---- Retrieve audits from GreenIt-Analysis, Google Lighthouse and W3C Validator aggregated per project in an Excel format. @@ -601,6 +752,8 @@ EcoSonar API is not able to request to the MongoDB Database : **EcoSonar ANALYSIS - SAVE PROCEDURE FOR THE PROJECT** ---- +![SAVE PROCEDURE FOR THE PROJECT](./images/save-procedure-for-the-project.webp) + Procedure in Ecosonar are the configuration chosen by delivery teams to sort the EcoSonar recommandations related to ecodesign. You have 3 different configurations available in EcoSonar: - `scoreImpact` : best practices will be sorted by descending order of implementation (best practices not implemented returned first) @@ -653,6 +806,8 @@ EcoSonar API is not able to request to the MongoDB Database : **EcoSonar ANALYSIS - RETRIEVE PROCEDURE SAVED FOR THE PROJECT** ---- +![RETRIEVE PROCEDURE SAVED FOR THE PROJECT](./images/retrieve-procedure-saved-for-the-project.webp) + Procedure in Ecosonar are the configuration chosen by delivery teams to sort the EcoSonar recommandations related to ecodesign. This request will return you the procedure chosen for this project. @@ -700,6 +855,8 @@ EcoSonar API is not able to request to the MongoDB Database : **EcoSonar ANALYSIS - RETRIEVE BEST PRACTICES PER PROJECT** ---- +![RETRIEVE BEST PRACTICES PER PROJECT](./images/retrieve-best-practices-per-project.webp) + Retrieve audits from GreenIt-Analysis and Google Lighthouse aggregated per project. * **URL** @@ -786,6 +943,8 @@ EcoSonar API is not able to request to the MongoDB Database : **EcoSonar ANALYSIS - RETRIEVE BEST PRACTICES PER URL** ---- +![RETRIEVE BEST PRACTICES PER URL](./images/retrieve-best-practices-per-url.webp) + Retrieve audits from GreenIt-Analysis and Google Lighthouse per url audited. * **URL** @@ -873,6 +1032,7 @@ EcoSonar API is not able to request to the MongoDB Database : **EcoSonar Login Configuration - SAVE LOGIN AND PROXY FOR PROJECT** ---- +![SAVE LOGIN AND PROXY FOR PROJECT](./images/save-login-and-proxy-for-project.webp) * **URL** @@ -914,6 +1074,7 @@ EcoSonar API is not able to save into the MongoDB Database : **EcoSonar Login Configuration - GET LOGIN FOR PROJECT** ---- +![GET LOGIN FOR PROJECT](./images/get-login-for-project.webp) * **URL** @@ -960,6 +1121,7 @@ EcoSonar API is not able to request to the MongoDB Database : **EcoSonar Login Configuration - GET PROXY FOR PROJECT** ---- +![GET PROXY FOR PROJECT](./images/get-proxy-for-project.webp) * **URL** @@ -1006,6 +1168,7 @@ EcoSonar API is not able to request to the MongoDB Database : **EcoSonar Login Configuration - DELETE LOGIN FOR PROJECT** ---- +![DELETE LOGIN FOR PROJECT](./images/delete-login-for-project.webp) * **URL** @@ -1049,6 +1212,7 @@ EcoSonar API is not able to request to the MongoDB Database : **EcoSonar Login Configuration - DELETE PROXY FOR PROJECT** ---- +![DELETE PROXY FOR PROJECT](./images/delete-proxy-for-project.webp) * **URL** @@ -1080,6 +1244,7 @@ EcoSonar API is not able to request to the MongoDB Database : **EcoSonar USER FLOW Configuration - GET USER FLOW for URL** ---- +![GET USER FLOW for URL](./images/get-user-flow-for-url.webp) * **URL** @@ -1129,6 +1294,7 @@ EcoSonar API is not able to request to the MongoDB Database : **EcoSonar USER FLOW Configuration - SAVE USER FLOW for URL** ---- +![SAVE USER FLOW for URL](./images/save-user-flow-for-url.webp) * **URL** @@ -1169,7 +1335,7 @@ You want to add user flow to unexisting url : * **Code:** 400 BAD REQUEST
**Content:** `{ "error": "Url not found" -}}` +}` OR @@ -1179,6 +1345,7 @@ EcoSonar API is not able to request to the MongoDB Database : **EcoSonar USER FLOW Configuration - DELETE USER FLOW FOR URL** ---- +![DELETE USER FLOW FOR URL](./images/delete-user-flow-for-url.webp) * **URL** diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cf5e7e..9a63def 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,37 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## Version 3.3 , 07/11/2023 + +### Added +- Integrate new EcoCode features including : + - additional rules for Python and PHP + - new languages covered : Javascript, Typescript, Android and iOS +- Implement Swagger User Interface for a more friendly user interface of the API +- Automatically push a new Docker Image as a Github package for each new commit in the 'main' branch of the Github repository +- Add new API Endpoints to retrieve projects scores average at a selected date with filter and sorting configuration + +### Removed + +### Changed +- Fix some security vulnerabilities + +--- + +## Version 3.2 , 10/08/2023 + +### Added +- Include Ecocode documentation into EcoSonar website +- Update best practices documentation +- Add MongoDB Community Server connection as a potential database for EcoSonar + +### Removed + +### Changed +- BUG FIX: user journey flow not working when some CSS selectors are hidden in the page + +--- + ## Version 3.1 , 27/03/2023 ### Added diff --git a/EcoSonar-API/README.md b/EcoSonar-API/README.md index ad7b851..e652e1b 100644 --- a/EcoSonar-API/README.md +++ b/EcoSonar-API/README.md @@ -10,6 +10,10 @@ Then, the API can allow you to retrieve pre-formatted audit results using json f API Documentation : https://github.com/Accenture/EcoSonar/blob/main/API.md +Swagger User Interface available at the link : `[ECOSONAR-API-URL]/swagger/` + +Locally, available at this address : `http://localhost:3002/swagger/` + # Summary - [To start with](#to-start-with) - [MongoDB Database](#mongodb-database) @@ -206,30 +210,30 @@ Then choose among the variables below the ones required and add it into `.env` f ##### MongoDB Community Server ``` -ECOSONAR_ENV_DB_TYPE=’MongoDB’ -ECOSONAR_ENV_CLUSTER = 'localhost' or ‘127.0.0.1’ +ECOSONAR_ENV_DB_TYPE = 'MongoDB' +ECOSONAR_ENV_CLUSTER = 'localhost' or '127.0.0.1' ECOSONAR_ENV_DB_PORT = '27017' ECOSONAR_ENV_DB_NAME = 'EcoSonar' ``` ##### MongoDB Atlas ``` -ECOSONAR_ENV_DB_TYPE= MongoDB_Atlas +ECOSONAR_ENV_DB_TYPE= 'MongoDB_Atlas' ECOSONAR_ENV_CLUSTER = #cluster ECOSONAR_ENV_DB_NAME = 'EcoSonar' ECOSONAR_ENV_USER = #user -ECOSONAR_ENV_CLOUD_PROVIDER= ‘local’ (the password will be retrieved from the environment variables) +ECOSONAR_ENV_CLOUD_PROVIDER= 'local' (the password will be retrieved from the environment variables) ECOSONAR_ENV_PASSWORD = #password ``` ###### Azure CosmosDB ``` -ECOSONAR_ENV_DB_TYPE= ‘CosmosDB’ +ECOSONAR_ENV_DB_TYPE= 'CosmosDB' ECOSONAR_ENV_CLUSTER = #cluster ECOSONAR_ENV_DB_PORT = #port ECOSONAR_ENV_DB_NAME = 'EcoSonar' ECOSONAR_ENV_USER = #user -ECOSONAR_ENV_CLOUD_PROVIDER= ‘AZURE’ (the password will be retrieved from the Azure Key Vault) or ‘local’ (the password will be retrieved from the environment variables) +ECOSONAR_ENV_CLOUD_PROVIDER= 'AZURE' (the password will be retrieved from the Azure Key Vault) or ‘local’ (the password will be retrieved from the environment variables) ECOSONAR_ENV_PASSWORD = #password (if ECOSONAR_ENV_CLOUD_PROVIDER=’local’) ECOSONAR_ENV_KEY_VAULT_NAME= #keyVaultName (if ECOSONAR_ENV_CLOUD_PROVIDER=’AZURE’) ECOSONAR_ENV_SECRET_NAME = #keyVaultSecretName (if ECOSONAR_ENV_CLOUD_PROVIDER=’AZURE’) @@ -239,9 +243,8 @@ ECOSONAR_ENV_SECRET_NAME = #keyVaultSecretName (if ECOSONAR_ENV_CLOUD_PROVIDER= ##### Other database configuration possible If you are not using the same MongoDB database than us, you can develop your own. -Please check to the `EcoSonar-API/configuration/database.js` to set up a different connection string to your database. +Please check to the `EcoSonar-API/configuration/database.js` to set up a different connection string to your database and `EcoSonar-API/configuration/retrieveDatabasePasswordFromCloud.js` for another password manager solution. We would be very happy if you want to share this new set up in a Pull Request in the Github Repository to enrich the community. -and `EcoSonar-API/configuration/retrieveDatabasePasswordFromCloud.js` for another password manager solution. #### CORS Setup @@ -269,9 +272,19 @@ ECOSONAR_ENV_USER_JOURNEY_ENABLED = take `true`or `false` # API Endpoints +1. With Postman + [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/9592977-29c7010f-0efd-4063-b76a-5b0f455b1829?action=collection%2Ffork&collection-url=entityId%3D9592977-29c7010f-0efd-4063-b76a-5b0f455b1829%26entityType%3Dcollection%26workspaceId%3Df7ed92ee-00aa-4dc1-95aa-9f7d2da44e68) -For documentation on available API : https://github.com/Accenture/EcoSonar/blob/main/API.md +2. With Swagger + +For Swagger User Interface : `[ECOSONAR-API-URL]/swagger/` + +Locally, available at this address : `http://localhost:3002/swagger/` + +3. Additional documentation + +https://github.com/Accenture/EcoSonar/blob/main/API.md # Authentication Configuration diff --git a/EcoSonar-API/dataBase/greenItRepository.js b/EcoSonar-API/dataBase/greenItRepository.js index 117f971..7efd697 100644 --- a/EcoSonar-API/dataBase/greenItRepository.js +++ b/EcoSonar-API/dataBase/greenItRepository.js @@ -32,6 +32,22 @@ const GreenItRepository = function () { }) } + /** + * find all EcoIndex analysis + * @returns + */ + this.findAllAnalysis = async function () { + return new Promise((resolve, reject) => { + greenits.find({}) + .then((res) => { + resolve(res) + }) + .catch(() => { + reject(new SystemError()) + }) + }) + } + /** * find analysis for one url : OK * @param {project Name} projectNameReq @@ -52,7 +68,7 @@ const GreenItRepository = function () { .find({ idUrlGreen: res[0].idKey }, { domSize: 1, nbRequest: 1, responsesSize: 1, dateGreenAnalysis: 1, ecoIndex: 1, grade: 1 }) .sort({ dateGreenAnalysis: 1 }) if (allAnalysis.length === 0) { - stringErr = 'no greenit analysis found for ' + urlNameReq + stringErr = 'Greenit - no greenit analysis found for ' + urlNameReq console.log(stringErr) } } @@ -123,7 +139,7 @@ const GreenItRepository = function () { const lastAnalysis = deployments.filter((greenitAnalysis) => greenitAnalysis.dateGreenAnalysis.getTime() === dateLastAnalysis.getTime()) results = { deployments, lastAnalysis } } else { - console.log('no greenit analysis found for ' + projectNameReq) + console.log('Greenit - no greenit analysis found for ' + projectNameReq) results = { deployments: [], lastAnalysis: null } } } @@ -142,6 +158,7 @@ const GreenItRepository = function () { } }) } + /** * find EcoIndex from last analysis for one Project * @param {project Name} projectNameReq @@ -165,13 +182,12 @@ const GreenItRepository = function () { i++ } analysis = await greenits.find({ idUrlGreen: listIdKey }, { ecoIndex: 1, dateGreenAnalysis: 1 }).sort({ dateGreenAnalysis: 1 }) - if (analysis.length !== 0) { const dateLastAnalysis = analysis[analysis.length - 1].dateGreenAnalysis const lastAnalysis = analysis.filter((greenitAnalysis) => greenitAnalysis.dateGreenAnalysis.getTime() === dateLastAnalysis.getTime()) result = { scores: lastAnalysis } } else { - console.log('no greenit analysis found for ' + projectNameReq) + console.log('Greenit - no greenit analysis found for ' + projectNameReq) result = { scores: null } } } diff --git a/EcoSonar-API/dataBase/lighthouseRepository.js b/EcoSonar-API/dataBase/lighthouseRepository.js index fbe1fe0..9961776 100644 --- a/EcoSonar-API/dataBase/lighthouseRepository.js +++ b/EcoSonar-API/dataBase/lighthouseRepository.js @@ -32,6 +32,22 @@ const LighthouseRepository = function () { }) } + /** + * find all Lighthouse analysis + * @returns + */ + this.findAllAnalysis = async function () { + return new Promise((resolve, reject) => { + lighthouses.find({}) + .then((res) => { + resolve(res) + }) + .catch(() => { + reject(new SystemError()) + }) + }) + } + /** * find analysis for one url in a project * @param {project Name} projectNameReq @@ -73,7 +89,7 @@ const LighthouseRepository = function () { ) .sort({ dateLighthouseAnalysis: 1 }) if (allAnalysis.length === 0) { - stringErr = 'no lighthouse analysis found for ' + urlNameReq + stringErr = 'Lighthouse - No lighthouse analysis found for ' + urlNameReq console.log(stringErr) } } @@ -222,13 +238,10 @@ const LighthouseRepository = function () { stringErr = 'url or project :' + projectNameReq + ' not found' } else { // create a list of idKey - let i = 0 const listIdKey = [] - while (i < resList.length) { + for (let i = 0; i < resList.length; i++) { listIdKey[i] = resList[i].idKey - i++ } - deployments = await lighthouses .find( { idUrlLighthouse: listIdKey }, @@ -239,10 +252,8 @@ const LighthouseRepository = function () { } ) .sort({ dateLighthouseAnalysis: 1 }) - if (deployments.length !== 0) { - const dateLastAnalysis = - deployments[deployments.length - 1].dateLighthouseAnalysis + const dateLastAnalysis = deployments[deployments.length - 1].dateLighthouseAnalysis const lastDeployment = deployments.filter( (deployment) => deployment.dateLighthouseAnalysis.getTime() === @@ -252,7 +263,7 @@ const LighthouseRepository = function () { scores: lastDeployment } } else { - console.log('no lighthouse analysis found for ' + projectNameReq) + console.log('Lighthouse - No lighthouse analysis found for ' + projectNameReq) result = { scores: null } } } diff --git a/EcoSonar-API/dataBase/projectsRepository.js b/EcoSonar-API/dataBase/projectsRepository.js index ff4fa23..951ae5f 100644 --- a/EcoSonar-API/dataBase/projectsRepository.js +++ b/EcoSonar-API/dataBase/projectsRepository.js @@ -1,7 +1,28 @@ const projects = require('./models/projects') const SystemError = require('../utils/SystemError') +const urlsProject = require('./models/urlsprojects') const ProjectsRepository = function () { + /** + * get all projects in database + * @returns an array with the projectName for all projects founded + */ + this.findAllProjectsNames = async function (filterName) { + let query = {} + if (filterName !== null) { + query = { projectName: { $regex: new RegExp(filterName, 'i') } } + } + return new Promise((resolve, reject) => { + urlsProject.find(query) + .then((res) => { + resolve(res) + }) + .catch(() => { + reject(new SystemError()) + }) + }) + } + /** * add a new procedure for a project * @param {projectName} : the name of the project @@ -79,7 +100,7 @@ const ProjectsRepository = function () { this.updateLoginConfiguration = async function (projectName, procedure, loginCredentials, proxy) { const loginMap = new Map(Object.entries(loginCredentials)) return new Promise((resolve, reject) => { - projects.updateOne({ projectName }, { login: loginMap, proxy }) + projects.updateOne({ projectName }, { login: loginMap, proxy, procedure }) .then(() => { resolve() }) .catch((error) => { console.error('PROJECTS REPOSITORY - login update failed') diff --git a/EcoSonar-API/dataBase/w3cRepository.js b/EcoSonar-API/dataBase/w3cRepository.js index 750b416..cb31c21 100644 --- a/EcoSonar-API/dataBase/w3cRepository.js +++ b/EcoSonar-API/dataBase/w3cRepository.js @@ -32,6 +32,23 @@ const W3cRepository = function () { } }) } + + /** + * find all w3c analysis + * @returns + */ + this.findAllAnalysis = async function () { + return new Promise((resolve, reject) => { + w3cs.find({}) + .then((res) => { + resolve(res) + }) + .catch(() => { + reject(new SystemError()) + }) + }) + } + /** * find analysis for one url in a project * @param {project Name} projectNameReq @@ -50,11 +67,11 @@ const W3cRepository = function () { ) if (urlMatching.length === 0) { stringErr = - 'url : ' + - urlNameReq + - ' or project : ' + - projectNameReq + - ' not found' + 'url : ' + + urlNameReq + + ' or project : ' + + projectNameReq + + ' not found' } else { allAnalysis = await w3cs .find( @@ -151,11 +168,11 @@ const W3cRepository = function () { .sort({ dateW3cAnalysis: 1 }) if (deployments.length !== 0) { const dateLastAnalysis = - deployments[deployments.length - 1].dateW3cAnalysis + deployments[deployments.length - 1].dateW3cAnalysis const lastDeployment = deployments.filter( (deployment) => deployment.dateW3cAnalysis.getTime() === - dateLastAnalysis.getTime() + dateLastAnalysis.getTime() ) result = { deployments, diff --git a/EcoSonar-API/package.json b/EcoSonar-API/package.json index cd1f7a4..dbaa9dc 100644 --- a/EcoSonar-API/package.json +++ b/EcoSonar-API/package.json @@ -30,9 +30,11 @@ "js-yaml": "^4.1.0", "lighthouse": "^9.6.8", "mongodb": "^5.0.1", - "mongoose": " ^5.0.0", + "mongoose": " ^7.6.2", "puppeteer": "^18.2.1", "puppeteer-har": "^1.1.2", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0", "uniqid": "^5.4.0" }, "devDependencies": { diff --git a/EcoSonar-API/routes/app.js b/EcoSonar-API/routes/app.js index 4318819..625fe8c 100644 --- a/EcoSonar-API/routes/app.js +++ b/EcoSonar-API/routes/app.js @@ -12,6 +12,9 @@ const userJourneyService = require('../services/userJourneyService') const exportAuditService = require('../services/exportAuditService') const SystemError = require('../utils/SystemError') const asyncMiddleware = require('../utils/AsyncMiddleware') +const swaggerUi = require('swagger-ui-express') +const swaggerSpec = require('../swagger') +const projectService = require('../services/projectService') dotenv.config() @@ -20,6 +23,10 @@ app.disable('x-powered-by') app.use(express.urlencoded({ extended: true })) app.use(express.json()) +app.use('/swagger', swaggerUi.serve, swaggerUi.setup(swaggerSpec)) + +const PORT = process.env.SWAGGER_PORT || 3002 +app.listen(PORT, () => console.log(`Swagger in progress on port ${PORT}`)) const sonarqubeServerUrl = process.env.ECOSONAR_ENV_SONARQUBE_SERVER_URL || '' const whitelist = [sonarqubeServerUrl] @@ -47,7 +54,28 @@ app.use((_req, res, next) => { }) // API CRUD UrlsProject -// retrieve list of URLs saved and audited by EcoSonar for the project +/** + * @swagger + * /api/all: + * get: + * tags: + * - "URL Configuration" + * summary: "Get All URLs from Project" + * description: retrieve list of URLs saved and audited by EcoSonar for the project. + * parameters: + * - name: projectName + * in: query + * description: The name of the project + * required: true + * type: string + * responses: + * 200: + * description: Success. + * 400: + * description: No url found for the project. + * 500: + * description: System error. + */ app.get('/api/all', asyncMiddleware(async (req, res, _next) => { const projectName = req.query.projectName console.log('GET URLS PROJECT - retrieve all urls from project ' + projectName) @@ -65,7 +93,40 @@ app.get('/api/all', asyncMiddleware(async (req, res, _next) => { }) })) -// add list of URLs to be audited once project is audited by EcoSonar +/** + * @swagger + * /api/insert: + * post: + * tags: + * - "URL Configuration" + * summary: "Insert URL into Project" + * description: add list of URLs to be audited once project is audited by EcoSonar. + * parameters: + * - name: urls + * in: body + * description: urls to be inserted in the project + * required: true + * schema: + * type: object + * properties: + * urls: + * type: + * projectName: + * type: string + * urlName: + * type: array + * items: string + * example: + * projectName: "" + * urlName: ["url"] + * responses: + * 200: + * description: Success. + * 400: + * description: Validation failed. + * 500: + * description: System error. + */ app.post('/api/insert', asyncMiddleware(async (req, res, _next) => { const projectName = req.body.projectName const urlsList = req.body.urlName @@ -84,7 +145,34 @@ app.post('/api/insert', asyncMiddleware(async (req, res, _next) => { }) })) -// Delete url to be audited in a project configuration +/** + * @swagger + * /api/delete: + * delete: + * tags: + * - "URL Configuration" + * summary: "Delete URL from Project" + * description: Delete url to be audited in a project configuration. + * parameters: + * - name: urlDeleted + * in: body + * description: The url to be deleted in the project + * required: true + * schema: + * type: object + * properties: + * projectName: + * type: string + * urlName: + * type: string + * responses: + * 200: + * description: Success. + * 400: + * description: Url not found, could not be deleted. + * 500: + * description: System error. + */ app.delete('/api/delete', asyncMiddleware(async (req, res, _next) => { const projectName = req.body.projectName const urlName = req.body.urlName @@ -104,7 +192,51 @@ app.delete('/api/delete', asyncMiddleware(async (req, res, _next) => { })) // API CRUD LOGIN Credentials -// insert login credentials for a project +/** + * @swagger + * /api/login/insert: + * post: + * tags: + * - "Login Configuration" + * summary: "Save Login and Proxy For Project" + * description: Insert login credentials and proxy configuration for a project. + * parameters: + * - name: projectName + * in: query + * description: The name of the project + * required: true + * type: string + * - name: login and proxy + * in: body + * description: The login credentials and proxy settings + * required: true + * schema: + * type: object + * properties: + * login: + * type: object + * properties: + * authentication_url: + * type: string + * steps: + * type: array + * items: + * type: object + * proxy: + * type: object + * properties: + * ipAddress: + * type: string + * port: + * type: string + * responses: + * 201: + * description: Success. + * 400: + * description: Insertion failed. + * 500: + * description: System error. + */ app.post('/api/login/insert', asyncMiddleware(async (req, res, _next) => { const projectName = req.query.projectName const loginCredentials = req.body.login @@ -124,7 +256,28 @@ app.post('/api/login/insert', asyncMiddleware(async (req, res, _next) => { }) })) -// Find login credentials for a project +/** + * @swagger + * /api/login/find: + * get: + * tags: + * - "Login Configuration" + * summary: "Get Login For Project" + * description: Find login credentials for a project. + * parameters: + * - name: projectName + * in: query + * description: The name of the project + * required: true + * type: string + * responses: + * 200: + * description: Success. + * 400: + * description: No login credentials retrieved. + * 500: + * description: System error. + */ app.get('/api/login/find', asyncMiddleware(async (req, res, _next) => { const projectName = req.query.projectName console.log('FIND LOGIN CREDENTIALS - credentials into project ' + projectName) @@ -142,7 +295,28 @@ app.get('/api/login/find', asyncMiddleware(async (req, res, _next) => { }) })) -// Find proxy configuration for a project +/** + * @swagger + * /api/proxy/find: + * get: + * tags: + * - "Login Configuration" + * summary: "Get Proxy For Project" + * description: Find proxy configuration for a project. + * parameters: + * - name: projectName + * in: query + * description: The name of the project + * required: true + * type: string + * responses: + * 200: + * description: Success. + * 400: + * description: No proxy configuration retrieved. + * 500: + * description: System error. + */ app.get('/api/proxy/find', asyncMiddleware(async (req, res, _next) => { const projectName = req.query.projectName console.log('FIND PROXY CONFIGURATION - credentials into project ' + projectName) @@ -160,7 +334,28 @@ app.get('/api/proxy/find', asyncMiddleware(async (req, res, _next) => { }) })) -// Delete login credentials for a project +/** + * @swagger + * /api/login: + * delete: + * tags: + * - "Login Configuration" + * summary: "Delete Login for Project" + * description: Delete login credentials for a project. + * parameters: + * - name: projectName + * in: query + * description: The name of the project + * required: true + * type: string + * responses: + * 200: + * description: Success. + * 400: + * description: Delete login credentials failed. + * 500: + * description: System error. + */ app.delete('/api/login', asyncMiddleware(async (req, res, _next) => { const projectName = req.query.projectName console.log('DELETE LOGIN CREDENTIALS - delete credentials into project ' + projectName) @@ -178,7 +373,28 @@ app.delete('/api/login', asyncMiddleware(async (req, res, _next) => { }) })) -// Delete proxy configuration for a project +/** + * @swagger + * /api/proxy: + * delete: + * tags: + * - "Login Configuration" + * summary: "Delete Proxy for Project" + * description: Delete proxy configuration for a project. + * parameters: + * - name: projectName + * in: query + * description: The name of the project + * required: true + * type: string + * responses: + * 200: + * description: Success. + * 400: + * description: Delete proxy configuration failed. + * 500: + * description: System error. + */ app.delete('/api/proxy', asyncMiddleware(async (req, res, _next) => { const projectName = req.query.projectName console.log('DELETE PROXY CONFIGURATION - delete credentials into project ' + projectName) @@ -197,7 +413,41 @@ app.delete('/api/proxy', asyncMiddleware(async (req, res, _next) => { })) // API CRUD User Flow -// insert new user flow for a url in a project +/** + * @swagger + * /api/user-flow/insert: + * post: + * tags: + * - "User Flow Configuration" + * summary: "Save User Flow for URL" + * description: Insert new user flow for a url in a project. + * parameters: + * - name: userFlow + * in: body + * description: user flow to be added + * required: true + * schema: + * type: object + * properties: + * projectName: + * type: string + * url: + * type: string + * userFlow: + * type: object + * properties: + * steps: + * type: array + * items: + * type: object + * responses: + * 200: + * description: Success. + * 400: + * description: Insertion failed. + * 500: + * description: System error. + */ app.post('/api/user-flow/insert', asyncMiddleware(async (req, res, _next) => { const url = req.body.url const projectName = req.body.projectName @@ -217,7 +467,33 @@ app.post('/api/user-flow/insert', asyncMiddleware(async (req, res, _next) => { }) })) -// Find user flow for a project +/** + * @swagger + * /api/user-flow/find: + * post: + * tags: + * - "User Flow Configuration" + * summary: "Get User Flow for URL" + * description: Find user flow for a URL. + * parameters: + * - name: userFlow + * in: body + * required: true + * schema: + * type: object + * properties: + * projectName: + * type: string + * url: + * type: string + * responses: + * 200: + * description: Success. + * 400: + * description: No user flow retrieved. + * 500: + * description: System error. + */ app.post('/api/user-flow/find', asyncMiddleware(async (req, res, _next) => { const url = req.body.url const projectName = req.body.projectName @@ -236,7 +512,33 @@ app.post('/api/user-flow/find', asyncMiddleware(async (req, res, _next) => { }) })) -// Delete user flow for a project +/** + * @swagger + * /api/user-flow: + * delete: + * tags: + * - "User Flow Configuration" + * summary: "Delete User Flow for URL" + * description: Delete user flow for a URL + * parameters: + * - name: userFlow + * in: body + * required: true + * schema: + * type: object + * properties: + * projectName: + * type: string + * url: + * type: string + * responses: + * 200: + * description: Success. + * 400: + * description: Delete user flow failed. + * 500: + * description: System error. + */ app.delete('/api/user-flow', asyncMiddleware(async (req, res, _next) => { const url = req.body.url const projectName = req.body.projectName @@ -256,7 +558,29 @@ app.delete('/api/user-flow', asyncMiddleware(async (req, res, _next) => { })) // API CRUD GreenIT X Lighthouse x W3C Validator -// insert an analysis +/** + * @swagger + * /api/greenit/insert: + * post: + * tags: + * - "EcoSonar Analysis" + * summary: "Launch an EcoSonar Analysis" + * description: Launch an EcoSonar analysis (GreenIT-Analysis, Google Lighthouse and W3C Validator only) + * parameters: + * - name: projectName + * in: body + * description: The name of the project + * required: true + * schema: + * type: object + * properties: + * projectName: + * type: string + * responses: + * 202: + * description: Analysis launched + * + */ app.post('/api/greenit/insert', asyncMiddleware(async (req, res, _next) => { const projectName = req.body.projectName console.log('INSERT ANALYSIS - Launch analysis for project ' + projectName) @@ -264,7 +588,34 @@ app.post('/api/greenit/insert', asyncMiddleware(async (req, res, _next) => { res.status(202).send() })) -// get analysis for an url +/** + * @swagger + * /api/greenit/url: + * post: + * tags: + * - "EcoSonar Analysis" + * summary: "Get Analysis Per URL" + * description: Get last EcoSonar analysis for the url + * parameters: + * - name: projectName + * in: body + * description: The name of the project + * required: true + * schema: + * type: object + * properties: + * projectName: + * type: string + * urlName: + * type: string + * responses: + * 200: + * description: Success. + * 400: + * description: Analysis for url could not be retrieved. + * 500: + * description: System error. + */ app.post('/api/greenit/url', asyncMiddleware(async (req, res, _next) => { const projectName = req.body.projectName const urlName = req.body.urlName @@ -283,7 +634,28 @@ app.post('/api/greenit/url', asyncMiddleware(async (req, res, _next) => { }) })) -// get analysis for all urls of a project at one date +/** + * @swagger + * /api/greenit/project: + * get: + * tags: + * - "EcoSonar Analysis" + * summary: "Get Analysis Per Project" + * description: Get last EcoSonar analysis for the project. + * parameters: + * - name: projectName + * in: query + * description: The name of the project + * required: true + * type: string + * responses: + * 200: + * description: Success. + * 400: + * description: Analysis for project could not be retrieved. + * 500: + * description: System error. + */ app.get('/api/greenit/project', asyncMiddleware(async (req, res, _next) => { const projectName = req.query.projectName console.log('GET ANALYSIS PROJECT - retrieve analysis for project ' + projectName) @@ -300,7 +672,28 @@ app.get('/api/greenit/project', asyncMiddleware(async (req, res, _next) => { }) })) -// Retrieve EcoSonar scores (greenit, lighthouse and w3c validator) to be returned to the CICD pipelines +/** + * @swagger + * /api/ecosonar/scores: + * get: + * tags: + * - "EcoSonar Analysis" + * summary: "Get Project Scores for latest analysis" + * description: Retrieve EcoSonar scores (EcoIndex, Google Lighthouse and W3C Validator) to be returned to the CICD pipelines. + * parameters: + * - name: projectName + * in: query + * description: The name of the project + * required: true + * type: string + * responses: + * 200: + * description: Success. + * 400: + * description: Scores for project could not be retrieved. + * 500: + * description: System error. + */ app.get('/api/ecosonar/scores', asyncMiddleware(async (req, res, _next) => { const projectNameReq = req.query.projectName console.log('GET ECOSONAR PROJECT SCORES - retrieve scores for project ' + projectNameReq) @@ -317,8 +710,162 @@ app.get('/api/ecosonar/scores', asyncMiddleware(async (req, res, _next) => { }) })) +/** + * @swagger + * /api/ecosonar/info: + * get: + * tags: + * - "EcoSonar Analysis" + * summary: "Get Average of all scores for projects registered in EcoSonar at a defined date" + * description: Retrieve all EcoSonar projects average for all scores (EcoIndex, Google Lighthouse and W3C Validator). You can retrieve the scores at a date defined or for last analysis made if no date defined. + * parameters: + * - name: date + * in: query + * description: endpoint will return the last analysis before that date, today if none + * required: false + * type: string + * format: date + * example: "2020-12-31" + * responses: + * 200: + * description: Success. + * 400: + * description: Scores for projects could not be retrieved. + * 500: + * description: System error. + */ +app.get('/api/ecosonar/info', asyncMiddleware(async (req, res, _next) => { + const date = req.query.date ?? null + console.log('GET AVERAGE PROJECT SCORE - retrieve all informations for all projects for the date defined') + projectService.getAllInformationsAverage(date) + .then((result) => { + console.log('GET AVERAGE PROJECT SCORES - Average of scores from all projects for the date defined') + return res.status(200).json(result) + }).catch((error) => { + if (error instanceof SystemError) { + return res.status(500).send() + } + console.log('GET AVERAGE PROJECT SCORES - Average of scores from all projects for the date defined could not be retrieved') + return res.status(400).json({ error: error.message }) + }) +})) + +/** + * @swagger + * /api/project/all: + * post: + * tags: + * - "EcoSonar Analysis" + * summary: "Get all projects scores from date defined" + * description: Retrieve all EcoSonar projects and return the scores for each of them at the date defined, if date not filled it would be the latest analysis. + * parameters: + * - name: date + * in: query + * description: endpoint will return the last analysis before that date, today if none + * required: false + * type: string + * format: date + * example: "2020-12-31" + * - name: filterName + * in: query + * description: retrieve projects whose name contains the string 'filterName', case insensitive + * required: false + * type: string + * example: "my-project" + * - name: filterAndSort + * in: body + * description: filter and sorting configuration for projects scores of all projects registered in EcoSonar API. CATEGORY in filterScore can take the following enum ecoIndex, perfScore, accessScore, w3cScore. score is a value from 0 to 100, it will be the threshold for the CATEGORY. select takes the value upper or lower according if you want only project whose scores have an average value higher than score or lower. CATEGORY in sortBy can take the following enum ecoIndex, perfScore, accessScore, w3cScore and name. order can take the value asc or desc if you want to sort your projects according to the type. + * required: false + * schema: + * type: object + * properties: + * filterScore: + * type: object + * properties: + * cat: + * type: string + * example: ecoIndex + * enum: + * - ecoIndex + * - perfScore + * - accessScore + * - w3cScore + * score: + * type: integer + * select: + * type: string + * example: upper + * enum: + * - upper + * - lower + * sortBy: + * type: object + * properties: + * type: + * type: string + * example: ecoIndex + * enum: + * - ecoIndex + * - perfScore + * - accessScore + * - w3cScore + * - name + * order: + * type: string + * enum: + * - asc + * - desc + * example: asc + * responses: + * 200: + * description: Success. + * 400: + * description: Scores for project could not be retrieved. + * 500: + * description: System error. + */ +app.post('/api/project/all', asyncMiddleware(async (req, res, _next) => { + const date = req.query.date ?? null + const filterName = req.query.filterName ?? null + const sortBy = req.body.sortBy ?? null + const filterScore = req.body.filterScore ?? null + console.log('GET PROJECTS SCORES - Retrieve scores for each projects.') + projectService.getAllProjectInformations(date, sortBy, filterName, filterScore) + .then((result) => { + console.log('GET PROJECTS SCORES - Average scores for each projects.') + return res.status(200).json(result) + }).catch((error) => { + if (error instanceof SystemError) { + return res.status(500).send() + } + console.log('GET PROJECT SCORES - Average scores for each each projects could not be retrieved') + return res.status(400).json({ error: error.message }) + }) +})) + // API CRUD BestPractices -// retrieve all best practices for a project +/** + * @swagger + * /api/bestPractices/project: + * get: + * tags: + * - "EcoSonar Analysis" + * summary: "Get Best Practices per Project" + * description: Retrieve all best practices for a project from the last analysis. + * parameters: + * - name: projectName + * in: query + * description: The name of the project + * required: true + * type: string + * responses: + * 200: + * description: Success. + * 400: + * description: Best practices analysis for project could not be retrieved. + * 500: + * description: System error. + */ app.get('/api/bestPractices/project', asyncMiddleware(async (req, res, _next) => { const projectName = req.query.projectName console.log('GET BEST PRACTICES PROJECT - retrieve best practices analysis for project ' + projectName) @@ -338,7 +885,34 @@ app.get('/api/bestPractices/project', asyncMiddleware(async (req, res, _next) => })) // API CRUD BestPractices -// retrieve best practices for an URL +/** + * @swagger + * /api/bestPractices/url: + * post: + * tags: + * - "EcoSonar Analysis" + * summary: "Get Best Practices per URL" + * description: Retrieve best practices for an URL from the last analysis + * parameters: + * - name: projectName + * in: body + * description: The name of the project + * required: true + * schema: + * type: object + * properties: + * projectName: + * type: string + * urlName: + * type: string + * responses: + * 200: + * description: Success. + * 400: + * description: Best practices analysis for url could not be retrieved. + * 500: + * description: System error. + */ app.post('/api/bestPractices/url', asyncMiddleware(async (req, res, _next) => { const projectName = req.body.projectName const urlName = req.body.urlName @@ -352,13 +926,38 @@ app.post('/api/bestPractices/url', asyncMiddleware(async (req, res, _next) => { if (error instanceof SystemError) { return res.status(500).send() } - console.log('GET BEST PRACTICES URL - Best practices analysis for project could not be retrieved') + console.log('GET BEST PRACTICES URL - Best practices analysis for url could not be retrieved') return res.status(400).json({ error: error.message }) }) })) // Crawler service -// Crawl across the given website to find URLs +/** + * @swagger + * /api/crawl: + * post: + * tags: + * - "URL Configuration" + * summary: "Get Crawler Result" + * description: Crawl the given website to find all pages related. + * parameters: + * - name: crawledUrl + * in: body + * description: crawling the website + * required: true + * schema: + * type: object + * properties: + * projectName: + * type: string + * mainUrl: + * type: string + * responses: + * 200: + * description: Success. + * 500: + * description: System error. + */ app.post('/api/crawl', asyncMiddleware(async (req, res, _next) => { const projectName = req.body.projectName const mainUrl = req.body.mainUrl @@ -369,13 +968,40 @@ app.post('/api/crawl', asyncMiddleware(async (req, res, _next) => { return res.status(200).json(results) }) .catch(() => { - console.log('CRAWLER - Crawler has encountered and error') + console.log('CRAWLER - Crawler has encountered an error') return res.status(500).send() }) })) // API PROCEDURE -// POST method is used to update the procedure of a project +/** + * @swagger + * /api/procedure: + * post: + * tags: + * - "Procedure Configuration" + * summary: "Add Procedure for project" + * description: Update the procedure of a project, procedure can take the following values quickWins, highestImpact, scoreImpact. Procedure is the sorting method for best practices analysis. + * parameters: + * - name: projectName + * in: body + * description: The name of the project + * required: true + * schema: + * type: object + * properties: + * projectName: + * type: string + * selectedProcedure: + * type: string + * responses: + * 200: + * description: Success. + * 400: + * description: Procedure could not be updated. + * 500: + * description: System Error. + */ app.post('/api/procedure', asyncMiddleware(async (req, res, _next) => { const projectName = req.body.projectName const selectedProcedure = req.body.selectedProcedure @@ -394,7 +1020,28 @@ app.post('/api/procedure', asyncMiddleware(async (req, res, _next) => { }) })) -// get method is used to get the procedure of the project +/** + * @swagger + * /api/procedure: + * get: + * tags: + * - "Procedure Configuration" + * summary: "Get Procedure for project" + * description: Retrieve the sorting method used for best practices analysis. + * parameters: + * - name: projectName + * in: query + * description: The name of the project + * required: true + * type: string + * responses: + * 200: + * description: Success. + * 400: + * description: Procedure could not be retrieved. + * 500: + * description: System Error. + */ app.get('/api/procedure', asyncMiddleware(async (req, res, _next) => { const projectName = req.query.projectName procedureService.getProcedure(projectName) @@ -412,7 +1059,6 @@ app.get('/api/procedure', asyncMiddleware(async (req, res, _next) => { }) })) -// export excel app.post('/api/export', asyncMiddleware(async (req, res, _next) => { const projectName = req.body.projectName console.log(`POST EXCEL - audit for project ${projectName} to be retrieved`) diff --git a/EcoSonar-API/services/exportAuditService.js b/EcoSonar-API/services/exportAuditService.js index e8693a4..5185e78 100644 --- a/EcoSonar-API/services/exportAuditService.js +++ b/EcoSonar-API/services/exportAuditService.js @@ -164,7 +164,7 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni row.getCell(2).alignment = { wrapText: true } row.getCell(3).alignment = { wrapText: true } - let i = 3 + let rowIndex = 3 if (urlName !== '') { row = sheet.getRow(4) row.getCell(1).value = 'Audited Page:' @@ -191,10 +191,10 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni } row.getCell(1).alignment = { wrapText: true } row.getCell(2).alignment = { wrapText: true } - i = 6 + rowIndex = 6 } // score and grade - row = sheet.getRow(i) + row = sheet.getRow(rowIndex) row.getCell(1).value = 'EcoIndex project grade (average)' if (analysisGreenit !== null) { row.getCell(2).value = analysisGreenit.grade @@ -266,9 +266,9 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni row.getCell(6).alignment = { wrapText: true } row.getCell(7).alignment = { wrapText: true } row.getCell(8).alignment = { wrapText: true } - i++ + rowIndex++ - row = sheet.getRow(i) + row = sheet.getRow(rowIndex) row.getCell(1).value = 'EcoIndex project score (average)' if (analysisGreenit !== null) { row.getCell(2).value = analysisGreenit.ecoIndex @@ -340,11 +340,11 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni row.getCell(6).alignment = { wrapText: true } row.getCell(7).alignment = { wrapText: true } row.getCell(8).alignment = { wrapText: true } - i++ + rowIndex++ // greenit Analysis metrics - i++ - row = sheet.getRow(i) + rowIndex++ + row = sheet.getRow(rowIndex) row.getCell(1).value = 'GreenIT-Analysis Metrics:' row.getCell(1).font = { bold: true, @@ -352,7 +352,7 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni } // formatting row.getCell(1).alignment = { wrapText: true } - i++ + rowIndex++ // draw border of the cell row.getCell(1).border = { top: { style: 'thick' }, @@ -365,7 +365,7 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni right: { style: 'thick' }, top: { style: 'thick' } } - row = sheet.getRow(i) + row = sheet.getRow(rowIndex) if (analysisGreenit !== null) { row.getCell(1).value = 'Size of the DOM (average)' row.getCell(2).value = analysisGreenit.domSize.displayValue @@ -375,7 +375,7 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni row.getCell(1).alignment = { wrapText: true } row.getCell(2).alignment = { wrapText: true } row.getCell(3).alignment = { wrapText: true } - i++ + rowIndex++ // draw border of the cell row.getCell(1).border = { left: { style: 'thick' } @@ -383,7 +383,7 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni row.getCell(3).border = { right: { style: 'thick' } } - row = sheet.getRow(i) + row = sheet.getRow(rowIndex) row.getCell(1).value = 'Number of requests (average)' row.getCell(2).value = analysisGreenit.nbRequest.displayValue row.getCell(3).value = analysisGreenit.nbRequest.complianceLevel @@ -399,9 +399,9 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni row.getCell(1).alignment = { wrapText: true } row.getCell(2).alignment = { wrapText: true } row.getCell(3).alignment = { wrapText: true } - i++ + rowIndex++ - row = sheet.getRow(i) + row = sheet.getRow(rowIndex) row.getCell(1).value = 'Size of the page (Kb) (average)' row.getCell(2).value = analysisGreenit.responsesSize.displayValue row.getCell(3).value = analysisGreenit.responsesSize.complianceLevel @@ -422,7 +422,7 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni row.getCell(1).alignment = { wrapText: true } row.getCell(2).alignment = { wrapText: true } row.getCell(3).alignment = { wrapText: true } - i++ + rowIndex++ } else { row.getCell(1).value = 'no analysis' setColor(row.getCell(1), '') @@ -441,12 +441,12 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni right: { style: 'thick' }, bottom: { style: 'thick' } } - i++ + rowIndex++ } // Lighthouse performance metrics - i++ - row = sheet.getRow(i) + rowIndex++ + row = sheet.getRow(rowIndex) row.getCell(1).value = 'Lighthouse Performance Metrics:' row.getCell(1).font = { bold: true, @@ -454,7 +454,7 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni } // formatting row.getCell(1).alignment = { wrapText: true } - i++ + rowIndex++ // draw border of the cell row.getCell(1).border = { top: { style: 'thick' }, @@ -471,7 +471,7 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni top: { style: 'thick' } } - row = sheet.getRow(i) + row = sheet.getRow(rowIndex) if (lighthouseMetrics !== null) { row.getCell(1).value = 'Largest Contentful Paint (s) (average)' row.getCell(2).value = lighthouseMetrics.largestContentfulPaint.displayValue @@ -491,9 +491,9 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni row.getCell(2).alignment = { wrapText: true } row.getCell(3).alignment = { wrapText: true } row.getCell(4).alignment = { wrapText: true } - i++ + rowIndex++ - row = sheet.getRow(i) + row = sheet.getRow(rowIndex) row.getCell(1).value = 'Cumulative Layout Shift (average)' row.getCell(2).value = lighthouseMetrics.cumulativeLayoutShift.displayValue row.getCell(3).value = lighthouseMetrics.cumulativeLayoutShift.complianceLevel @@ -512,9 +512,9 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni row.getCell(2).alignment = { wrapText: true } row.getCell(3).alignment = { wrapText: true } row.getCell(4).alignment = { wrapText: true } - i++ + rowIndex++ - row = sheet.getRow(i) + row = sheet.getRow(rowIndex) row.getCell(1).value = 'First Contentful Paint (s) (average)' row.getCell(2).value = lighthouseMetrics.firstContentfulPaint.displayValue row.getCell(3).value = lighthouseMetrics.firstContentfulPaint.complianceLevel @@ -533,9 +533,9 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni row.getCell(2).alignment = { wrapText: true } row.getCell(3).alignment = { wrapText: true } row.getCell(4).alignment = { wrapText: true } - i++ + rowIndex++ - row = sheet.getRow(i) + row = sheet.getRow(rowIndex) row.getCell(1).value = 'Speed Index (s) (average)' row.getCell(2).value = lighthouseMetrics.speedIndex.displayValue row.getCell(3).value = lighthouseMetrics.speedIndex.complianceLevel @@ -554,9 +554,9 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni row.getCell(2).alignment = { wrapText: true } row.getCell(3).alignment = { wrapText: true } row.getCell(4).alignment = { wrapText: true } - i++ + rowIndex++ - row = sheet.getRow(i) + row = sheet.getRow(rowIndex) row.getCell(1).value = 'Total Blocking Time (ms) (average)' row.getCell(2).value = lighthouseMetrics.totalBlockingTime.displayValue row.getCell(3).value = lighthouseMetrics.totalBlockingTime.complianceLevel @@ -575,9 +575,9 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni row.getCell(2).alignment = { wrapText: true } row.getCell(3).alignment = { wrapText: true } row.getCell(4).alignment = { wrapText: true } - i++ + rowIndex++ - row = sheet.getRow(i) + row = sheet.getRow(rowIndex) row.getCell(1).value = 'Time to interactive (s) (average)' row.getCell(2).value = lighthouseMetrics.interactive.displayValue row.getCell(3).value = lighthouseMetrics.interactive.complianceLevel @@ -604,7 +604,7 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni row.getCell(2).alignment = { wrapText: true } row.getCell(3).alignment = { wrapText: true } row.getCell(4).alignment = { wrapText: true } - i++ + rowIndex++ } else { row.getCell(1).value = 'no analysis' setColor(row.getCell(1), '') @@ -623,12 +623,12 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni right: { style: 'thick' }, bottom: { style: 'thick' } } - i++ + rowIndex++ } // W3c validator - i++ - row = sheet.getRow(i) + rowIndex++ + row = sheet.getRow(rowIndex) row.getCell(1).value = 'W3C Validator Metrics:' row.getCell(1).font = { bold: true, @@ -636,7 +636,7 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni } // formatting row.getCell(1).alignment = { wrapText: true } - i++ + rowIndex++ // draw border of the cell row.getCell(1).border = { top: { style: 'thick' }, @@ -647,14 +647,14 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni right: { style: 'thick' } } - row = sheet.getRow(i) + row = sheet.getRow(rowIndex) if (analysisW3c !== null) { row.getCell(1).value = 'Number of Infos' row.getCell(2).value = analysisW3c.totalInfo // formatting row.getCell(1).alignment = { wrapText: true } row.getCell(2).alignment = { wrapText: true } - i++ + rowIndex++ // draw border of the cell row.getCell(1).border = { left: { style: 'thick' } @@ -662,13 +662,13 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni row.getCell(2).border = { right: { style: 'thick' } } - row = sheet.getRow(i) + row = sheet.getRow(rowIndex) row.getCell(1).value = 'Number of Warnings' row.getCell(2).value = analysisW3c.totalWarning // formatting row.getCell(1).alignment = { wrapText: true } row.getCell(2).alignment = { wrapText: true } - i++ + rowIndex++ // draw border of the cell row.getCell(1).border = { left: { style: 'thick' } @@ -676,13 +676,13 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni row.getCell(2).border = { right: { style: 'thick' } } - row = sheet.getRow(i) + row = sheet.getRow(rowIndex) row.getCell(1).value = 'Number of Errors' row.getCell(2).value = analysisW3c.totalError // formatting row.getCell(1).alignment = { wrapText: true } row.getCell(2).alignment = { wrapText: true } - i++ + rowIndex++ // draw border of the cell row.getCell(1).border = { left: { style: 'thick' } @@ -690,7 +690,7 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni row.getCell(2).border = { right: { style: 'thick' } } - row = sheet.getRow(i) + row = sheet.getRow(rowIndex) row.getCell(1).value = 'Number of Fatal Errors' row.getCell(2).value = analysisW3c.totalFatalError // draw border of the cell @@ -718,7 +718,6 @@ function formatExcelSheet (urlName, index, workbook, projectName, analysisGreeni right: { style: 'thick' }, bottom: { style: 'thick' } } - i++ } // Adapting each column's size to view all text diff --git a/EcoSonar-API/services/projectService.js b/EcoSonar-API/services/projectService.js new file mode 100644 index 0000000..afee04a --- /dev/null +++ b/EcoSonar-API/services/projectService.js @@ -0,0 +1,262 @@ +const SystemError = require('../utils/SystemError') +const retrieveAnalysisService = require('../services/retrieveAnalysisService') +const scores = ['ecoIndex', 'perfScore', 'accessScore', 'w3cScore'] + +class ProjectService { +} + +/** + * get an average of all score for all projects of the database of last analysis + * @param {date} Date limit date of analysis + * @returns nbr of projects and an average for each score on the database at each date + */ +ProjectService.prototype.getAllInformationsAverage = async function (date) { + return new Promise((resolve, reject) => { + this.getAllProjectInformations(date, null, null, null) + .then((result) => { + const resultformatted = { nbProjects: result.nbProjects, ecoIndex: null, perfScore: null, accessScore: null, w3cScore: null } + const scoreNbProject = { ecoIndex: null, perfScore: null, accessScore: null, w3cScore: null } + Object.keys(result.projects).forEach(project => { + for (const scoreType of scores) { + if (resultformatted[scoreType] !== null) { + if (result.projects[project][scoreType] !== null) { + resultformatted[scoreType] += result.projects[project][scoreType] + scoreNbProject[scoreType] += 1 + } + } else { + if (result.projects[project][scoreType] !== null) { + resultformatted[scoreType] = result.projects[project][scoreType] + scoreNbProject[scoreType] = 1 + } + } + } + }) + scores.forEach(score => { + resultformatted[score] = Math.round(resultformatted[score] / scoreNbProject[score]) + }) + resolve(resultformatted) + }).catch((err) => { + reject(new Error(err)) + }) + }) +} + +function selectRightAnalysisByDateAndUrl (searchDate, projectsAnalysis, urlFieldName) { + const allAnalysisPerUrl = [] + const groupedAnalysisByIdKeys = projectsAnalysis.reduce((acc, obj) => { + if (!acc[obj[urlFieldName]]) { + acc[obj[urlFieldName]] = [] + } + acc[obj[urlFieldName]].push(obj) + return acc + }, {}) + Object.keys(groupedAnalysisByIdKeys).forEach(UrlAnalysisId => { + const retainedAnalysis = filterPerDate(searchDate, groupedAnalysisByIdKeys[UrlAnalysisId]) + allAnalysisPerUrl[UrlAnalysisId] = retainedAnalysis + }) + return allAnalysisPerUrl +} + +function filterPerDate (searchDate, projectsAnalysis) { + const allDates = Object.keys(projectsAnalysis) + const allDatesInRange = searchDate === null ? allDates : allDates.filter(date => new Date(date) <= searchDate) + + if (allDatesInRange.length === 0) return null + + const selectedDate = allDatesInRange.reduce((a, b) => new Date(a) > new Date(b) ? a : b) + return projectsAnalysis[selectedDate] +} + +function filterByScore (projectsList, filterCategory, filterLevel, filterDirection) { + const projectListFiltered = {} + Object.keys(projectsList.projects).forEach(projectName => { + if ((filterDirection === 'upper' && projectsList.projects[projectName][filterCategory] >= filterLevel) || (filterDirection === 'lower' && projectsList.projects[projectName][filterCategory] <= filterLevel)) { + projectListFiltered[projectName] = projectsList.projects[projectName] + } + }) + projectsList.projects = projectListFiltered + return projectsList +} + +function sortProjects (resultList, sortParams) { + if (!sortParams) return resultList + + const { type, order } = sortParams + let sortedProjects = {} + + if (type === 'name') { + sortedProjects = sortByName(resultList.projects, order) + } else if (scores.includes(type)) { + sortedProjects = sortByScore(resultList.projects, type, order) + } else { + console.error('sort type does not exist') + return resultList + } + + resultList.projects = sortedProjects + return resultList +} + +function sortByName (projects, order) { + const sortedKeys = Object.keys(projects).sort(Intl.Collator().compare) + if (order === 'desc') sortedKeys.reverse() + + return sortedKeys.reduce((result, key) => { + result[key] = projects[key] + return result + }, {}) +} + +function sortByScore (projects, type, order) { + const arr = Object.entries(projects) + arr.sort((a, b) => b[1][type] - a[1][type]) + if (order === 'desc') arr.reverse() + + return Object.fromEntries(arr) +} + +function validateAndConvertDate (dateToValidate) { + return new Promise((resolve, reject) => { + if (dateToValidate !== null) { + const regex = /^\d{4}-\d{2}-\d{2}$/ + if (regex.test(dateToValidate)) { + resolve(new Date(dateToValidate)) + } else { + reject(new Error('Bad date format: YYYY-MM-DD')) + } + } else { + resolve(null) + } + }) +} + +function getFieldScore (analysis, fieldToSum) { + if (!analysis) { + return null + } + const countUrls = Object.keys(analysis).length + if (countUrls === 0) { + return null + } + let sumScore = 0 + for (const url in analysis) { + sumScore += analysis[url][fieldToSum] + } + return Math.round(sumScore / countUrls) +} + +function getFieldScore2 (analysis, fieldToSum1, fieldToSum2) { + if (!analysis) { + return null + } + const countUrls = Object.keys(analysis).length + if (countUrls === 0) { + return null + } + let sumScore = 0 + for (const url in analysis) { + sumScore += analysis[url][fieldToSum1][fieldToSum2] + } + return Math.round(sumScore / countUrls) +} + +function getDateAnalysis (lighthouseAnalysis, greenItAnalysis, w3cAnalysis) { + if (!lighthouseAnalysis && !greenItAnalysis && !w3cAnalysis) { + return null + } + try { + return greenItAnalysis[Object.keys(greenItAnalysis)[0]].dateGreenAnalysis + } catch (e) { + // Ignored + } + try { + return lighthouseAnalysis[Object.keys(lighthouseAnalysis)[0]].dateLighthouseAnalysis + } catch (e) { + // Ignored + } + try { + return w3cAnalysis[Object.keys(w3cAnalysis)[0]].dateW3cAnalysis + } catch (e) { + // Ignored + } + return null +} + +/** + * get informations about all project of the database on last analysis + * @param {date} date limit date of analysis + * @param {sortBy} object content to sort projects: {"type": "", "order": "" } + * @param {filterName} string filter on a string + * @param {filterScore} object filter content on a score: {"cat": "", "score": , "select": "" } + * @returns all the informations for each project on the database at each date + */ +ProjectService.prototype.getAllProjectInformations = async function (date, sortBy, filterName, filterScore) { + let error = null + try { + date = await validateAndConvertDate(date) + } catch (err) { + error = err + } + let AnalysisPerProject = {} + if (error === null) { + try { + AnalysisPerProject = await retrieveAnalysisService.getProjectScoresAverageAll(filterName, date) + } catch (err) { + error = err + } + if (error === null) { + return new Promise((resolve, reject) => { + let resultwithfilter = { nbProjects: null, projects: {} } + try { + for (const analysis of AnalysisPerProject) { + if (analysis.lighthouse.length > 0 || analysis.greenIt.length > 0 || analysis.w3c > 0) { + const lighthouseAnalysis = selectRightAnalysisByDateAndUrl(date, analysis.lighthouse, 'idUrlLighthouse') + const greenItAnalysis = selectRightAnalysisByDateAndUrl(date, analysis.greenIt, 'idUrlGreen') + const w3cAnalysis = selectRightAnalysisByDateAndUrl(date, analysis.w3c, 'idUrlW3c') + + let projectInfos = {} + + if (greenItAnalysis !== null || lighthouseAnalysis !== null || w3cAnalysis !== null) { + let allUrls = [...(greenItAnalysis !== null ? Object.keys(greenItAnalysis) : []), ...(lighthouseAnalysis !== null ? Object.keys(lighthouseAnalysis) : []), ...(w3cAnalysis !== null ? Object.keys(w3cAnalysis) : [])] + allUrls = Array.from(new Set(allUrls.map(JSON.stringify))) + + projectInfos = { + ecoIndex: getFieldScore(greenItAnalysis, 'ecoIndex'), + perfScore: getFieldScore2(lighthouseAnalysis, 'performance', 'score'), + accessScore: getFieldScore2(lighthouseAnalysis, 'accessibility', 'score'), + w3cScore: getFieldScore(w3cAnalysis, 'score'), + nbUrl: allUrls.length, + dateAnalysis: getDateAnalysis(lighthouseAnalysis, greenItAnalysis, w3cAnalysis) + } + resultwithfilter.projects[analysis.name] = projectInfos + } + } + } + } catch (err) { + console.error(err) + reject(new SystemError(err)) + } + if (error !== null) { + reject(new SystemError()) + } else { + if (filterScore !== null) { + resultwithfilter = filterByScore(resultwithfilter, filterScore.cat, filterScore.score, filterScore.select) + } + resultwithfilter.nbProjects = Object.keys(resultwithfilter.projects).length + resolve(sortProjects(resultwithfilter, sortBy)) + } + }) + } else { + return new Promise((_resolve, reject) => { + reject(new Error(error)) + }) + } + } else { + return new Promise((_resolve, reject) => { + reject(new Error(error)) + }) + } +} + +const projectService = new ProjectService() +module.exports = projectService diff --git a/EcoSonar-API/services/retrieveAnalysisService.js b/EcoSonar-API/services/retrieveAnalysisService.js index f443cae..3db4bba 100644 --- a/EcoSonar-API/services/retrieveAnalysisService.js +++ b/EcoSonar-API/services/retrieveAnalysisService.js @@ -1,12 +1,13 @@ const greenItRepository = require('../dataBase/greenItRepository') const lighthouseRepository = require('../dataBase/lighthouseRepository') const w3cRepository = require('../dataBase/w3cRepository') +const projectsRepository = require('../dataBase/projectsRepository') const formatLighthouseAnalysis = require('./format/formatLighthouseAnalysis') const SystemError = require('../utils/SystemError') const formatGreenItAnalysis = require('./format/formatGreenItAnalysis') const formatW3cAnalysis = require('./format/formatW3cAnalysis') -class RetrieveAnalysisService {} +class RetrieveAnalysisService { } /** * Get an analysis (GreenIt & Lighthouse) from a given project and URL @@ -180,10 +181,64 @@ RetrieveAnalysisService.prototype.getProjectAnalysis = async function (projectNa }) } +function groupByProject (idKeys, fieldName, allAnalysis) { + return allAnalysis.filter(obj => idKeys.includes(obj[fieldName])) +} + +function regroupUrlIdKeyByProjectName (projects) { + return projects.reduce((acc, obj) => { + const existingObj = acc.find(o => o.projectName === obj.projectName) + if (existingObj) { + existingObj.IdKeys.push(obj.idKey) + } else { + acc.push({ projectName: obj.projectName, IdKeys: [obj.idKey] }) + } + return acc + }, []) +} + +/** + * Get the average of scores (EcoIndex & Performance & Accessibility) from all projects at a range of date + * @param {string} filterName by default equal to null, if stirng is not null allow to filter project by their name + * @returns {Object} Returns the formatted average scores + */ +RetrieveAnalysisService.prototype.getProjectScoresAverageAll = async function (filterName = null) { + const result = [] + let error = null + try { + const allLighthouseAnalysis = await lighthouseRepository.findAllAnalysis() + const allgreenITAnalysis = await greenItRepository.findAllAnalysis() + const allW3CAnalysis = await w3cRepository.findAllAnalysis() + + let projects = await projectsRepository.findAllProjectsNames(filterName) + projects = regroupUrlIdKeyByProjectName(projects) + for (const project of projects) { + const analysisOfProjectLighthouse = groupByProject(project.IdKeys, 'idUrlLighthouse', allLighthouseAnalysis) + const analysisOfProjectGreenIt = groupByProject(project.IdKeys, 'idUrlGreen', allgreenITAnalysis) + const analysisOfProjectW3C = groupByProject(project.IdKeys, 'idUrlW3c', allW3CAnalysis) + result.push({ + name: project.projectName, + lighthouse: analysisOfProjectLighthouse, + greenIt: analysisOfProjectGreenIt, + w3c: analysisOfProjectW3C + }) + } + } catch (err) { + error = err + } + return new Promise((resolve, reject) => { + if (error !== null) { + reject(new SystemError()) + } else { + resolve(result) + } + }) +} + /** * Get the EcoSonar scores (GreenIt & Lighthouse & W3C Validator) from a given project * @param {string} projectName - * @returns {Object} Returns the formatted scores for the given project + * @returns {Object} Returns the formatted scores for the given project */ RetrieveAnalysisService.prototype.getProjectScores = async function (projectName) { let ecoIndex = 0 diff --git a/EcoSonar-API/services/userJourneyService.js b/EcoSonar-API/services/userJourneyService.js index 55ace35..bb3b68f 100644 --- a/EcoSonar-API/services/userJourneyService.js +++ b/EcoSonar-API/services/userJourneyService.js @@ -141,11 +141,11 @@ UserJourneyService.prototype.deleteUserFlow = async function (projectName, url) UserJourneyService.prototype.scrollUntilPercentage = async function (page, distancePercentage) { console.log('AUTOSCROLL - autoscroll has started') - await page.evaluate(async (distancePercentage) => { + await page.evaluate(async (percentage) => { await new Promise((resolve, _reject) => { let totalHeight = 0 const distance = 100 - const scrollHeight = document.body.scrollHeight * distancePercentage / 100 + const scrollHeight = document.body.scrollHeight * percentage / 100 const timer = setInterval(() => { window.scrollBy(0, distance) totalHeight += distance diff --git a/EcoSonar-API/swagger.js b/EcoSonar-API/swagger.js new file mode 100644 index 0000000..6cb3a18 --- /dev/null +++ b/EcoSonar-API/swagger.js @@ -0,0 +1,16 @@ +const swaggerJSDoc = require('swagger-jsdoc') + +const swagger = { + swaggerDefinition: { + info: { + title: 'API EcoSonar', + version: '3.2', + description: 'Swagger UI of EcoSonar API' + } + }, + apis: ['routes/*.js'] +} + +const swaggerSpec = swaggerJSDoc(swagger) + +module.exports = swaggerSpec diff --git a/EcoSonar-SonarQube/README.md b/EcoSonar-SonarQube/README.md index 3cccf63..609d82a 100644 --- a/EcoSonar-SonarQube/README.md +++ b/EcoSonar-SonarQube/README.md @@ -50,12 +50,12 @@ mvn clean package -Durl=#EcoSonar-API-URL EcoSonar-API-URL should be replaced in local by `http://localhost:3000` and by the EcoSonar API URL for a deployed version. -### Install Sonarqube Plugins +### Install Sonarqube Plugins Manually 1. Copy the file located at the following path `target/ecosonar-X-SNAPSHOT.jar`. 2. Go to your Sonarqube folder `extensions/plugins/` and paste the JAR. -3. Retrieve 3 JAR files available in the `ecocode` folder : -4. Go to your Sonarqube folder `extensions/plugins/` and paste the 3 JAR files to add the EcoCode Sonarqube plugins. +3. Retrieve all JAR files available in the `ecocode` folder (there should be 6, one by language): +4. Go to your Sonarqube folder `extensions/plugins/` and paste the JAR files to add the EcoCode Sonarqube plugins. To finally launch Sonarqube with the plugin, run the shell script: `bin/windows-x86-64/StartSonar.bat`. diff --git a/EcoSonar-SonarQube/ecocode/ecocode-android-1.0.1.jar b/EcoSonar-SonarQube/ecocode/ecocode-android-1.0.1.jar new file mode 100644 index 0000000..5fb6c73 Binary files /dev/null and b/EcoSonar-SonarQube/ecocode/ecocode-android-1.0.1.jar differ diff --git a/EcoSonar-SonarQube/ecocode/ecocode-ios-1.1.0.jar b/EcoSonar-SonarQube/ecocode/ecocode-ios-1.1.0.jar new file mode 100644 index 0000000..608fba6 Binary files /dev/null and b/EcoSonar-SonarQube/ecocode/ecocode-ios-1.1.0.jar differ diff --git a/EcoSonar-SonarQube/ecocode/ecocode-java-plugin-1.2.1.jar b/EcoSonar-SonarQube/ecocode/ecocode-java-plugin-1.2.1.jar deleted file mode 100644 index a3ff40a..0000000 Binary files a/EcoSonar-SonarQube/ecocode/ecocode-java-plugin-1.2.1.jar and /dev/null differ diff --git a/EcoSonar-SonarQube/ecocode/ecocode-java-plugin-1.4.0.jar b/EcoSonar-SonarQube/ecocode/ecocode-java-plugin-1.4.0.jar new file mode 100644 index 0000000..7ac8078 Binary files /dev/null and b/EcoSonar-SonarQube/ecocode/ecocode-java-plugin-1.4.0.jar differ diff --git a/EcoSonar-SonarQube/ecocode/ecocode-javascript-plugin-1.3.1-SNAPSHOT.jar b/EcoSonar-SonarQube/ecocode/ecocode-javascript-plugin-1.3.1-SNAPSHOT.jar new file mode 100644 index 0000000..c9ff09f Binary files /dev/null and b/EcoSonar-SonarQube/ecocode/ecocode-javascript-plugin-1.3.1-SNAPSHOT.jar differ diff --git a/EcoSonar-SonarQube/ecocode/ecocode-php-plugin-1.2.1.jar b/EcoSonar-SonarQube/ecocode/ecocode-php-plugin-1.2.1.jar deleted file mode 100644 index 7b7c570..0000000 Binary files a/EcoSonar-SonarQube/ecocode/ecocode-php-plugin-1.2.1.jar and /dev/null differ diff --git a/EcoSonar-SonarQube/ecocode/ecocode-php-plugin-1.4.0.jar b/EcoSonar-SonarQube/ecocode/ecocode-php-plugin-1.4.0.jar new file mode 100644 index 0000000..f4160f9 Binary files /dev/null and b/EcoSonar-SonarQube/ecocode/ecocode-php-plugin-1.4.0.jar differ diff --git a/EcoSonar-SonarQube/ecocode/ecocode-python-plugin-1.2.1.jar b/EcoSonar-SonarQube/ecocode/ecocode-python-plugin-1.2.1.jar deleted file mode 100644 index 86aae60..0000000 Binary files a/EcoSonar-SonarQube/ecocode/ecocode-python-plugin-1.2.1.jar and /dev/null differ diff --git a/EcoSonar-SonarQube/ecocode/ecocode-python-plugin-1.4.0.jar b/EcoSonar-SonarQube/ecocode/ecocode-python-plugin-1.4.0.jar new file mode 100644 index 0000000..52410c0 Binary files /dev/null and b/EcoSonar-SonarQube/ecocode/ecocode-python-plugin-1.4.0.jar differ diff --git a/EcoSonar-SonarQube/package.json b/EcoSonar-SonarQube/package.json index c5372fd..865c2a3 100644 --- a/EcoSonar-SonarQube/package.json +++ b/EcoSonar-SonarQube/package.json @@ -1,6 +1,6 @@ { "name": "ecosonar-plugin", - "version": "3.2.0", + "version": "3.3.0", "description": "Ecodesign and accessibility tool to help developpers minimize carbon footprint of their web-application", "main": "index.js", "scripts": { diff --git a/EcoSonar-SonarQube/pom.xml b/EcoSonar-SonarQube/pom.xml index 597bcd8..5d6c7d0 100644 --- a/EcoSonar-SonarQube/pom.xml +++ b/EcoSonar-SonarQube/pom.xml @@ -6,7 +6,7 @@ com.ls ecosonar - 3.2 + 3.3 sonar-plugin @@ -101,6 +101,22 @@ + + org.owasp + dependency-check-maven + 8.4.0 + + HTML + ${project.build.directory}/dependency-check + + + + + check + + + + \ No newline at end of file diff --git a/EcoSonar-SonarQube/src/main/.babelrc.json b/EcoSonar-SonarQube/src/main/.babelrc.json index 6dc2197..4dde1cd 100644 --- a/EcoSonar-SonarQube/src/main/.babelrc.json +++ b/EcoSonar-SonarQube/src/main/.babelrc.json @@ -1,4 +1,4 @@ { "presets": ["@babel/preset-react"], "plugins": ["@babel/plugin-transform-runtime"] -} +} \ No newline at end of file diff --git a/EcoSonar-SonarQube/src/main/js/ecosonar_analysis_page/components/AnalysisPerUrlPanel.js b/EcoSonar-SonarQube/src/main/js/ecosonar_analysis_page/components/AnalysisPerUrlPanel.js index f11acc8..7fe3e5f 100644 --- a/EcoSonar-SonarQube/src/main/js/ecosonar_analysis_page/components/AnalysisPerUrlPanel.js +++ b/EcoSonar-SonarQube/src/main/js/ecosonar_analysis_page/components/AnalysisPerUrlPanel.js @@ -1,9 +1,9 @@ import React from 'react' -import { getAnalysisUrlConfiguration } from '../../services/ecosonarService' +import { getAnalysisUrlConfiguration } from '../../services/ecoSonarService' +import AccessibilityAlert from '../../utils/AccessibilityAlert' import GraphPanelForUrl from './GraphPanel/GraphPanelForUrl' import GreenItPanelPerUrl from './GreenItPanel/GreenItPanelPerUrl' import LightHousePanelPerUrl from './LighthousePanel/LighthousePanelPerUrl' -import AccessibilityAlert from '../../utils/AccessibilityAlert' import W3cPanelPerUrl from './W3cPanel/W3cPanelPerUrl' export default class AnalysisPerUrlPanel extends React.PureComponent { constructor (props) { diff --git a/EcoSonar-SonarQube/src/main/js/ecosonar_analysis_page/components/EcoSonarAnalysisPage.js b/EcoSonar-SonarQube/src/main/js/ecosonar_analysis_page/components/EcoSonarAnalysisPage.js index 713f4b2..1b5d57c 100644 --- a/EcoSonar-SonarQube/src/main/js/ecosonar_analysis_page/components/EcoSonarAnalysisPage.js +++ b/EcoSonar-SonarQube/src/main/js/ecosonar_analysis_page/components/EcoSonarAnalysisPage.js @@ -1,6 +1,6 @@ import React from 'react' import { getUrlsConfiguration } from '../../services/configUrlService' -import { getAnalysisForProjectConfiguration } from '../../services/ecosonarService' +import { getAnalysisForProjectConfiguration } from '../../services/ecoSonarService' import AnalysisPanel from './AnalysisPanel' import DisclaimerPanel from './IndexPanel/DisclaimerPanel' import EcoIndexPanel from './IndexPanel/EcoIndexPanel' diff --git a/EcoSonar-SonarQube/src/main/js/ecosonar_bestpractices_page/components/BestPracticesPage.js b/EcoSonar-SonarQube/src/main/js/ecosonar_bestpractices_page/components/BestPracticesPage.js index ff2d723..012d07d 100644 --- a/EcoSonar-SonarQube/src/main/js/ecosonar_bestpractices_page/components/BestPracticesPage.js +++ b/EcoSonar-SonarQube/src/main/js/ecosonar_bestpractices_page/components/BestPracticesPage.js @@ -128,12 +128,10 @@ export default function BestPracticesPage (props) { getBestPractices(props.project.key) .then((data) => { setResultData(data) - setLoading(false) }) .catch((result) => { if (result instanceof Error) { setError(result.message) - setLoading(false) } }) }) @@ -142,6 +140,9 @@ export default function BestPracticesPage (props) { setSaveProcedureError(err.message) } }) + .finally(() => { + setLoading(false) + }) } function setResultData (data) { setBestPracticesEcodesign(data.ecodesign) @@ -154,7 +155,7 @@ export default function BestPracticesPage (props) { function handleAccessibilityAlert () { setAriaAlertForAccessibility(false) setAriaAlertForAccessibility(true) - }; + } function handleCloseAll () { setIsFolded(!isFolded) diff --git a/EcoSonar-SonarQube/src/main/js/models/Analysis.js b/EcoSonar-SonarQube/src/main/js/models/Analysis.js deleted file mode 100644 index d462d05..0000000 --- a/EcoSonar-SonarQube/src/main/js/models/Analysis.js +++ /dev/null @@ -1,54 +0,0 @@ -import { GreenITAnalysis } from './GreenITAnalysis' -import { PractisesAnalysis } from './PracticesAnalysis' - -export class Analysis { - constructor (url) { - this.url = url - this.greenITAnalysis = new GreenITAnalysis() - this.devPractices = new PractisesAnalysis() - } - - setAnalysisFromJsonFile (data) { - this.greenITAnalysis.setGreenITAnalysis( - data.grade, - data.ecoIndex, - data.waterConsumption, - data.greenhouseGasesEmission, - data.domSize, - data.nbRequest, - Math.round(data.responsesSize / 1000), - data.pluginsNumber, - data.printStyleSheetsNumber, - data.inlineStyleSheetsNumber, - data.inlineJsScriptsNumber, - data.emptySrcTagNumber, - data.imagesResizedInBrowser.length - ) - this.devPractices.setPracticesAnalysis( - data.bestPractices.AddExpiresOrCacheControlHeaders.complianceLevel || 'A', - data.bestPractices.CompressHttp.complianceLevel || 'A', - data.bestPractices.DomainsNumber.complianceLevel || 'A', - data.bestPractices.DontResizeImageInBrowser.complianceLevel || 'A', - data.bestPractices.EmptySrcTag.complianceLevel || 'A', - data.bestPractices.ExternalizeCss.complianceLevel || 'A', - data.bestPractices.ExternalizeJs.complianceLevel || 'A', - data.bestPractices.HttpError.complianceLevel || 'A', - data.bestPractices.HttpRequests.complianceLevel || 'A', - data.bestPractices.ImageDownloadedNotDisplayed.complianceLevel || 'A', - data.bestPractices.JsValidate.complianceLevel || 'A', - data.bestPractices.MaxCookiesLength.complianceLevel || 'A', - data.bestPractices.MinifiedCss.complianceLevel || 'A', - data.bestPractices.MinifiedJs.complianceLevel || 'A', - data.bestPractices.NoCookieForStaticRessources.complianceLevel || 'A', - data.bestPractices.NoRedirect.complianceLevel || 'A', - data.bestPractices.OptimizeBitmapImages.complianceLevel || 'A', - data.bestPractices.OptimizeSvg.complianceLevel || 'A', - data.bestPractices.Plugins.complianceLevel || 'A', - data.bestPractices.PrintStyleSheet.complianceLevel || 'A', - data.bestPractices.SocialNetworkButton.complianceLevel || 'A', - data.bestPractices.StyleSheets.complianceLevel || 'A', - data.bestPractices.UseETags.complianceLevel || 'A', - data.bestPractices.UseStandardTypefaces.complianceLevel || 'A' - ) - } -} diff --git a/EcoSonar-SonarQube/src/main/js/models/GreenITAnalysis.js b/EcoSonar-SonarQube/src/main/js/models/GreenITAnalysis.js deleted file mode 100644 index 984fc99..0000000 --- a/EcoSonar-SonarQube/src/main/js/models/GreenITAnalysis.js +++ /dev/null @@ -1,51 +0,0 @@ -export class GreenITAnalysis { - setGreenITAnalysis ( - grade, - ecoIndexGrade, - water, - ghg, - domSize, - requests, - pageSize, - plugins, - cssFiles, - inlineCss, - inlineJs, - emptySrcTag, - imagesResized - ) { - this.grade = grade - this.ecoIndexGrade = ecoIndexGrade - this.water = water - this.ghg = ghg - this.domSize = domSize - this.requests = requests - this.pageSize = pageSize - this.plugins = plugins - this.cssFiles = cssFiles - this.inlineCss = inlineCss - this.inlineJs = inlineJs - this.emptySrcTag = emptySrcTag - this.imagesResized = imagesResized - } - - setGreenITAnalysisFromJsonFile (data) { - this.grade = data.grade - this.ecoIndexGrade = data.ecoIndex - this.water = data.waterConsumption - this.ghg = data.greenhouseGasesEmission - this.domSize = data.domSize - this.requests = data.nbRequest - this.pageSize = Math.round(data.responsesSize / 1000) - this.plugins = data.pluginsNumber - this.cssFiles = data.printStyleSheetsNumber - this.inlineCss = data.inlineStyleSheetsNumber - this.inlineJs = data.inlineJsScriptsNumber - this.emptySrcTag = data.emptySrcTagNumber - this.imagesResized = data.imagesResizedInBrowser.length - } - - setGreenITAnalysisDate (date) { - this.date = date - } -} diff --git a/EcoSonar-SonarQube/src/main/js/models/PracticesAnalysis.js b/EcoSonar-SonarQube/src/main/js/models/PracticesAnalysis.js deleted file mode 100644 index 006101a..0000000 --- a/EcoSonar-SonarQube/src/main/js/models/PracticesAnalysis.js +++ /dev/null @@ -1,53 +0,0 @@ -export class PractisesAnalysis { - setPracticesAnalysis ( - addExpiresOrCacheControlHeaders, - compressHttp, - domainsNumber, - dontResizeImageInBrowser, - emptySrcTag, - externalizeCss, - externalizeJs, - httpError, - httpRequests, - imageDownloadedNotDisplayed, - jsValidate, - maxCookiesLength, - minifiedCss, - minifiedJs, - noCookieForStaticRessources, - noRedirect, - optimizeBitmapImages, - optimizeSvg, - plugins, - printStyleSheet, - socialNetworkButton, - styleSheets, - useETags, - useStandardTypefaces - ) { - this.addExpiresOrCacheControlHeaders = addExpiresOrCacheControlHeaders - this.compressHttp = compressHttp - this.domainsNumber = domainsNumber - this.dontResizeImageInBrowser = dontResizeImageInBrowser - this.emptySrcTag = emptySrcTag - this.externalizeCss = externalizeCss - this.externalizeJs = externalizeJs - this.httpError = httpError - this.httpRequests = httpRequests - this.imageDownloadedNotDisplayed = imageDownloadedNotDisplayed - this.jsValidate = jsValidate - this.maxCookiesLength = maxCookiesLength - this.minifiedCss = minifiedCss - this.minifiedJs = minifiedJs - this.noCookieForStaticRessources = noCookieForStaticRessources - this.noRedirect = noRedirect - this.optimizeBitmapImages = optimizeBitmapImages - this.optimizeSvg = optimizeSvg - this.plugins = plugins - this.printStyleSheet = printStyleSheet - this.socialNetworkButton = socialNetworkButton - this.styleSheets = styleSheets - this.useETags = useETags - this.useStandardTypefaces = useStandardTypefaces - } -} diff --git a/README.md b/README.md index e2b0eaa..bcf0c62 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ For EcoSonar Sonarqube plugin : https://github.com/Accenture/EcoSonar/blob/main/ For specific details on Ecocode, please look at their GitHub repository: https://github.com/green-code-initiative/ecoCode . You will find here https://github.com/Accenture/EcoSonar/tree/main/EcoSonar-SonarQube/ecocode the EcoCode Sonarqube plugin that needs to be imported into your Sonarqube instance. -To install plugins, you can follow the same instructions provided for EcoSonar Sonarqube plugin https://github.com/Accenture/EcoSonar/tree/main/EcoSonar-SonarQube#install-sonarqube-plugins and copy/paste the 3 jar files into the same `extensions/plugins/` folder. +To install plugins, you can follow the same instructions provided for EcoSonar Sonarqube plugin https://github.com/Accenture/EcoSonar/tree/main/EcoSonar-SonarQube#install-sonarqube-plugins and copy/paste the jar files into the same `extensions/plugins/` folder. When using Sonarqube as code analysis, a default Quality Profile is set up for each language. If you want to use EcoCode rules related to eco-design, you will have to: diff --git a/ROADMAP.md b/ROADMAP.md index b8caa51..9e84433 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -5,5 +5,3 @@ 2) Integration of WebSite Analytics : weighted-average of EcoSonar scores according to page frequency, detection of pages not visited that could be decomissionned, etc. 3) More green coding rules - -4) Investigation on added-value that LLM Models (such as GPT-3) could procure diff --git a/docker-compose.yml b/docker-compose.yml index 318caef..8fb6091 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,27 +25,31 @@ services: SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonarqube SONAR_ES_BOOTSTRAP_CHECKS_DISABLE: 'true' volumes: - - ./EcoSonar-SonarQube/target/ecosonar-3.2.jar:/opt/sonarqube/extensions/plugins/ecosonar-3.2.jar:ro - - ./EcoSonar-SonarQube/ecocode/ecocode-java-plugin-1.2.1.jar:/opt/sonarqube/extensions/plugins/ecosonar-java-plugin-1.2.1.jar:ro - - ./EcoSonar-SonarQube/ecocode/ecocode-php-plugin-1.2.1.jar:/opt/sonarqube/extensions/plugins/ecosonar-php-plugin-1.2.1.jar:ro - - ./EcoSonar-SonarQube/ecocode/ecocode-python-plugin-1.2.1.jar:/opt/sonarqube/extensions/plugins/ecosonar-python-plugin-1.2.1.jar:ro - - "extensions:/opt/sonarqube/extensions" - - "logs:/opt/sonarqube/logs" - - "data:/opt/sonarqube/data" + - ./EcoSonar-SonarQube/target/ecosonar-3.3.jar:/opt/sonarqube/extensions/plugins/ecosonar-3.3.jar:ro + - ./EcoSonar-SonarQube/ecocode/ecocode-android-1.0.1.jar:/opt/sonarqube/extensions/plugins/ecocode-android-1.0.1.jar + - ./EcoSonar-SonarQube/ecocode/ecocode-ios-1.1.0.jar:/opt/sonarqube/extensions/plugins/ecocode-ios-1.1.0.jar:ro + - ./EcoSonar-SonarQube/ecocode/ecocode-java-plugin-1.4.0.jar:/opt/sonarqube/extensions/plugins/ecocode-java-plugin-1.4.0.jar:ro + - ./EcoSonar-SonarQube/ecocode/ecocode-javascript-plugin-1.3.1-SNAPSHOT.jar:/opt/sonarqube/extensions/plugins/ecocode-javascript-plugin-1.3.1-SNAPSHOT.jar:ro + - ./EcoSonar-SonarQube/ecocode/ecocode-php-plugin-1.4.0.jar:/opt/sonarqube/extensions/plugins/ecocode-php-plugin-1.4.0.jar:ro + - ./EcoSonar-SonarQube/ecocode/ecocode-python-plugin-1.4.0.jar:/opt/sonarqube/extensions/plugins/ecocode-python-plugin-1.4.0.jar:ro + - sonarqube_extensions:/opt/sonarqube/extensions + - sonarqube_logs:/opt/sonarqube/logs + - sonarqube_data:/opt/sonarqube/data restart: unless-stopped db: - image: postgres:alpine + image: postgres:13 container_name: ecosonar_postgresql networks: - sonarnet volumes: - - pg_data:/var/lib/postgresql/data + - postgresql:/var/lib/postgresql + - postgresql_data:/var/lib/postgresql/data environment: POSTGRES_USER: sonar POSTGRES_PASSWORD: sonar POSTGRES_DB: sonarqube - PGDATA: pg_data:/var/lib/postgresql/data/pgdata + PGDATA: postgresql_data:/var/lib/postgresql/data/pgdata restart: unless-stopped networks: @@ -53,7 +57,8 @@ networks: driver: bridge volumes: - data: - logs: - extensions: - pg_data: + sonarqube_data: + sonarqube_logs: + sonarqube_extensions: + postgresql: + postgresql_data: diff --git a/images/delete-login-for-project.webp b/images/delete-login-for-project.webp new file mode 100644 index 0000000..688090a Binary files /dev/null and b/images/delete-login-for-project.webp differ diff --git a/images/delete-proxy-for-project.webp b/images/delete-proxy-for-project.webp new file mode 100644 index 0000000..37e424d Binary files /dev/null and b/images/delete-proxy-for-project.webp differ diff --git a/images/delete-url-in-project.webp b/images/delete-url-in-project.webp new file mode 100644 index 0000000..dc19451 Binary files /dev/null and b/images/delete-url-in-project.webp differ diff --git a/images/delete-user-flow-for-url.webp b/images/delete-user-flow-for-url.webp new file mode 100644 index 0000000..bcd5f38 Binary files /dev/null and b/images/delete-user-flow-for-url.webp differ diff --git a/images/ecosonar-architecture.webp b/images/ecosonar-architecture.webp index f322195..d8be5fd 100644 Binary files a/images/ecosonar-architecture.webp and b/images/ecosonar-architecture.webp differ diff --git a/images/ecosonar-plugin.webp b/images/ecosonar-plugin.webp index 9c518d6..84256ea 100644 Binary files a/images/ecosonar-plugin.webp and b/images/ecosonar-plugin.webp differ diff --git a/images/get-all-scores-projects.webp b/images/get-all-scores-projects.webp new file mode 100644 index 0000000..7c1a8c6 Binary files /dev/null and b/images/get-all-scores-projects.webp differ diff --git a/images/get-average-all-scores-projects.webp b/images/get-average-all-scores-projects.webp new file mode 100644 index 0000000..45185cb Binary files /dev/null and b/images/get-average-all-scores-projects.webp differ diff --git a/images/get-crawler-result.webp b/images/get-crawler-result.webp new file mode 100644 index 0000000..9de78ae Binary files /dev/null and b/images/get-crawler-result.webp differ diff --git a/images/get-login-for-project.webp b/images/get-login-for-project.webp new file mode 100644 index 0000000..d7b5f16 Binary files /dev/null and b/images/get-login-for-project.webp differ diff --git a/images/get-project-scores.webp b/images/get-project-scores.webp new file mode 100644 index 0000000..d4bd872 Binary files /dev/null and b/images/get-project-scores.webp differ diff --git a/images/get-proxy-for-project.webp b/images/get-proxy-for-project.webp new file mode 100644 index 0000000..b763e61 Binary files /dev/null and b/images/get-proxy-for-project.webp differ diff --git a/images/get-urls-from-project.webp b/images/get-urls-from-project.webp new file mode 100644 index 0000000..0645189 Binary files /dev/null and b/images/get-urls-from-project.webp differ diff --git a/images/get-user-flow-for-url.webp b/images/get-user-flow-for-url.webp new file mode 100644 index 0000000..fa9115f Binary files /dev/null and b/images/get-user-flow-for-url.webp differ diff --git a/images/insert-urls-in-project.webp b/images/insert-urls-in-project.webp new file mode 100644 index 0000000..45007cc Binary files /dev/null and b/images/insert-urls-in-project.webp differ diff --git a/images/launch-analysis.webp b/images/launch-analysis.webp new file mode 100644 index 0000000..f618dab Binary files /dev/null and b/images/launch-analysis.webp differ diff --git a/images/retrieve-analysis-per-project.webp b/images/retrieve-analysis-per-project.webp new file mode 100644 index 0000000..d78b35d Binary files /dev/null and b/images/retrieve-analysis-per-project.webp differ diff --git a/images/retrieve-analysis-per-url.webp b/images/retrieve-analysis-per-url.webp new file mode 100644 index 0000000..e38b902 Binary files /dev/null and b/images/retrieve-analysis-per-url.webp differ diff --git a/images/retrieve-best-practices-per-project.webp b/images/retrieve-best-practices-per-project.webp new file mode 100644 index 0000000..8645209 Binary files /dev/null and b/images/retrieve-best-practices-per-project.webp differ diff --git a/images/retrieve-best-practices-per-url.webp b/images/retrieve-best-practices-per-url.webp new file mode 100644 index 0000000..5e17a22 Binary files /dev/null and b/images/retrieve-best-practices-per-url.webp differ diff --git a/images/retrieve-procedure-saved-for-the-project.webp b/images/retrieve-procedure-saved-for-the-project.webp new file mode 100644 index 0000000..ae95344 Binary files /dev/null and b/images/retrieve-procedure-saved-for-the-project.webp differ diff --git a/images/save-login-and-proxy-for-project.webp b/images/save-login-and-proxy-for-project.webp new file mode 100644 index 0000000..f1df744 Binary files /dev/null and b/images/save-login-and-proxy-for-project.webp differ diff --git a/images/save-procedure-for-the-project.webp b/images/save-procedure-for-the-project.webp new file mode 100644 index 0000000..8ccdc76 Binary files /dev/null and b/images/save-procedure-for-the-project.webp differ diff --git a/images/save-user-flow-for-url.webp b/images/save-user-flow-for-url.webp new file mode 100644 index 0000000..8d57018 Binary files /dev/null and b/images/save-user-flow-for-url.webp differ