diff --git a/database/migrations/functions/packages/search_packages.sql b/database/migrations/functions/packages/search_packages.sql index 363fa02e0..7c6370202 100644 --- a/database/migrations/functions/packages/search_packages.sql +++ b/database/migrations/functions/packages/search_packages.sql @@ -217,6 +217,7 @@ begin order by case when v_sort = 'relevance' then (relevance, stars) end desc, case when v_sort = 'stars' then (stars, relevance) end desc, + case when v_sort = 'last_updated' then (ts, stars) end desc, official desc, verified_publisher desc, name asc diff --git a/database/tests/functions/packages/search_packages.sql b/database/tests/functions/packages/search_packages.sql index ed0c022dd..475d11818 100644 --- a/database/tests/functions/packages/search_packages.sql +++ b/database/tests/functions/packages/search_packages.sql @@ -1,6 +1,6 @@ -- Start transaction and plan tests begin; -select plan(30); +select plan(31); -- Declare some variables \set user1ID '00000000-0000-0000-0000-000000000001' @@ -93,7 +93,7 @@ insert into snapshot ( 'digest-package1-1.0.0', 'readme', 'basic install', - '2020-06-16 11:20:34+02' + '2020-06-16 11:20:35+02' ); insert into snapshot ( package_id, @@ -290,7 +290,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -438,7 +438,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -513,7 +513,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -559,7 +559,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -636,7 +636,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -682,7 +682,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -760,7 +760,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -838,7 +838,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -923,7 +923,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -1027,7 +1027,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -1348,7 +1348,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -1399,7 +1399,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -1478,7 +1478,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -1528,7 +1528,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -1609,7 +1609,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -1727,7 +1727,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -1884,7 +1884,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -1962,7 +1962,7 @@ select results_eq( "app_version": "12.1.0", "license": "Apache-2.0", "production_organizations_count": 1, - "ts": 1592299234, + "ts": 1592299235, "repository": { "repository_id": "00000000-0000-0000-0000-000000000001", "kind": 0, @@ -1983,6 +1983,84 @@ select results_eq( $$, 'Sort: stars TSQueryWeb: kw1 | Packages 2 and 1 expected' ); +select results_eq( + $$ + select data::jsonb, total_count::integer from search_packages('{ + "ts_query_web": "kw1", + "sort": "last_updated", + "deprecated": true + }') + $$, + $$ + values ( + '{ + "packages": [ + { + "package_id": "00000000-0000-0000-0000-000000000001", + "name": "package1", + "normalized_name": "package1", + "category": 1, + "stars": 10, + "official": false, + "cncf": true, + "display_name": "Package 1", + "description": "description", + "logo_image_id": "00000000-0000-0000-0000-000000000001", + "version": "1.0.0", + "app_version": "12.1.0", + "license": "Apache-2.0", + "production_organizations_count": 1, + "ts": 1592299235, + "repository": { + "repository_id": "00000000-0000-0000-0000-000000000001", + "kind": 0, + "name": "repo1", + "display_name": "Repo 1", + "url": "https://repo1.com", + "verified_publisher": true, + "official": true, + "cncf": true, + "scanner_disabled": false, + "user_alias": "user1" + } + }, + { + "package_id": "00000000-0000-0000-0000-000000000002", + "name": "package2", + "normalized_name": "package2", + "stars": 11, + "official": true, + "display_name": "Package 2", + "description": "description", + "logo_image_id": "00000000-0000-0000-0000-000000000002", + "version": "1.0.0", + "app_version": "12.1.0", + "deprecated": true, + "signed": true, + "signatures": ["cosign"], + "all_containers_images_whitelisted": false, + "production_organizations_count": 0, + "ts": 1592299234, + "repository": { + "repository_id": "00000000-0000-0000-0000-000000000002", + "kind": 0, + "name": "repo2", + "display_name": "Repo 2", + "url": "https://repo2.com", + "verified_publisher": false, + "official": false, + "scanner_disabled": false, + "organization_name": "org1", + "organization_display_name": "Organization 1" + } + } + ] + }'::jsonb, + 2 + ) + $$, + 'Sort: last_updated TSQueryWeb: kw1 | Packages 1 and 2 expected' +); -- Finish tests and rollback transaction select * from finish(); diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index fdb5334a7..1d0a28d3b 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -5225,7 +5225,7 @@ components: name: sort schema: type: string - enum: ["relevance", "stars"] + enum: ["relevance", "stars", "last_updated"] example: relevance required: false description: Sort criteria diff --git a/internal/pkg/manager.go b/internal/pkg/manager.go index b7ef5c80c..c79bc50de 100644 --- a/internal/pkg/manager.go +++ b/internal/pkg/manager.go @@ -323,8 +323,8 @@ func (m *Manager) SearchJSON(ctx context.Context, input *hub.SearchPackageInput) if input.Offset < 0 { return nil, fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid offset (o >= 0)") } - if input.Sort != "" && input.Sort != "relevance" && input.Sort != "stars" { - return nil, fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid sort (relevance|stars)") + if input.Sort != "" && input.Sort != "relevance" && input.Sort != "stars" && input.Sort != "last_updated" { + return nil, fmt.Errorf("%w: %s", hub.ErrInvalidInput, "invalid sort (relevance|stars|last_updated)") } for _, alias := range input.Users { if alias == "" { diff --git a/internal/pkg/manager_test.go b/internal/pkg/manager_test.go index 35962c00c..01a899784 100644 --- a/internal/pkg/manager_test.go +++ b/internal/pkg/manager_test.go @@ -1214,7 +1214,7 @@ func TestSearchJSON(t *testing.T) { }, }, { - "invalid sort (relevance|stars)", + "invalid sort (relevance|stars|last_updated)", &hub.SearchPackageInput{ Limit: 10, Sort: "invalid", diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 4ef04072f..523fd4167 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -32,6 +32,7 @@ import { SearchResults, SecurityReport, SecurityReportResult, + SortOption, Stats, Subscription, TestWebhook, @@ -320,7 +321,7 @@ class API_CLASS { public searchPackages(query: SearchQuery, facets: boolean = true): Promise { const q = getURLSearchParams(query); q.set('facets', facets ? 'true' : 'false'); - q.set('sort', query.sort || 'relevance'); + q.set('sort', query.sort || SortOption.Relevance); q.set('limit', query.limit.toString()); q.set('offset', query.offset.toString()); diff --git a/web/src/layout/search/SortOptions.module.css b/web/src/layout/search/SortOptions.module.css index 445745c21..606b2271b 100644 --- a/web/src/layout/search/SortOptions.module.css +++ b/web/src/layout/search/SortOptions.module.css @@ -1,6 +1,6 @@ .select { height: 26px !important; - width: 118px !important; + width: 135px !important; line-height: 1rem !important; padding-left: 0.75rem !important; border-color: var(--color-1-500) !important; diff --git a/web/src/layout/search/SortOptions.tsx b/web/src/layout/search/SortOptions.tsx index d9377f6a7..0b55c56b0 100644 --- a/web/src/layout/search/SortOptions.tsx +++ b/web/src/layout/search/SortOptions.tsx @@ -1,23 +1,23 @@ import { isNull } from 'lodash'; import { ChangeEvent, useEffect, useRef } from 'react'; -import capitalizeFirstLetter from '../../utils/capitalizeFirstLetter'; +import { SortOption } from '../../types'; import styles from './SortOptions.module.css'; interface Props { - activeSort: string; - updateSort: (value: string) => void; + activeSort: SortOption; + updateSort: (value: SortOption) => void; disabled: boolean; } -const DEFAULT_SORT = 'relevance'; -const SORT_OPTS = [DEFAULT_SORT, 'stars']; +const DEFAULT_SORT = SortOption.Relevance; +const SORT_OPTS = Object.values(SortOption); const SortOptions = (props: Props) => { const selectEl = useRef(null); const handleChange = (event: ChangeEvent) => { - props.updateSort(event.target.value); + props.updateSort(event.target.value as SortOption); forceBlur(); }; @@ -27,6 +27,12 @@ const SortOptions = (props: Props) => { } }; + const getSortOptionKey = (value: string): string => { + const index = Object.values(SortOption).indexOf(value as unknown as SortOption); + const key = Object.keys(SortOption)[index]; + return key; + }; + useEffect(() => { if (!SORT_OPTS.includes(props.activeSort)) { props.updateSort(DEFAULT_SORT); @@ -46,7 +52,7 @@ const SortOptions = (props: Props) => { > {SORT_OPTS.map((value: string) => ( ))} ; diff --git a/web/src/layout/search/index.tsx b/web/src/layout/search/index.tsx index 551b20fd6..f16f95a4d 100644 --- a/web/src/layout/search/index.tsx +++ b/web/src/layout/search/index.tsx @@ -18,6 +18,7 @@ import { RepositoryKind, SearchFiltersURL, SearchResults, + SortOption, } from '../../types'; import buildSearchParams from '../../utils/buildSearchParams'; import getSampleQueries from '../../utils/getSampleQueries'; @@ -41,7 +42,7 @@ interface FiltersProp { [key: string]: string[]; } -const DEFAULT_SORT = 'relevance'; +const DEFAULT_SORT = SortOption.Relevance; interface FilterLabel { key: string; @@ -444,7 +445,7 @@ const SearchView = () => { {/* Only display sort options when ts_query_web is defined */} {tsQueryWeb && tsQueryWeb !== '' && ( diff --git a/web/src/types.ts b/web/src/types.ts index 62134579b..8d866a345 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -873,3 +873,9 @@ export interface OutletContext { viewedPackage?: string; setViewedPackage: (value?: string) => void; } + +export enum SortOption { + Relevance = 'relevance', + Stars = 'stars', + 'Last updated' = 'last_updated', +} diff --git a/web/src/utils/prepareQueryString.ts b/web/src/utils/prepareQueryString.ts index 4a4dd72ab..c78142173 100644 --- a/web/src/utils/prepareQueryString.ts +++ b/web/src/utils/prepareQueryString.ts @@ -1,6 +1,6 @@ import { isEmpty, isUndefined } from 'lodash'; -import { BasicQuery, SearchFiltersURL, SearchQuery } from '../types'; +import { BasicQuery, SearchFiltersURL, SearchQuery, SortOption } from '../types'; export const getURLSearchParams = (query: BasicQuery): URLSearchParams => { const q = new URLSearchParams(); @@ -44,7 +44,7 @@ export const prepareAPIQueryString = (query: SearchQuery): string => { export const prepareQueryString = (query: SearchFiltersURL): string => { const q = getURLSearchParams(query); - q.set('sort', query.sort || 'relevance'); + q.set('sort', query.sort || SortOption.Relevance); q.set('page', query.pageNumber.toString()); return `?${q.toString()}`; };