From 968278a05790791157f818286ebbae217ea6080f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Casta=C3=B1o=20Arteaga?= Date: Mon, 12 Feb 2024 08:55:39 +0100 Subject: [PATCH] Prepare stats to support more maturities (#1431) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Sergio Castaño Arteaga Signed-off-by: Cintia Sanchez Garcia Co-authored-by: Sergio Castaño Arteaga Co-authored-by: Cintia Sanchez Garcia --- .../migrations/functions/stats/get_stats.sql | 158 ++++++------- database/tests/functions/stats/get_stats.sql | 14 +- web/package.json | 2 +- web/src/data.tsx | 73 +++--- .../search/__snapshots__/index.test.tsx.snap | 208 ------------------ .../layout/search/filters/Section.test.tsx | 55 +++-- .../__snapshots__/Section.test.tsx.snap | 83 +++++-- .../filters/__snapshots__/index.test.tsx.snap | 104 --------- web/src/layout/search/filters/index.test.tsx | 27 ++- web/src/layout/search/filters/index.tsx | 51 +++-- web/src/layout/search/index.tsx | 9 +- .../stats/__snapshots__/index.test.tsx.snap | 5 - web/src/layout/stats/index.test.tsx | 3 +- web/src/layout/stats/index.tsx | 175 +++++++-------- web/src/types.ts | 18 +- web/yarn.lock | 6 +- 16 files changed, 382 insertions(+), 609 deletions(-) diff --git a/database/migrations/functions/stats/get_stats.sql b/database/migrations/functions/stats/get_stats.sql index ae2176b5..f2b703e9 100644 --- a/database/migrations/functions/stats/get_stats.sql +++ b/database/migrations/functions/stats/get_stats.sql @@ -7,9 +7,7 @@ returns json as $$ select p.maturity, p.rating, count(*) as total from project p where p.rating is not null - and - case when p_foundation is not null then - p.foundation_id = p_foundation else true end + and p.foundation_id = p_foundation group by p.maturity, p.rating ) select json_strip_nulls(json_build_object( @@ -19,12 +17,7 @@ returns json as $$ from ( select date from stats_snapshot - where - case when p_foundation is not null then - foundation_id = p_foundation - else - foundation_id is null - end + where foundation_id = p_foundation order by date desc ) s ), @@ -44,9 +37,7 @@ returns json as $$ count(*) as total from project p where p.accepted_at is not null - and - case when p_foundation is not null then - p.foundation_id = p_foundation else true end + and p.foundation_id = p_foundation group by date_trunc('month', p.accepted_at) ) mt ) rt @@ -60,82 +51,79 @@ returns json as $$ count(*) as total from project p where p.accepted_at is not null - and - case when p_foundation is not null then - p.foundation_id = p_foundation else true end + and p.foundation_id = p_foundation group by extract('year' from p.accepted_at), extract('month' from p.accepted_at) order by year desc, month desc ) entry_count ), - 'rating_distribution', json_build_object( - 'all', ( - select json_agg(json_build_object(rating, total)) - from ( - select rating, sum(total) as total - from ratings - group by rating - order by rating asc - ) rg - ), - 'graduated', ( - select json_agg(json_build_object(rating, total)) - from ( - select rating, sum(total) as total - from ratings where maturity = 'graduated' - group by rating - order by rating asc - ) rg - ), - 'incubating', ( - select json_agg(json_build_object(rating, total)) - from ( - select rating, sum(total) as total - from ratings where maturity = 'incubating' - group by rating - order by rating asc - ) rg - ), - 'sandbox', ( - select json_agg(json_build_object(rating, total)) - from ( - select rating, sum(total) as total - from ratings where maturity = 'sandbox' - group by rating - order by rating asc - ) rg - ) + 'rating_distribution', ( + select json_object_agg(maturity, rating_totals) + from ( + ( + select + 'all' as maturity, + jsonb_agg(jsonb_build_object(rating, total)) as rating_totals + from ( + select rating, sum(total) as total + from ratings + where maturity is not null + group by rating + order by rating asc + ) as all_rating_totals + ) + union + ( + select + maturity, + jsonb_agg(jsonb_build_object(rating, total)) as rating_totals + from ( + select maturity, rating, sum(total) as total + from ratings + where maturity is not null + group by maturity, rating + order by maturity, rating asc + ) as maturity_rating_totals + group by maturity + order by maturity asc + ) + ) as rating_distribution ), - 'sections_average', json_build_object( - 'all', json_build_object( - 'documentation', (average_section_score(p_foundation, 'documentation', null)), - 'license', (average_section_score(p_foundation, 'license', null)), - 'best_practices', (average_section_score(p_foundation, 'best_practices', null)), - 'security', (average_section_score(p_foundation, 'security', null)), - 'legal', (average_section_score(p_foundation, 'legal', null)) - ), - 'graduated', json_build_object( - 'documentation', (average_section_score(p_foundation, 'documentation', 'graduated')), - 'license', (average_section_score(p_foundation, 'license', 'graduated')), - 'best_practices', (average_section_score(p_foundation, 'best_practices', 'graduated')), - 'security', (average_section_score(p_foundation, 'security', 'graduated')), - 'legal', (average_section_score(p_foundation, 'legal', 'graduated')) - ), - 'incubating', json_build_object( - 'documentation', (average_section_score(p_foundation, 'documentation', 'incubating')), - 'license', (average_section_score(p_foundation, 'license', 'incubating')), - 'best_practices', (average_section_score(p_foundation, 'best_practices', 'incubating')), - 'security', (average_section_score(p_foundation, 'security', 'incubating')), - 'legal', (average_section_score(p_foundation, 'legal', 'incubating')) - ), - 'sandbox', json_build_object( - 'documentation', (average_section_score(p_foundation, 'documentation', 'sandbox')), - 'license', (average_section_score(p_foundation, 'license', 'sandbox')), - 'best_practices', (average_section_score(p_foundation, 'best_practices', 'sandbox')), - 'security', (average_section_score(p_foundation, 'security', 'sandbox')), - 'legal', (average_section_score(p_foundation, 'legal', 'sandbox')) - ) + 'sections_average', ( + select json_object_agg(maturity, sections_average) + from ( + ( + select + 'all' as maturity, + ( + select jsonb_build_object( + 'documentation', (average_section_score(p_foundation, 'documentation', null)), + 'license', (average_section_score(p_foundation, 'license', null)), + 'best_practices', (average_section_score(p_foundation, 'best_practices', null)), + 'security', (average_section_score(p_foundation, 'security', null)), + 'legal', (average_section_score(p_foundation, 'legal', null)) + ) as sections_average + ) + from project + ) + union + ( + select + distinct maturity, + ( + select jsonb_build_object( + 'documentation', (average_section_score(p_foundation, 'documentation', maturity)), + 'license', (average_section_score(p_foundation, 'license', maturity)), + 'best_practices', (average_section_score(p_foundation, 'best_practices', maturity)), + 'security', (average_section_score(p_foundation, 'security', maturity)), + 'legal', (average_section_score(p_foundation, 'legal', maturity)) + ) as sections_average + ) + from project + where maturity is not null + ) + ) sections_average ), 'views_daily', ( select json_agg(json_build_array(extract(epoch from day)*1000, total)) @@ -144,9 +132,7 @@ returns json as $$ from project_views pv join project p using (project_id) where pv.day >= current_date - '1 month'::interval - and - case when p_foundation is not null then - p.foundation_id = p_foundation else true end + and p.foundation_id = p_foundation group by day order by day asc ) dt @@ -158,9 +144,7 @@ returns json as $$ from project_views pv join project p using (project_id) where pv.day >= current_date - '2 year'::interval - and - case when p_foundation is not null then - p.foundation_id = p_foundation else true end + and p.foundation_id = p_foundation group by month order by month asc ) mt diff --git a/database/tests/functions/stats/get_stats.sql b/database/tests/functions/stats/get_stats.sql index b955dd41..a2b95759 100644 --- a/database/tests/functions/stats/get_stats.sql +++ b/database/tests/functions/stats/get_stats.sql @@ -533,7 +533,7 @@ select is( [1643673600000, 3] ], "rating_distribution": { - "all": [ + "all": [ {"a": 1}, {"b": 1}, {"c": 1} @@ -557,19 +557,17 @@ select is( "documentation": 73, "best_practices": 70 }, - "sandbox": { - "license": 100, - "security": 100, - "documentation": 80, - "best_practices": 100 - }, "graduated": { "license": 65, "security": 60, "documentation": 70, "best_practices": 55 }, - "incubating": { + "sandbox": { + "license": 100, + "security": 100, + "documentation": 80, + "best_practices": 100 } } }, diff --git a/web/package.json b/web/package.json index dc7575cf..442475c9 100644 --- a/web/package.json +++ b/web/package.json @@ -5,7 +5,7 @@ "dependencies": { "apexcharts": "^3.45.1", "classnames": "^2.5.1", - "clo-ui": "https://github.com/cncf/clo-ui.git#v0.2.0", + "clo-ui": "https://github.com/cncf/clo-ui.git#v0.2.1", "lodash": "^4.17.21", "moment": "^2.30.1", "nth-check": "^2.0.1", diff --git a/web/src/data.tsx b/web/src/data.tsx index a9a33f98..5feae1ee 100644 --- a/web/src/data.tsx +++ b/web/src/data.tsx @@ -31,6 +31,7 @@ import { ChecksPerCategory, FilterKind, FiltersSection, + MaturityFilters, Rating, ReportOption, ReportOptionInfo, @@ -40,6 +41,20 @@ import { SortOption, } from './types'; +export const FOUNDATIONS: FoundationInfo = { + [Foundation.cdf]: { + name: 'CDF', + }, + [Foundation.cncf]: { + name: 'CNCF', + }, + [Foundation.lfaidata]: { + name: 'LF AI & Data', + }, +}; + +export const DEFAULT_FOUNDATION = Foundation.cncf; + export const DEFAULT_SORT_BY = SortBy.Name; export const DEFAULT_SORT_DIRECTION = SortDirection.ASC; @@ -47,20 +62,9 @@ export const FILTERS: FiltersSection[] = [ { name: FilterKind.Foundation, title: 'Foundation', - filters: [ - { name: Foundation.cdf, label: 'CDF' }, - { name: Foundation.cncf, label: 'CNCF' }, - { name: Foundation.lfaidata, label: 'LF AI & Data' }, - ], - }, - { - name: FilterKind.Maturity, - title: 'Maturity level', - filters: [ - { name: Maturity.graduated, label: 'Graduated' }, - { name: Maturity.incubating, label: 'Incubating' }, - { name: Maturity.sandbox, label: 'Sandbox' }, - ], + filters: Object.keys(FOUNDATIONS).map((f: string) => { + return { name: f, label: FOUNDATIONS[f as Foundation]!.name }; + }), }, { name: FilterKind.Rating, @@ -94,6 +98,35 @@ export const FILTERS: FiltersSection[] = [ }, ]; +export const MATURITY_FILTERS: MaturityFilters = { + [Foundation.cdf]: { + name: FilterKind.Maturity, + title: 'Maturity level', + filters: [ + { name: Maturity.graduated, label: 'Graduated' }, + { name: Maturity.incubating, label: 'Incubating' }, + ], + }, + [Foundation.cncf]: { + name: FilterKind.Maturity, + title: 'Maturity level', + filters: [ + { name: Maturity.graduated, label: 'Graduated' }, + { name: Maturity.incubating, label: 'Incubating' }, + { name: Maturity.sandbox, label: 'Sandbox' }, + ], + }, + [Foundation.lfaidata]: { + name: FilterKind.Maturity, + title: 'Maturity level', + filters: [ + { name: Maturity.graduated, label: 'Graduated' }, + { name: Maturity.incubating, label: 'Incubating' }, + { name: Maturity.sandbox, label: 'Sandbox' }, + ], + }, +}; + export const SORT_OPTIONS: SortOption[] = [ { label: 'Alphabetically (A-Z)', @@ -505,18 +538,6 @@ export const REPORT_OPTIONS: ReportOptionInfo = { }, }; -export const FOUNDATIONS: FoundationInfo = { - [Foundation.cdf]: { - name: 'CDF', - }, - [Foundation.cncf]: { - name: 'CNCF', - }, - [Foundation.lfaidata]: { - name: 'LF AI & Data', - }, -}; - export type FoundationInfo = { [key in Foundation]?: { name: string; diff --git a/web/src/layout/search/__snapshots__/index.test.tsx.snap b/web/src/layout/search/__snapshots__/index.test.tsx.snap index ca0a051b..57c9973f 100644 --- a/web/src/layout/search/__snapshots__/index.test.tsx.snap +++ b/web/src/layout/search/__snapshots__/index.test.tsx.snap @@ -191,110 +191,6 @@ exports[`Project detail index creates snapshot 1`] = ` -
- - Maturity level - -
-
-
- - -
-
- - -
-
- - -
-
@@ -2339,110 +2235,6 @@ exports[`Project detail index creates snapshot 1`] = `
-
- - Maturity level - -
-
-
- - -
-
- - -
-
- - -
-
diff --git a/web/src/layout/search/filters/Section.test.tsx b/web/src/layout/search/filters/Section.test.tsx index bb6af981..fb4f9d97 100644 --- a/web/src/layout/search/filters/Section.test.tsx +++ b/web/src/layout/search/filters/Section.test.tsx @@ -1,20 +1,36 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { Maturity } from 'clo-ui'; -import { FilterKind } from '../../../types'; +import { FilterKind, Rating } from '../../../types'; import Section from './Section'; const mockOnChange = jest.fn(); const defaultProps = { section: { - name: FilterKind.Maturity, - title: 'Maturity level', + name: FilterKind.Rating, + title: 'Rating', filters: [ - { name: Maturity.graduated, label: 'Graduated' }, - { name: Maturity.incubating, label: 'Incubating' }, - { name: Maturity.sandbox, label: 'Sandbox' }, + { + name: Rating.A, + label: 'A', + legend: '[75-100]', + }, + { + name: Rating.B, + label: 'B', + legend: '[50-74]', + }, + { + name: Rating.C, + label: 'C', + legend: '[25-49]', + }, + { + name: Rating.D, + label: 'D', + legend: '[0-24]', + }, ], }, activeFilters: [], @@ -37,43 +53,44 @@ describe('Section', () => { it('renders Section', () => { render(
); - expect(screen.getByText('Maturity level')).toBeInTheDocument(); - expect(screen.getByRole('checkbox', { name: 'Graduated' })).toBeInTheDocument(); - expect(screen.getByRole('checkbox', { name: 'Incubating' })).toBeInTheDocument(); - expect(screen.getByRole('checkbox', { name: 'Sandbox' })).toBeInTheDocument(); + expect(screen.getByText('Rating')).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: 'A [75-100]' })).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: 'B [50-74]' })).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: 'C [25-49]' })).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: 'D [0-24]' })).toBeInTheDocument(); }); it('renders Section with selected options', () => { - render(
); + render(
); - expect(screen.getByRole('checkbox', { name: 'Incubating' })).toBeChecked(); - expect(screen.getByRole('checkbox', { name: 'Sandbox' })).toBeChecked(); + expect(screen.getByRole('checkbox', { name: 'A [75-100]' })).toBeChecked(); + expect(screen.getByRole('checkbox', { name: 'B [50-74]' })).toBeChecked(); }); it('calls onChange to click filter', async () => { render(
); - const check = screen.getByRole('checkbox', { name: 'Incubating' }); + const check = screen.getByRole('checkbox', { name: 'B [50-74]' }); expect(check).not.toBeChecked(); await userEvent.click(check); expect(mockOnChange).toHaveBeenCalledTimes(1); - expect(mockOnChange).toHaveBeenCalledWith('maturity', 'incubating', true); + expect(mockOnChange).toHaveBeenCalledWith('rating', 'b', true); }); it('calls onChange to click selected filter', async () => { - render(
); + render(
); - const check = screen.getByRole('checkbox', { name: 'Graduated' }); + const check = screen.getByRole('checkbox', { name: 'B [50-74]' }); expect(check).toBeChecked(); await userEvent.click(check); expect(mockOnChange).toHaveBeenCalledTimes(1); - expect(mockOnChange).toHaveBeenCalledWith('maturity', 'graduated', false); + expect(mockOnChange).toHaveBeenCalledWith('rating', 'b', false); }); }); }); diff --git a/web/src/layout/search/filters/__snapshots__/Section.test.tsx.snap b/web/src/layout/search/filters/__snapshots__/Section.test.tsx.snap index 5e8c97fe..3dda1b00 100644 --- a/web/src/layout/search/filters/__snapshots__/Section.test.tsx.snap +++ b/web/src/layout/search/filters/__snapshots__/Section.test.tsx.snap @@ -6,7 +6,7 @@ exports[`Section creates snapshot 1`] = ` class="fw-bold text-uppercase text-primary categoryTitle" > - Maturity level + Rating
+
+
+ +
@@ -49,16 +90,16 @@ exports[`Section creates snapshot 1`] = ` @@ -80,16 +126,16 @@ exports[`Section creates snapshot 1`] = ` diff --git a/web/src/layout/search/filters/__snapshots__/index.test.tsx.snap b/web/src/layout/search/filters/__snapshots__/index.test.tsx.snap index 488633f6..951cb0f6 100644 --- a/web/src/layout/search/filters/__snapshots__/index.test.tsx.snap +++ b/web/src/layout/search/filters/__snapshots__/index.test.tsx.snap @@ -115,110 +115,6 @@ exports[`Filters creates snapshot 1`] = ` -
- - Maturity level - -
-
-
- - -
-
- - -
-
- - -
-
diff --git a/web/src/layout/search/filters/index.test.tsx b/web/src/layout/search/filters/index.test.tsx index 15514421..f378480e 100644 --- a/web/src/layout/search/filters/index.test.tsx +++ b/web/src/layout/search/filters/index.test.tsx @@ -47,11 +47,6 @@ describe('Filters', () => { expect(screen.getByRole('checkbox', { name: 'CNCF' })).toBeInTheDocument(); expect(screen.getByRole('checkbox', { name: 'LF AI & Data' })).toBeInTheDocument(); - expect(screen.getByText('Maturity level')).toBeInTheDocument(); - expect(screen.getByRole('checkbox', { name: 'Graduated' })).toBeInTheDocument(); - expect(screen.getByRole('checkbox', { name: 'Incubating' })).toBeInTheDocument(); - expect(screen.getByRole('checkbox', { name: 'Sandbox' })).toBeInTheDocument(); - expect(screen.getByText('Rating')).toBeInTheDocument(); expect(screen.getByRole('checkbox', { name: 'A [75-100]' })).toBeInTheDocument(); expect(screen.getByRole('checkbox', { name: 'B [50-74]' })).toBeInTheDocument(); @@ -67,10 +62,24 @@ describe('Filters', () => { expect(screen.getByRole('button', { name: 'Open checks modal' })).toHaveTextContent('Add checks filters'); }); + it('renders Filters', () => { + render(); + + expect(screen.getByText('Filters')).toBeInTheDocument(); + + expect(screen.getByText('Foundation')).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: 'CNCF' })).toBeChecked(); + + expect(screen.getByText('Maturity level')).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: 'Graduated' })).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: 'Incubating' })).toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: 'Sandbox' })).toBeInTheDocument(); + }); + it('renders Filters with selected options', () => { - render(); + render(); - expect(screen.getByRole('checkbox', { name: 'Sandbox' })).toBeChecked(); + expect(screen.getByRole('checkbox', { name: 'CNCF' })).toBeChecked(); expect(screen.getByRole('checkbox', { name: 'A [75-100]' })).toBeChecked(); expect(screen.getByRole('checkbox', { name: 'B [50-74]' })).toBeChecked(); }); @@ -78,14 +87,14 @@ describe('Filters', () => { it('calls onChange to click filter', async () => { render(); - const check = screen.getByRole('checkbox', { name: 'Sandbox' }); + const check = screen.getByRole('checkbox', { name: 'A [75-100]' }); expect(check).not.toBeChecked(); await userEvent.click(check); expect(mockOnChange).toHaveBeenCalledTimes(1); - expect(mockOnChange).toHaveBeenCalledWith('maturity', 'sandbox', true); + expect(mockOnChange).toHaveBeenCalledWith('rating', 'a', true); }); }); }); diff --git a/web/src/layout/search/filters/index.tsx b/web/src/layout/search/filters/index.tsx index 70e89abb..d27121f9 100644 --- a/web/src/layout/search/filters/index.tsx +++ b/web/src/layout/search/filters/index.tsx @@ -1,9 +1,9 @@ -import { DateRangeFilter } from 'clo-ui'; +import { DateRangeFilter, Foundation } from 'clo-ui'; import { isEmpty, isUndefined } from 'lodash'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { IoMdCloseCircleOutline } from 'react-icons/io'; -import { FILTERS } from '../../../data'; +import { FILTERS, MATURITY_FILTERS } from '../../../data'; import { FilterKind, FiltersSection, ReportOption } from '../../../types'; import Checks from './checks'; import Section from './Section'; @@ -26,6 +26,19 @@ interface Props { } const Filters = (props: Props) => { + const [selectedFoundation, setSelectedFoundation] = useState(null); + + useEffect(() => { + if ( + !isUndefined(props.activeFilters[FilterKind.Foundation]) && + props.activeFilters[FilterKind.Foundation].length === 1 + ) { + setSelectedFoundation(props.activeFilters[FilterKind.Foundation][0] as Foundation); + } else { + setSelectedFoundation(null); + } + }, [props.activeFilters]); + return ( <> {props.visibleTitle && ( @@ -43,16 +56,28 @@ const Filters = (props: Props) => {
)} - {FILTERS.map((section: FiltersSection) => ( - -
- - ))} + {FILTERS.map((section: FiltersSection) => { + return ( + +
+ {section.name === FilterKind.Foundation && + selectedFoundation && + !isUndefined(MATURITY_FILTERS[selectedFoundation]) && ( +
+ )} + + ); + })}
{ const onFiltersChange = (name: string, value: string, checked: boolean): void => { const currentFilters = filters || {}; + let additionalFilters = {}; let newFilters = isUndefined(currentFilters[name]) ? [] : currentFilters[name].slice(); if (checked) { newFilters.push(value); + // Remove selected maturity levels when selected foundations is different to only one + if (name === FilterKind.Foundation && newFilters.length !== 1) { + additionalFilters = { [FilterKind.Maturity]: [] }; + } } else { newFilters = newFilters.filter((el) => el !== value); } updateCurrentPage({ - filters: { ...currentFilters, [name]: newFilters }, + filters: { ...currentFilters, [name]: newFilters, ...additionalFilters }, }); }; diff --git a/web/src/layout/stats/__snapshots__/index.test.tsx.snap b/web/src/layout/stats/__snapshots__/index.test.tsx.snap index 007ca93f..338cbe62 100644 --- a/web/src/layout/stats/__snapshots__/index.test.tsx.snap +++ b/web/src/layout/stats/__snapshots__/index.test.tsx.snap @@ -53,11 +53,6 @@ exports[`StatsView creates snapshot 1`] = ` aria-label="Foundation options select" class="form-select rounded-0 cursorPointer foundation select" > -