Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/sonar-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"@creedengo/vue-dashboard": "workspace:*"
},
"devDependencies": {
"vite": "^7.1.3"
"vite": "^7.1.4"
},
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748"
}
11 changes: 0 additions & 11 deletions apps/vue-app/.storybook/main.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@

const coverageConfig = {
istanbul: {
include: ['**/*.stories.js'],
},
};

/** @type { import('@storybook/vue3-vite').StorybookConfig } */
const config = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.js'],
addons: [
'@storybook/addon-links',
'@storybook/addon-docs',
'@storybook/addon-a11y',
{
name: '@storybook/addon-coverage',
options: coverageConfig,
},
],
core: {
builder: '@storybook/builder-vite',
Expand Down
8 changes: 7 additions & 1 deletion apps/vue-app/.storybook/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ const preview = {
color: /(background|color)$/i,
date: /Date$/i
}
}
},
docs: {
canvas: {
sourceState: 'shown',
},
codePanel: true,
},
},
loaders: [mswLoader],
}
Expand Down
8 changes: 4 additions & 4 deletions apps/vue-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"dependencies": {
"@creedengo/sonar-services": "workspace:*",
"@creedengo/vue-ui": "workspace: *",
"vue": "^3.5.18"
"vue": "^3.5.20"
},
"devDependencies": {
"@creedengo/core-services": "workspace: *",
Expand All @@ -55,15 +55,15 @@
"eslint-plugin-playwright": "^2.2.2",
"eslint-plugin-storybook": "^9.1.3",
"eslint-plugin-vue": "^10.4.0",
"jest": "^30.1.1",
"jest": "^30.1.2",
"jsdom": "^26.1.0",
"msw": "^2.10.5",
"msw": "^2.11.1",
"msw-storybook-addon": "^2.0.5",
"@playwright/test": "^1.55.0",
"sonarqube-scanner": "^4.3.0",
"storybook": "^9.1.3",
"vite-plugin-vue-devtools": "^8.0.1",
"vite": "^7.1.3",
"vite": "^7.1.4",
"vitest": "^3.2.4"
},
"msw": {
Expand Down
7 changes: 0 additions & 7 deletions apps/vue-app/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}

Expand All @@ -26,10 +25,4 @@ a,
display: flex;
place-items: center;
}

#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}
10 changes: 6 additions & 4 deletions apps/vue-app/src/components/pages/DashboardPage.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<script setup>
import { onMounted, reactive } from 'vue';

import { AbcdeScore } from '@creedengo/vue-ui'
import SonarAPI from '@creedengo/sonar-services'
import core from '@creedengo/core-services';
import { DashboardPageTemplate } from '@creedengo/vue-ui';

const { api, calculateProjectScore } = core;
const { api, calculateProjectScore, getScoreTexts } = core;

api.init(SonarAPI)
const props = defineProps({
Expand All @@ -23,7 +23,9 @@ const state = reactive({ score: '', error: null });

onMounted(async () => {
try {
state.score = await calculateProjectScore({ ...props });
const value = await calculateProjectScore({ ...props });
const { label, description, tips } = getScoreTexts(value);
state.score = { value, label, description, tips };
} catch (error) {
state.score = 'N/A';
globalThis.console.error('Error fetching score:', error);
Expand All @@ -45,7 +47,7 @@ onMounted(async () => {
<i class="fa fa-exclamation-triangle" /> Score not available - {{ state.error }}
</span>
<span v-else>
<AbcdeScore :value="state.score" />
<DashboardPageTemplate :score="state.score" />
</span>
</div>
</main>
Expand Down
970 changes: 551 additions & 419 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions shared/core-services/src/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import api from './adapter.js'
import settings from './settings.service.js'

import * as priorityRule from './priority-rule.service.js'
import * as score from './score.service.js'

export default {
api,
settings,
...priorityRule,
...score
}
72 changes: 72 additions & 0 deletions shared/core-services/src/priority-rule.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* @module priority-rule.services
* @description This module provides functions to recommend a priority rule to fix.
*/

import api from './adapter.js';

/**
* @typedef {Object} DescriptionSection
* @property {string} key
* @property {string} content
* @property {string} [context]
*/

/**
* @typedef {Object} RuleDetails
* @property {string} key
* @property {string} name
* @property {string} htmlDesc
* @property {'INFO'|'MINOR'|'MAJOR'|'CRITICAL'|'BLOCKER'} severity
* @property {string[]} tags
* @property {string} langName
* @property {DescriptionSection[]} descriptionSections
*/

/**
* @param {Object} ruleFacet
* @returns {string}
*/
function getMostIssuesRule(ruleFacet) {
let ruleKey = null
let max = 0
for (let [key, value] of Object.entries(ruleFacet)) {
if (value > max) {
max = value
ruleKey = key
}
}
return ruleKey
}

const SEVERITY_LEVELS = [
'BLOCKER',
'CRITICAL',
'MAJOR',
'MINOR,INFO'
]

/**
* Select a PriorityRule based the score calculation algorithm
*
* @param {Object} config
* @param {string} config.project key of the project
* @param {string} config.branch
* @returns {Promise<RuleDetails|null>}
*/
export async function getPriorityRule(config) {
let ruleKey;

for (let severity of SEVERITY_LEVELS) {
const ruleFacet = await api.services.getIssuesFacet('rules', { ...config, severity });
ruleKey = getMostIssuesRule(ruleFacet)
if (ruleKey) {
break
}
}

const ruleDetails = ruleKey ?
await api.services.getRuleDetails(ruleKey) :
null
return ruleDetails;
}
70 changes: 70 additions & 0 deletions shared/core-services/src/priority-rule.service.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { vi, describe, expect, test } from 'vitest'

import api from './adapter'
import { getPriorityRule } from './priority-rule.service'

vi.mock('./adapter', async () => ({ default: { services: {
getIssuesFacet: vi.fn(),
getRuleDetails: vi.fn().mockImplementation(key => ({
'blocker:key': { key },
'critical:key': { key },
'major:key': { key },
'minor:key': { key },
'info:key': { key },
})[key])
}}}))

const mockConfig = { project: 'foo' }

function mockGetIssuesFacet(data) {
return (facet, config) => {
expect(config).toHaveProperty('severity')
expect(config).toHaveProperty('project', 'foo')
expect(facet).toBe('rules')
return data[config.severity] || {}
}
}

const BLOCKER = { 'blocker:key': 4, 'blocker:key2': 3 }
const CRITICAL = { 'critical:key': 7, 'critical:key2': 4 }
const MAJOR = { 'major:key': 12, 'critical:key2': 4 }
const MINOR_OR_INFO = { 'minor:key': 31, 'info:key': 12, 'minor:key2': 4 }

describe('getPriorityRule()', () => {

test('Should return Blocker issue related rule if one exist', async () => {
const mock = mockGetIssuesFacet({ BLOCKER, CRITICAL, MAJOR })
api.services.getIssuesFacet.mockImplementation(mock);
const rule = await getPriorityRule(mockConfig)
expect(rule).toHaveProperty('key', 'blocker:key')
})

test('Should return Critical issue related rule if one exist and no blocker exist', async () => {
const mock = mockGetIssuesFacet({ CRITICAL, MAJOR })
api.services.getIssuesFacet.mockImplementation(mock);
const rule = await getPriorityRule(mockConfig)
expect(rule).toHaveProperty('key', 'critical:key')
})

test('Should return major issue related rule if one exist and no blocker or critical exist', async () => {
const mock = mockGetIssuesFacet({ MAJOR, 'MINOR,INFO': MINOR_OR_INFO })
api.services.getIssuesFacet.mockImplementation(mock);
const rule = await getPriorityRule(mockConfig)
expect(rule).toHaveProperty('key', 'major:key')
})

test('Should return minor issue related rule if one exist and no blocker, critical or major exist', async () => {
const mock = mockGetIssuesFacet({ 'MINOR,INFO': MINOR_OR_INFO })
api.services.getIssuesFacet.mockImplementation(mock);
const rule = await getPriorityRule(mockConfig)
expect(rule).toHaveProperty('key', 'minor:key')
})

test('Should return null if no issue exist', async () => {
const mock = mockGetIssuesFacet({ })
api.services.getIssuesFacet.mockImplementation(mock);
const rule = await getPriorityRule(mockConfig)
expect(rule).toBe(null)
})

})
37 changes: 37 additions & 0 deletions shared/core-services/src/score.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@

import api from './adapter.js';

const scoreTexts = {
A: {
label: "Your app is fully optimized, congratulations!",
description: "Don't forget to check it again if you update your app.",
tips: "100 % optimized, congrats!"
},
B: {
label: "Your app is nearly optimized.",
description: "Well done! You can continue by fixing the recommended rule on the right side. This is the one that currently has the highest impact on your app.",
tips: "You have between 1 and 9 minor severities."
},
C: {
label: "Your app is not fully optimized.",
description: "Keep going! You can continue by fixing the recommended rule on the right side. This is the one that currently has the highest impact on your app.",
tips: "You have between 10 and 19 minor severities or you have 1 or many major severity."
},
D: {
label: "Many elements of your application can be optimized.",
description: "Don't worry! You can start by fixing the recommended rule on the right side. This is the one that currently has the highest impact on your app.",
tips: "You have more than 20 minor severities or more than 10 major severities or 1 or many critical severities."
},
E: {
label: "Several elements of your application can be optimized.",
description: "Don't worry! You can start by fixing the recommended rule on the right side. This is the one that currently has the highest impact on your app.",
tips: "You have 1 or more than 1 blocker severities."
}
};

/**
* Calculate an ABCDE score from the sustainability issues related to the project size.
* @param {Object} config
Expand Down Expand Up @@ -35,3 +63,12 @@ export async function calculateProjectScore(config) {
}
return 'A';
}

/**
* Returns the texts corresponding to the score.
* @param {string} score the value of the project's score.
* @returns {Object} texts corresponding to the project score.
*/
export function getScoreTexts(score) {
return scoreTexts[score]
}
2 changes: 1 addition & 1 deletion shared/core-services/src/score.service.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ beforeAll(() => {
});

afterAll(() => {
// Mock the API calls
// Clean the API calls
});

describe('calculateProjectScore', () => {
Expand Down
48 changes: 48 additions & 0 deletions shared/core-services/src/settings.service.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { beforeEach, describe, expect, test } from 'vitest'

import settings from './settings.service'

let store;

const storageApi = {
set: (key, value) => store[key] = value,
get: (key) => store[key],
};

beforeEach(()=> {
store = {};
settings.resetSettings()
})

describe('Settings service', () => {

describe('initStorage', () => {
test('apply the given store', async () => {
settings.initStorage(storageApi)
expect(settings.get('foo')).toBe(undefined)
settings.set('foo', 'bar')
expect(settings.get('foo')).toBe('bar')
})

})

describe('storageReady', () => {
test('return false if not initialized', async () => {
expect(settings.storageReady).toBe(false)
})
test('return true if initialized', async () => {
settings.initStorage(storageApi)
expect(settings.storageReady).toBe(true)
})
})

describe('resetSettings', () => {
test('storageReady return false after initialized and reset', async () => {
settings.initStorage(storageApi)
settings.resetSettings()
expect(settings.storageReady).toBe(false)
})

})

})
Loading