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"
>
-