diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/repository/ScoreRepository.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/repository/ScoreRepository.java index 415aa6134..8dc0b66cf 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/repository/ScoreRepository.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/repository/ScoreRepository.java @@ -31,14 +31,14 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.stereotype.Repository; @Repository public interface ScoreRepository extends JpaRepository { Optional findFirstByPluginOrderByComputedAtDesc(Plugin plugin); - @Query( + @NativeQuery( value = """ SELECT DISTINCT ON (s.plugin_id) @@ -50,23 +50,21 @@ SELECT DISTINCT ON (s.plugin_id) FROM scores s JOIN plugins p on s.plugin_id = p.id ORDER BY s.plugin_id, s.computed_at DESC; - """, - nativeQuery = true) + """) List findLatestScoreForAllPlugins(); - @Query( + @NativeQuery( value = """ SELECT DISTINCT ON (s.plugin_id) s.value FROM scores s ORDER BY s.plugin_id, s.computed_at DESC, s.value; - """, - nativeQuery = true) + """) int[] getLatestScoreValueOfEveryPlugin(); @Modifying - @Query( + @NativeQuery( value = """ DELETE FROM scores @@ -78,11 +76,10 @@ SELECT id, ROW_NUMBER() OVER (PARTITION BY plugin_id ORDER BY computed_at DESC) ) s WHERE row_num <= 5 ); - """, - nativeQuery = true) + """) int deleteOldScoreFromPlugin(); - @Query( + @NativeQuery( value = """ SELECT @@ -99,7 +96,35 @@ SELECT DISTINCT ON (s.plugin_id) FROM scores s ORDER BY s.plugin_id, s.computed_at DESC ); - """, - nativeQuery = true) + """) List getAllLatestScoresWithValue(int score); + + @NativeQuery( + value = + """ + SELECT + s.id, + s.plugin_id, + s.computed_at, + s.details, + s.value + FROM scores s + LEFT JOIN plugins p on s.plugin_id = p.id + WHERE s.id IN ( + SELECT DISTINCT ON (s.plugin_id) + s.id + FROM scores s + ORDER BY s.plugin_id, s.computed_at DESC + ) AND (EXISTS ( + SELECT * + FROM jsonb_array_elements(s.details) as r(detail) + WHERE detail ->> 'key' = ?1 + AND (detail -> 'value')::int < 100 + ) OR NOT EXISTS ( + SELECT * + FROM jsonb_array_elements(s.details) as r(detail) + WHERE detail ->> 'key' = ?1 + )); + """) + List getAllLatestScoresWithIncompleteScoring(String scoringKey); } diff --git a/core/src/main/java/io/jenkins/pluginhealth/scoring/service/ScoreService.java b/core/src/main/java/io/jenkins/pluginhealth/scoring/service/ScoreService.java index dd1881f34..f855928f8 100644 --- a/core/src/main/java/io/jenkins/pluginhealth/scoring/service/ScoreService.java +++ b/core/src/main/java/io/jenkins/pluginhealth/scoring/service/ScoreService.java @@ -101,4 +101,9 @@ public Map getScoresDistribution() { public List getAllLatestScoresWithValue(int value) { return repository.getAllLatestScoresWithValue(value); } + + @Transactional(readOnly = true) + public List getAllLatestScoresWithIncompleteScoring(String scoringKey) { + return repository.getAllLatestScoresWithIncompleteScoring(scoringKey); + } } diff --git a/core/src/test/java/io/jenkins/pluginhealth/scoring/service/ScoreServiceIT.java b/core/src/test/java/io/jenkins/pluginhealth/scoring/service/ScoreServiceIT.java index e8c8f77ed..b529dfc07 100644 --- a/core/src/test/java/io/jenkins/pluginhealth/scoring/service/ScoreServiceIT.java +++ b/core/src/test/java/io/jenkins/pluginhealth/scoring/service/ScoreServiceIT.java @@ -226,4 +226,39 @@ void shouldBeAbleToRetrieveAllPluginsWithSpecificScore() { assertThat(scoreService.getAllLatestScoresWithValue(50)).containsExactly(s2); assertThat(scoreService.getAllLatestScoresWithValue(75)).containsExactlyInAnyOrder(s3, s4); } + + @Test + void shouldBeAbleToRetrieveScoresWithIncompleteSections() { + final Plugin p1 = + entityManager.persist(new Plugin("foo", new VersionNumber("1.0"), "scm", ZonedDateTime.now())); + final Plugin p2 = + entityManager.persist(new Plugin("bar", new VersionNumber("1.1"), "scm", ZonedDateTime.now())); + final Plugin p3 = + entityManager.persist(new Plugin("zoo", new VersionNumber("1.1"), "scm", ZonedDateTime.now())); + + final Score s1 = new Score(p1, ZonedDateTime.now().minusDays(1)); + s1.addDetail(new ScoreResult("key-1", 90, 1, Set.of(), 1)); + s1.addDetail(new ScoreResult("key-2", 80, 1, Set.of(), 1)); + final Score s2 = new Score(p1, ZonedDateTime.now()); + s2.addDetail(new ScoreResult("key-1", 50, 1, Set.of(), 1)); + s2.addDetail(new ScoreResult("key-2", 80, 1, Set.of(), 1)); + final Score s3 = new Score(p2, ZonedDateTime.now()); + s3.addDetail(new ScoreResult("key-1", 100, 1, Set.of(), 1)); + s3.addDetail(new ScoreResult("key-2", 100, 1, Set.of(), 1)); + final Score s4 = new Score(p3, ZonedDateTime.now()); + s4.addDetail(new ScoreResult("key-1", 75, 1, Set.of(), 1)); + s4.addDetail(new ScoreResult("key-2", 100, 1, Set.of(), 1)); + + entityManager.persist(s1); + entityManager.persist(s2); + entityManager.persist(s3); + entityManager.persist(s4); + + assertThat(scoreService.getAllLatestScoresWithIncompleteScoring("key-1")) + .containsExactlyInAnyOrder(s2, s4); + assertThat(scoreService.getAllLatestScoresWithIncompleteScoring("key-2")) + .containsOnly(s2); + assertThat(scoreService.getAllLatestScoresWithIncompleteScoring("key-3")) + .containsExactlyInAnyOrder(s2, s3, s4); + } } diff --git a/war/src/main/java/io/jenkins/pluginhealth/scoring/http/DataController.java b/war/src/main/java/io/jenkins/pluginhealth/scoring/http/DataController.java index 9ffa6e45a..3dac094d3 100644 --- a/war/src/main/java/io/jenkins/pluginhealth/scoring/http/DataController.java +++ b/war/src/main/java/io/jenkins/pluginhealth/scoring/http/DataController.java @@ -23,7 +23,13 @@ */ package io.jenkins.pluginhealth.scoring.http; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import io.jenkins.pluginhealth.scoring.scores.Scoring; import io.jenkins.pluginhealth.scoring.service.ScoreService; +import io.jenkins.pluginhealth.scoring.service.ScoringService; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @@ -37,9 +43,11 @@ public class DataController { private final ScoreService scoreService; + private final ScoringService scoringService; - public DataController(ScoreService scoreService) { + public DataController(ScoreService scoreService, ScoringService scoringService) { this.scoreService = scoreService; + this.scoringService = scoringService; } @ModelAttribute(name = "module") @@ -61,4 +69,23 @@ public ModelAndView pluginsPerScore(@PathVariable int score) { modelAndView.addObject("scores", scoreService.getAllLatestScoresWithValue(score)); return modelAndView; } + + @GetMapping(path = {"/pluginsPerScoring", "/pluginsPerScoring/", "/pluginsPerScoring/{scoring}"}) + public ModelAndView pluginsPerScoring(@PathVariable(required = false) String scoring) { + final ModelAndView modelAndView = new ModelAndView("data/pluginsPerScoring"); + final Set scoringKeys = + scoringService.getScoringList().stream().map(Scoring::key).collect(Collectors.toSet()); + modelAndView.addObject("scorings", scoringKeys); + if (scoring != null) { + modelAndView.addObject("selectedScoring", scoring); + modelAndView.addObject("scores", scoreService.getAllLatestScoresWithIncompleteScoring(scoring)); + } else { + final Map distribution = scoringKeys.stream() + .collect(Collectors.toMap(key -> key, key -> scoreService + .getAllLatestScoresWithIncompleteScoring(key) + .size())); + modelAndView.addObject("distribution", distribution); + } + return modelAndView; + } } diff --git a/war/src/main/js/table.js b/war/src/main/js/table.js index 1cae25982..3ddce43f4 100644 --- a/war/src/main/js/table.js +++ b/war/src/main/js/table.js @@ -8,5 +8,5 @@ export function setupDataTable(elementId, option = {}) { }, ...option } - new DataTable(elementId, mergedOpt); + return new DataTable(elementId, mergedOpt); } diff --git a/war/src/main/less/modules/score.less b/war/src/main/less/modules/score.less index 8dbd83567..f185dd287 100644 --- a/war/src/main/less/modules/score.less +++ b/war/src/main/less/modules/score.less @@ -1,5 +1,12 @@ @import '../abstracts/colors'; +.score--failure { + color: var(--danger); +} +.score--success { + color: var(--success); +} + ion-icon { font-size: 24px; diff --git a/war/src/main/resources/templates/data/distribution.html b/war/src/main/resources/templates/data/distribution.html index f7549cd0a..9f09978d8 100644 --- a/war/src/main/resources/templates/data/distribution.html +++ b/war/src/main/resources/templates/data/distribution.html @@ -6,7 +6,7 @@
-

Data

+

Data / Score distribution

diff --git a/war/src/main/resources/templates/data/pluginsPerScore.html b/war/src/main/resources/templates/data/pluginsPerScore.html index 98f29a2ab..e4bc1905d 100644 --- a/war/src/main/resources/templates/data/pluginsPerScore.html +++ b/war/src/main/resources/templates/data/pluginsPerScore.html @@ -6,24 +6,35 @@
-

Data

+

+ Data / Per score +

+ - + + + diff --git a/war/src/main/resources/templates/data/pluginsPerScoring.html b/war/src/main/resources/templates/data/pluginsPerScoring.html new file mode 100644 index 000000000..cd03e1a38 --- /dev/null +++ b/war/src/main/resources/templates/data/pluginsPerScoring.html @@ -0,0 +1,108 @@ + + + + Data + + + +
+

+ Data / + Per scoring / + Per scoring +

+
+
Name ValueScorings Computation timestampDetails
- - details + + + + + + + details
+ + + + + + + + + + + + + + + + + + + + +
NameValuePopularityScoringsComputation timestampDetails
+ + + + + + + + + +
+
+
+ + + + + + +
+ + + diff --git a/war/src/main/resources/templates/scores/listing.html b/war/src/main/resources/templates/scores/listing.html index 4d6029312..0ad6005e5 100644 --- a/war/src/main/resources/templates/scores/listing.html +++ b/war/src/main/resources/templates/scores/listing.html @@ -15,6 +15,7 @@

Scores

Weight What is validated Description + View failing plugins @@ -28,6 +29,11 @@

Scores

+ + + + + diff --git a/war/src/main/svg/eye-outline.svg b/war/src/main/svg/eye-outline.svg new file mode 100644 index 000000000..663f3f1fb --- /dev/null +++ b/war/src/main/svg/eye-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file