diff --git a/.env.example b/.env.example index ec0143878..71e4738dc 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,11 @@ # automatically name the containers. COMPOSE_PROJECT_NAME="rsd" +# RSD REMOTE NAME +# identify this instance as remote by this name +# it is used as source label in RPC aggregated_software_overview +RSD_REMOTE_NAME=Local RSD + # ---- PUBLIC ENV VARIABLES ------------- # postgresql diff --git a/database/025-rsd-info.sql b/database/025-rsd-info.sql new file mode 100644 index 000000000..e64608499 --- /dev/null +++ b/database/025-rsd-info.sql @@ -0,0 +1,59 @@ +-- SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center) +-- SPDX-FileCopyrightText: 2024 Netherlands eScience Center +-- +-- SPDX-License-Identifier: Apache-2.0 + +-- RSD info table +-- used to obtain RSD name to use for remotes +-- it should provide basic info about rsd instance (eg. endpoints) +CREATE TABLE rsd_info ( + key VARCHAR(100) PRIMARY KEY, + value VARCHAR(250), + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL +); + +CREATE FUNCTION sanitise_insert_rsd_info() RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + NEW.created_at = LOCALTIMESTAMP; + NEW.updated_at = NEW.created_at; + return NEW; +END +$$; + +CREATE TRIGGER sanitise_insert_rsd_info BEFORE INSERT ON + rsd_info FOR EACH ROW EXECUTE PROCEDURE sanitise_insert_rsd_info(); + +CREATE FUNCTION sanitise_update_rsd_info() RETURNS TRIGGER LANGUAGE plpgsql AS +$$ +BEGIN + NEW.created_at = OLD.created_at; + NEW.updated_at = LOCALTIMESTAMP; + return NEW; +END +$$; + +CREATE TRIGGER sanitise_update_rsd_info BEFORE UPDATE ON + rsd_info FOR EACH ROW EXECUTE PROCEDURE sanitise_update_rsd_info(); + +-- Insert remote_name key extracted from env variable RSD_REMOTE_NAME, default value is 'Local RSD' +-- AND basic endpoints info +INSERT INTO rsd_info VALUES + ('remote_name', COALESCE(current_setting('rsd.remote_name',true),'Local RSD')), + ('postgrest_api','/api/v1'), + ('images_api','/images'), + ('swagger','/swagger'), + ('codemeta','/metadata/codemeta') +; + +-- RLS +-- rsd info table +ALTER TABLE rsd_info ENABLE ROW LEVEL SECURITY; +-- anyone can read (SELECT) +CREATE POLICY anyone_can_read ON rsd_info FOR SELECT TO rsd_web_anon, rsd_user + USING (TRUE); +-- rsd_admin has all rights +CREATE POLICY admin_all_rights ON rsd_info TO rsd_admin + USING (TRUE) + WITH CHECK (TRUE); diff --git a/database/124-aggregated-software-views.sql b/database/124-aggregated-software-views.sql index faeb3b3d7..279e19838 100644 --- a/database/124-aggregated-software-views.sql +++ b/database/124-aggregated-software-views.sql @@ -26,7 +26,8 @@ CREATE FUNCTION aggregated_software_overview() RETURNS TABLE ( $$ SELECT software_overview.id, - NULL AS source, + -- use remote_name information for local name + (SELECT value FROM rsd_info WHERE KEY='remote_name') AS source, NULL AS domain, software_overview.slug, software_overview.brand_name, @@ -128,7 +129,8 @@ CREATE FUNCTION aggregated_software_keywords_filter( search_filter TEXT DEFAULT '', keyword_filter CITEXT[] DEFAULT '{}', prog_lang_filter TEXT[] DEFAULT '{}', - license_filter VARCHAR[] DEFAULT '{}' + license_filter VARCHAR[] DEFAULT '{}', + source_filter VARCHAR[] DEFAULT NULL ) RETURNS TABLE ( keyword CITEXT, keyword_cnt INTEGER @@ -145,6 +147,12 @@ WHERE COALESCE(prog_lang, '{}') @> prog_lang_filter AND COALESCE(licenses, '{}') @> license_filter + AND + CASE + WHEN source_filter IS NULL THEN TRUE + ELSE + source = ANY(source_filter) + END GROUP BY keyword ; @@ -156,7 +164,8 @@ CREATE FUNCTION aggregated_software_languages_filter( search_filter TEXT DEFAULT '', keyword_filter CITEXT[] DEFAULT '{}', prog_lang_filter TEXT[] DEFAULT '{}', - license_filter VARCHAR[] DEFAULT '{}' + license_filter VARCHAR[] DEFAULT '{}', + source_filter VARCHAR[] DEFAULT NULL ) RETURNS TABLE ( prog_language TEXT, prog_language_cnt INTEGER @@ -173,6 +182,12 @@ WHERE COALESCE(prog_lang, '{}') @> prog_lang_filter AND COALESCE(licenses, '{}') @> license_filter + AND + CASE + WHEN source_filter IS NULL THEN TRUE + ELSE + source = ANY(source_filter) + END GROUP BY prog_language ; @@ -184,7 +199,8 @@ CREATE FUNCTION aggregated_software_licenses_filter( search_filter TEXT DEFAULT '', keyword_filter CITEXT[] DEFAULT '{}', prog_lang_filter TEXT[] DEFAULT '{}', - license_filter VARCHAR[] DEFAULT '{}' + license_filter VARCHAR[] DEFAULT '{}', + source_filter VARCHAR[] DEFAULT NULL ) RETURNS TABLE ( license VARCHAR, license_cnt INTEGER @@ -201,7 +217,48 @@ WHERE COALESCE(prog_lang, '{}') @> prog_lang_filter AND COALESCE(licenses, '{}') @> license_filter + AND + CASE + WHEN source_filter IS NULL THEN TRUE + ELSE + source = ANY(source_filter) + END GROUP BY license ; $$; + +-- REACTIVE SOURCE FILTER WITH COUNTS FOR SOFTWARE +-- DEPENDS ON: aggregated_software_search +CREATE FUNCTION aggregated_software_sources_filter( + search_filter TEXT DEFAULT '', + keyword_filter CITEXT[] DEFAULT '{}', + prog_lang_filter TEXT[] DEFAULT '{}', + license_filter VARCHAR[] DEFAULT '{}', + source_filter VARCHAR[] DEFAULT NULL +) RETURNS TABLE ( + source VARCHAR, + source_cnt INTEGER +) LANGUAGE sql STABLE AS +$$ +SELECT + source AS source, + COUNT(id) AS source_cnt +FROM + aggregated_software_search(search_filter) +WHERE + COALESCE(keywords, '{}') @> keyword_filter + AND + COALESCE(prog_lang, '{}') @> prog_lang_filter + AND + COALESCE(licenses, '{}') @> license_filter + AND + CASE + WHEN source_filter IS NULL THEN TRUE + ELSE + source = ANY(source_filter) + END +GROUP BY + source +; +$$; diff --git a/docker-compose.yml b/docker-compose.yml index bd66a0a04..00432fd52 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,6 +26,9 @@ services: - POSTGRES_USER - POSTGRES_PASSWORD - POSTGRES_AUTHENTICATOR_PASSWORD + - RSD_REMOTE_NAME + # insert remote_name into postgres settings, this value is used in initial SQL scripts + command: postgres -D /var/lib/postgresql/data -c 'rsd.remote_name=${RSD_REMOTE_NAME}' volumes: # persist data in named docker volume # to remove use: docker compose down --volumes diff --git a/frontend/__tests__/SoftwareOverview.test.tsx b/frontend/__tests__/SoftwareOverview.test.tsx index 51af8f507..f7045f429 100644 --- a/frontend/__tests__/SoftwareOverview.test.tsx +++ b/frontend/__tests__/SoftwareOverview.test.tsx @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2022 - 2023 Dusan Mijatovic (dv4all) // SPDX-FileCopyrightText: 2022 - 2023 dv4all -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) -// SPDX-FileCopyrightText: 2023 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center // // SPDX-License-Identifier: Apache-2.0 @@ -23,7 +23,8 @@ const mockProps = { keywords:null, prog_lang: null, licenses:null, - order:null, + sources: null, + order: '', page: 1, rows: 12, count: 408, @@ -31,8 +32,10 @@ const mockProps = { keywordsList: mockData.keywordsList, languagesList: mockData.languagesList, licensesList: mockData.licensesList, + sourcesList: [], software: mockData.software as any, - highlights: mockData.highlights as any + highlights: mockData.highlights as any, + hasRemotes: false } describe('pages/software/index.tsx', () => { @@ -57,7 +60,7 @@ describe('pages/software/index.tsx', () => { ) - const carousel = screen.getByTestId('highlights-carousel') + screen.getByTestId('highlights-carousel') const cards = screen.getAllByTestId('highlights-card') expect(cards.length).toEqual(mockData.highlights.length) }) @@ -71,7 +74,7 @@ describe('pages/software/index.tsx', () => { // get reference to filter panel const panel = screen.getByTestId('filters-panel') // find order by testid - const order = within(panel).getByTestId('filters-order-by') + within(panel).getByTestId('filters-order-by') // should have 3 filters const filters = within(panel).getAllByRole('combobox') expect(filters.length).toEqual(3) diff --git a/frontend/components/admin/remote-rsd/useRemoteRsd.tsx b/frontend/components/admin/remote-rsd/useRemoteRsd.tsx index 2af77362b..0d68b99c0 100644 --- a/frontend/components/admin/remote-rsd/useRemoteRsd.tsx +++ b/frontend/components/admin/remote-rsd/useRemoteRsd.tsx @@ -36,9 +36,7 @@ export default function useRemoteRsd() { },[token, searchFor, page, rows]) useEffect(()=>{ - if(token){ - loadRemoteRsd() - } + loadRemoteRsd() // we do not include setCount in order to avoid loop // eslint-disable-next-line react-hooks/exhaustive-deps },[token, searchFor, page, rows]) diff --git a/frontend/components/filter/RsdSourceFilter.tsx b/frontend/components/filter/RsdSourceFilter.tsx new file mode 100644 index 000000000..7e6e036ea --- /dev/null +++ b/frontend/components/filter/RsdSourceFilter.tsx @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center +// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) +// SPDX-FileCopyrightText: 2023 dv4all +// +// SPDX-License-Identifier: Apache-2.0 + +import {useEffect, useState} from 'react' + +import Autocomplete from '@mui/material/Autocomplete' +import TextField from '@mui/material/TextField' +import FilterTitle from './FilterTitle' +import FilterOption from './FilterOption' + +export type SourcesFilterOption = { + source: string + source_cnt: number +} + +type RsdSourceFilterProps = Readonly<{ + sources: string[], + sourcesList: SourcesFilterOption[], + handleQueryChange: (key: string, value: string | string[]) => void + title?: string +}> + +export default function RsdSourceFilter({sources, sourcesList,handleQueryChange,title='Host RSD'}: RsdSourceFilterProps) { + const [selected, setSelected] = useState([]) + const [options, setOptions] = useState(sourcesList) + + useEffect(() => { + if (sources.length > 0 && sourcesList.length > 0) { + const selected = sourcesList.filter(option => { + return sources.includes(option.source) + }) + setSelected(selected) + } else { + setSelected([]) + } + setOptions(sourcesList) + },[sources,sourcesList]) + + return ( +
+ + option.source} + isOptionEqualToValue={(option, value) => { + return option.source === value.source + }} + defaultValue={[]} + filterSelectedOptions + renderOption={(props, option) => ( + + )} + renderInput={(params) => ( + + )} + onChange={(_, newValue) => { + // extract values into string[] for url query + const queryFilter = newValue.map(item => item.source) + // update query url + handleQueryChange('sources', queryFilter) + }} + /> +
+ ) +} diff --git a/frontend/components/software/overview/SoftwareOverviewContent.tsx b/frontend/components/software/overview/SoftwareOverviewContent.tsx index f360be011..f88a745c0 100644 --- a/frontend/components/software/overview/SoftwareOverviewContent.tsx +++ b/frontend/components/software/overview/SoftwareOverviewContent.tsx @@ -20,12 +20,13 @@ import SoftwareListItemContent from './list/SoftwareListItemContent' import OverviewListItem from './list/OverviewListItem' import {getItemKey, getPageUrl} from './useSoftwareOverviewProps' -type SoftwareOverviewContentProps = { +type SoftwareOverviewContentProps = Readonly<{ layout: LayoutType software: SoftwareOverviewItemProps[] -} + hasRemotes?: boolean +}> -export default function SoftwareOverviewContent({layout, software}: SoftwareOverviewContentProps) { +export default function SoftwareOverviewContent({layout, software, hasRemotes}: SoftwareOverviewContentProps) { if (!software || software.length === 0) { return @@ -37,6 +38,10 @@ export default function SoftwareOverviewContent({layout, software}: SoftwareOver {software.map((item) => { const cardKey = getItemKey({id:item.id,domain:item.domain}) + // remove source if remotes are not present + if (hasRemotes===false && item.source!==null){ + item.source = null + } return (
@@ -53,6 +58,10 @@ export default function SoftwareOverviewContent({layout, software}: SoftwareOver {software.map(item => { const listKey = getItemKey({id:item.id,domain:item.domain}) const pageUrl = getPageUrl({domain:item.domain,slug:item.slug}) + // remove source if remotes are not present + if (hasRemotes===false && item.source!==null){ + item.source = null + } return ( {software.map((item) => { const cardKey = getItemKey({id:item.id,domain:item.domain}) + // remove source if remotes are not present + if (hasRemotes===false && item.source!==null){ + item.source = null + } return })} diff --git a/frontend/components/software/overview/filters/SoftwareFiltersModal.tsx b/frontend/components/software/overview/filters/SoftwareFiltersModal.tsx index 8c405fe85..4d4f21fd1 100644 --- a/frontend/components/software/overview/filters/SoftwareFiltersModal.tsx +++ b/frontend/components/software/overview/filters/SoftwareFiltersModal.tsx @@ -1,6 +1,6 @@ -// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2024 Dusan Mijatovic (Netherlands eScience Center) +// SPDX-FileCopyrightText: 2023 - 2024 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) -// SPDX-FileCopyrightText: 2023 Netherlands eScience Center // SPDX-FileCopyrightText: 2023 dv4all // // SPDX-License-Identifier: Apache-2.0 @@ -14,6 +14,7 @@ import Button from '@mui/material/Button' import {KeywordFilterOption} from '~/components/filter/KeywordsFilter' import {LanguagesFilterOption} from '~/components/filter/ProgrammingLanguagesFilter' import {LicensesFilterOption} from '~/components/filter/LicensesFilter' +import {SourcesFilterOption} from '~/components/filter/RsdSourceFilter' import SoftwareFilters from './index' type SoftwareFiltersModalProps = { @@ -24,6 +25,9 @@ type SoftwareFiltersModalProps = { languagesList: LanguagesFilterOption[], licenses?: string[], licensesList: LicensesFilterOption[], + sources?: string [] + sourcesList?: SourcesFilterOption[] + hasRemotes?: boolean order: string, filterCnt: number, setModal:(open:boolean)=>void @@ -33,7 +37,8 @@ export default function SoftwareFiltersModal({ open, keywords, keywordsList, prog_lang, languagesList, licenses, licensesList, - filterCnt, order, + sources, sourcesList, + hasRemotes, filterCnt, order, setModal }:SoftwareFiltersModalProps) { const smallScreen = useMediaQuery('(max-width:640px)') @@ -62,8 +67,11 @@ export default function SoftwareFiltersModal({ languagesList={languagesList} licenses={licenses ?? []} licensesList={licensesList} + sources={sources ?? []} + sourcesList={sourcesList} orderBy={order ?? ''} filterCnt={filterCnt} + hasRemotes={hasRemotes} />
diff --git a/frontend/components/software/overview/filters/index.tsx b/frontend/components/software/overview/filters/index.tsx index 5c22e9a76..8fe60813d 100644 --- a/frontend/components/software/overview/filters/index.tsx +++ b/frontend/components/software/overview/filters/index.tsx @@ -13,6 +13,7 @@ import FilterHeader from '~/components/filter/FilterHeader' import KeywordsFilter, {KeywordFilterOption} from '~/components/filter/KeywordsFilter' import ProgrammingLanguagesFilter, {LanguagesFilterOption} from '~/components/filter/ProgrammingLanguagesFilter' import LicensesFilter, {LicensesFilterOption} from '~/components/filter/LicensesFilter' +import RsdSourceFilter, {SourcesFilterOption} from '~/components/filter/RsdSourceFilter' import useSoftwareOverviewParams from '../useSoftwareOverviewParams' import OrderSoftwareBy, {OrderHighlightsBy} from './OrderSoftwareBy' @@ -26,6 +27,9 @@ type SoftwareFilterProps = { orderBy: string, filterCnt: number, highlightsOnly?: boolean + sources?: string [] + sourcesList?: SourcesFilterOption[] + hasRemotes?: boolean } export default function SoftwareFilters({ @@ -35,12 +39,20 @@ export default function SoftwareFilters({ languagesList, licenses, licensesList, + sources, + sourcesList, filterCnt, orderBy, - highlightsOnly = false + highlightsOnly = false, + hasRemotes = false }:SoftwareFilterProps) { const {resetFilters,handleQueryChange} = useSoftwareOverviewParams() + // console.group('SoftwareFilters') + // console.log('sources...', sources) + // console.log('sourcesList...', sourcesList) + // console.groupEnd() + function clearDisabled() { if (filterCnt && filterCnt > 0) return false return true @@ -54,8 +66,11 @@ export default function SoftwareFilters({ resetFilters={resetFilters} /> {/* Order by */} - {highlightsOnly && } - {!highlightsOnly && } + {highlightsOnly ? + + : + + } {/* Keywords */}
+ {/* RSD hosts list only if remotes are defined */} + {hasRemotes ? + + : null + } ) } diff --git a/frontend/components/software/overview/filters/softwareFiltersApi.ts b/frontend/components/software/overview/filters/softwareFiltersApi.ts index 4022a3071..a1aa6cc86 100644 --- a/frontend/components/software/overview/filters/softwareFiltersApi.ts +++ b/frontend/components/software/overview/filters/softwareFiltersApi.ts @@ -10,6 +10,7 @@ import {KeywordFilterOption} from '~/components/filter/KeywordsFilter' import {LicensesFilterOption} from '~/components/filter/LicensesFilter' import {LanguagesFilterOption} from '~/components/filter/ProgrammingLanguagesFilter' +import {SourcesFilterOption} from '~/components/filter/RsdSourceFilter' import {createJsonHeaders, getBaseUrl} from '~/utils/fetchHelpers' import logger from '~/utils/logger' @@ -18,6 +19,7 @@ type SoftwareFilterProps = { keywords?: string[] | null prog_lang?: string[] | null licenses?: string[] | null + sources?: string[] | null } type GenericSoftwareFilterProps = SoftwareFilterProps & { @@ -29,9 +31,10 @@ type SoftwareFilterApiProps = { keyword_filter?: string[] prog_lang_filter?: string[] license_filter?: string[] + source_filter?: string[] } -export function buildSoftwareFilter({search, keywords, prog_lang, licenses}: SoftwareFilterProps) { +export function buildSoftwareFilter({search, keywords, prog_lang, licenses, sources}: SoftwareFilterProps) { const filter: SoftwareFilterApiProps={} if (search) { filter['search_filter'] = search @@ -45,15 +48,18 @@ export function buildSoftwareFilter({search, keywords, prog_lang, licenses}: Sof if (licenses) { filter['license_filter'] = licenses } + if (sources) { + filter['source_filter'] = sources + } // console.group('buildSoftwareFilter') // console.log('filter...', filter) // console.groupEnd() return filter } -export async function softwareKeywordsFilter({search, keywords, prog_lang, licenses}: SoftwareFilterProps) { +export async function softwareKeywordsFilter({search, keywords, prog_lang, licenses, sources}: SoftwareFilterProps) { const rpc = 'aggregated_software_keywords_filter' - return genericSoftwareKeywordsFilter({search, keywords, prog_lang, licenses, rpc}) + return genericSoftwareKeywordsFilter({search, keywords, prog_lang, licenses, sources,rpc}) } export async function highlightKeywordsFilter({search, keywords, prog_lang, licenses}: SoftwareFilterProps) { @@ -61,7 +67,7 @@ export async function highlightKeywordsFilter({search, keywords, prog_lang, lice return genericSoftwareKeywordsFilter({search, keywords, prog_lang, licenses, rpc}) } -export async function genericSoftwareKeywordsFilter({search, keywords, prog_lang, licenses, rpc}: GenericSoftwareFilterProps) { +export async function genericSoftwareKeywordsFilter({search, keywords, prog_lang, licenses, sources, rpc}: GenericSoftwareFilterProps) { try { const query =`rpc/${rpc}?order=keyword` const url = `${getBaseUrl()}/${query}` @@ -69,7 +75,8 @@ export async function genericSoftwareKeywordsFilter({search, keywords, prog_lang search, keywords, prog_lang, - licenses + licenses, + sources }) // console.group('softwareKeywordsFilter') @@ -97,9 +104,9 @@ export async function genericSoftwareKeywordsFilter({search, keywords, prog_lang } } -export async function softwareLanguagesFilter({search, keywords, prog_lang, licenses}: SoftwareFilterProps) { +export async function softwareLanguagesFilter({search, keywords, prog_lang, licenses, sources}: SoftwareFilterProps) { const rpc = 'aggregated_software_languages_filter' - return genericSoftwareLanguagesFilter({search, keywords, prog_lang, licenses, rpc}) + return genericSoftwareLanguagesFilter({search, keywords, prog_lang, licenses, sources, rpc}) } export async function highlightLanguagesFilter({search, keywords, prog_lang, licenses}: SoftwareFilterProps) { @@ -107,7 +114,7 @@ export async function highlightLanguagesFilter({search, keywords, prog_lang, lic return genericSoftwareLanguagesFilter({search, keywords, prog_lang, licenses, rpc}) } -export async function genericSoftwareLanguagesFilter({search, keywords, prog_lang, licenses, rpc}: GenericSoftwareFilterProps) { +export async function genericSoftwareLanguagesFilter({search, keywords, prog_lang, licenses, sources, rpc}: GenericSoftwareFilterProps) { try { const query = `rpc/${rpc}?order=prog_language` const url = `${getBaseUrl()}/${query}` @@ -115,7 +122,8 @@ export async function genericSoftwareLanguagesFilter({search, keywords, prog_lan search, keywords, prog_lang, - licenses + licenses, + sources }) // console.group('softwareLanguagesFilter') @@ -143,9 +151,9 @@ export async function genericSoftwareLanguagesFilter({search, keywords, prog_lan } } -export async function softwareLicensesFilter({search, keywords, prog_lang, licenses}: SoftwareFilterProps) { +export async function softwareLicensesFilter({search, keywords, prog_lang, licenses, sources}: SoftwareFilterProps) { const rpc = 'aggregated_software_licenses_filter' - return genericSoftwareLicensesFilter({search, keywords, prog_lang, licenses, rpc}) + return genericSoftwareLicensesFilter({search, keywords, prog_lang, licenses, sources, rpc}) } @@ -154,7 +162,7 @@ export async function highlightLicensesFilter({search, keywords, prog_lang, lice return genericSoftwareLicensesFilter({search, keywords, prog_lang, licenses, rpc}) } -export async function genericSoftwareLicensesFilter({search, keywords, prog_lang, licenses, rpc}: GenericSoftwareFilterProps) { +export async function genericSoftwareLicensesFilter({search, keywords, prog_lang, licenses, sources, rpc}: GenericSoftwareFilterProps) { try { const query = `rpc/${rpc}?order=license` const url = `${getBaseUrl()}/${query}` @@ -162,7 +170,8 @@ export async function genericSoftwareLicensesFilter({search, keywords, prog_lang search, keywords, prog_lang, - licenses + licenses, + sources }) const resp = await fetch(url, { @@ -184,3 +193,35 @@ export async function genericSoftwareLicensesFilter({search, keywords, prog_lang return [] } } + +export async function softwareSourcesFilter({search, keywords, prog_lang, licenses, sources}: SoftwareFilterProps) { + try { + const query = 'aggregated_software_sources_filter?order=source' + const url = `${getBaseUrl()}/rpc/${query}` + const filter = buildSoftwareFilter({ + search, + keywords, + prog_lang, + licenses, + sources + }) + + const resp = await fetch(url, { + method: 'POST', + headers: createJsonHeaders(), + body: JSON.stringify(filter) + }) + + if (resp.status === 200) { + const json: SourcesFilterOption[] = await resp.json() + return json + } + + logger(`softwareSourcesFilter: ${resp.status} ${resp.statusText}`, 'warn') + return [] + + } catch (e: any) { + logger(`softwareSourcesFilter: ${e?.message}`, 'error') + return [] + } +} diff --git a/frontend/pages/admin/remote-rsd.tsx b/frontend/pages/admin/remote-rsd.tsx index 3ac37ee8e..99ae1f02f 100644 --- a/frontend/pages/admin/remote-rsd.tsx +++ b/frontend/pages/admin/remote-rsd.tsx @@ -52,14 +52,7 @@ export default function AdminRemoteRsdPage() { // try{ // const {req} = context // const token = req?.cookies['rsd_token'] - -// // get links to all pages server side -// const resp = await getCommunities({ -// page: 0, -// rows: 12, -// token: token ?? '' -// }) - +// // return { // // passed to the page component as props // props: { diff --git a/frontend/pages/software/index.tsx b/frontend/pages/software/index.tsx index 0155ff041..4e4611ef0 100644 --- a/frontend/pages/software/index.tsx +++ b/frontend/pages/software/index.tsx @@ -20,6 +20,7 @@ import {getBaseUrl} from '~/utils/fetchHelpers' import {softwareListUrl} from '~/utils/postgrestUrl' import {getSoftwareList} from '~/utils/getSoftware' import {ssrSoftwareParams} from '~/utils/extractQueryParam' +import {getUserSettings} from '~/utils/userSettings' import {SoftwareOverviewItemProps} from '~/types/SoftwareTypes' import MainContent from '~/components/layout/MainContent' import PageBackground from '~/components/layout/PageBackground' @@ -42,14 +43,16 @@ import SoftwareOverviewContent from '~/components/software/overview/SoftwareOver import SoftwareFilters from '~/components/software/overview/filters/index' import { softwareKeywordsFilter, softwareLanguagesFilter, - softwareLicensesFilter + softwareLicensesFilter, + softwareSourcesFilter } from '~/components/software/overview/filters/softwareFiltersApi' import SoftwareFiltersModal from '~/components/software/overview/filters/SoftwareFiltersModal' -import {getUserSettings} from '~/utils/userSettings' import {softwareOrderOptions} from '~/components/software/overview/filters/OrderSoftwareBy' import {LayoutType} from '~/components/software/overview/search/ViewToggleGroup' import {getRsdSettings} from '~/config/getSettingsServerSide' import {useUserSettings} from '~/config/UserSettingsContext' +import {SourcesFilterOption} from '~/components/filter/RsdSourceFilter' +import {getRemoteRsd} from '~/components/admin/remote-rsd/apiRemoteRsd' type SoftwareOverviewProps = { search?: string | null @@ -59,13 +62,16 @@ type SoftwareOverviewProps = { languagesList: LanguagesFilterOption[], licenses?: string[] | null, licensesList: LicensesFilterOption[], + sources?: string[] | null, + sourcesList: SourcesFilterOption[] order: string, page: number, rows: number, count: number, layout: LayoutType, software: SoftwareOverviewItemProps[], - highlights: SoftwareHighlight[] + highlights: SoftwareHighlight[], + hasRemotes: boolean } const pageTitle = `Software | ${app.title}` @@ -74,10 +80,11 @@ const pageDesc = 'The list of research software registered in the Research Softw export default function SoftwareOverviewPage({ search, keywords, prog_lang, licenses, - order, page, rows, - count, layout, + sources, order, page, + rows, count, layout, keywordsList, languagesList, - licensesList, software, highlights + licensesList, sourcesList, + software, highlights, hasRemotes }: SoftwareOverviewProps) { const smallScreen = useMediaQuery('(max-width:640px)') const {createUrl} = useSoftwareOverviewParams() @@ -94,6 +101,7 @@ export default function SoftwareOverviewPage({ // console.log('keywords...', keywords) // console.log('prog_lang...', prog_lang) // console.log('licenses...', licenses) + // console.log('sources...', sources) // console.log('order...', order) // console.log('page...', page) // console.log('rows...', rows) @@ -102,8 +110,10 @@ export default function SoftwareOverviewPage({ // console.log('keywordsList...', keywordsList) // console.log('languagesList...', languagesList) // console.log('licensesList...', licensesList) + // console.log('sourcesList...', sourcesList) // console.log('software...', software) // console.log('highlights...', highlights) + // console.log('hasRemotes...', hasRemotes) // console.groupEnd() function getFilterCount() { @@ -112,6 +122,7 @@ export default function SoftwareOverviewPage({ if (prog_lang) count++ if (licenses) count++ if (search) count++ + if (sources) count++ return count } @@ -159,8 +170,11 @@ export default function SoftwareOverviewPage({ languagesList={languagesList} licenses={licenses ?? []} licensesList={licensesList} + sources={sources ?? []} + sourcesList={sourcesList} orderBy={order} filterCnt={filterCnt} + hasRemotes={hasRemotes} /> } @@ -180,6 +194,7 @@ export default function SoftwareOverviewPage({ {/* Pagination */}
@@ -220,9 +235,12 @@ export default function SoftwareOverviewPage({ languagesList={languagesList} licenses={licenses ?? []} licensesList={licensesList} + sources={sources ?? []} + sourcesList={sourcesList} order={order ?? ''} filterCnt={filterCnt} setModal={setModal} + hasRemotes={hasRemotes} /> } @@ -234,7 +252,7 @@ export default function SoftwareOverviewPage({ export async function getServerSideProps(context: GetServerSidePropsContext) { let offset=0 // extract params from page-query - const {search, keywords, prog_lang, licenses, order, rows, page} = ssrSoftwareParams(context.query) + const {search, keywords, prog_lang, licenses, sources, order, rows, page} = ssrSoftwareParams(context.query) // extract user settings from cookie const {rsd_page_layout, rsd_page_rows} = getUserSettings(context.req) // use url param if present else user settings @@ -264,6 +282,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { search, keywords, licenses, + sources, prog_lang, order: orderBy, limit: page_rows, @@ -284,13 +303,19 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { keywordsList, languagesList, licensesList, + sourcesList, + // extract remotes count from fn response + {count:remotesCount}, // extract highlights from fn response (we don't need count) {highlights} ] = await Promise.all([ getSoftwareList({url}), - softwareKeywordsFilter({search, keywords, prog_lang, licenses}), - softwareLanguagesFilter({search, keywords, prog_lang, licenses}), - softwareLicensesFilter({search, keywords, prog_lang, licenses}), + softwareKeywordsFilter({search, keywords, prog_lang, licenses, sources}), + softwareLanguagesFilter({search, keywords, prog_lang, licenses, sources}), + softwareLicensesFilter({search, keywords, prog_lang, licenses, sources}), + softwareSourcesFilter({search, keywords, prog_lang, licenses, sources}), + // get remotes count + getRemoteRsd({page:0, rows:1}), page !== 1 ? Promise.resolve({highlights: []}) : getSoftwareHighlights({ limit: settings.host?.software_highlights?.limit ?? 3, offset: 0 @@ -308,13 +333,16 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { languagesList, licenses, licensesList, + sources, + sourcesList, page, order: softwareOrder, rows: page_rows, layout: rsd_page_layout, count: software.count, software: software.data, - highlights + highlights, + hasRemotes: remotesCount > 0 }, } } diff --git a/frontend/utils/extractQueryParam.test.ts b/frontend/utils/extractQueryParam.test.ts index 098b49647..4e27a2682 100644 --- a/frontend/utils/extractQueryParam.test.ts +++ b/frontend/utils/extractQueryParam.test.ts @@ -85,6 +85,7 @@ it('extracts ssrSoftwareParams from url query', () => { 'keywords': '["BAM","FAIR Sofware"]', 'prog_lang': '["Python","C++"]', 'licenses': '["MIT","GPL-2.0-or-later"]', + 'sources': '["RSD 1","RSD 2"]', 'order': 'test-order', 'page': '0', 'rows': '12' @@ -94,6 +95,7 @@ it('extracts ssrSoftwareParams from url query', () => { keywords: ['BAM', 'FAIR Sofware'], prog_lang: ['Python', 'C++'], licenses: ['MIT', 'GPL-2.0-or-later'], + sources: ['RSD 1','RSD 2'], order: 'test-order', page: 0, rows: 12 diff --git a/frontend/utils/extractQueryParam.ts b/frontend/utils/extractQueryParam.ts index 9e11d5488..063ffafbc 100644 --- a/frontend/utils/extractQueryParam.ts +++ b/frontend/utils/extractQueryParam.ts @@ -140,6 +140,7 @@ export type SoftwareParams = { keywords?: string[], prog_lang?: string[], licenses?: string[], + sources?: string[], page?: number, rows?: number } @@ -187,6 +188,12 @@ export function ssrSoftwareParams(query: ParsedUrlQuery): SoftwareParams { castToType: 'json-encoded', defaultValue: null }) + const sources:string[]|undefined = decodeQueryParam({ + query, + param: 'sources', + castToType: 'json-encoded', + defaultValue: null + }) const order:string = decodeQueryParam({ query, @@ -202,6 +209,7 @@ export function ssrSoftwareParams(query: ParsedUrlQuery): SoftwareParams { keywords, prog_lang, licenses, + sources, order, rows, page, diff --git a/frontend/utils/postgrestUrl.ts b/frontend/utils/postgrestUrl.ts index 1171d5bee..af93b6105 100644 --- a/frontend/utils/postgrestUrl.ts +++ b/frontend/utils/postgrestUrl.ts @@ -33,6 +33,8 @@ type baseQueryStringProps = { domains?: string[] | null, prog_lang?: string[] | null, licenses?: string[] | null, + // rsd sources (remote RSD) + sources?: string[] | null, organisations?: string[] | null, order?: string, limit?: number, @@ -50,6 +52,7 @@ export type QueryParams={ domains?:string[] | null, prog_lang?: string[] | null, licenses?: string[] | null, + sources?: string[] | null, organisations?: string[] | null, project_status?: string | null, page?:number | null, @@ -89,8 +92,9 @@ export function ssrProjectsUrl(params: QueryParams) { export function buildFilterUrl(params: QueryParams, view:string) { const { search, order, keywords, domains, - licenses, prog_lang, organisations, - project_status, rows, page + licenses, prog_lang, sources, + organisations, project_status, + rows, page } = params // console.log('buildFilterUrl...params...', params) const url = `/${view}?` @@ -126,6 +130,12 @@ export function buildFilterUrl(params: QueryParams, view:string) { param: 'licenses', value: licenses }) + // sources (rsd remote source) + query = encodeUrlQuery({ + query, + param: 'sources', + value: sources + }) // organisations query = encodeUrlQuery({ query, @@ -183,15 +193,18 @@ export function paginationUrlParams({rows=12, page=0}: * Provides basic url query string for postgrest endpoints */ export function baseQueryString(props: baseQueryStringProps) { - const {keywords, + const { + keywords, domains, prog_lang, licenses, + sources, organisations, project_status, order, limit, - offset} = props + offset + } = props let query // console.group('baseQueryString') // console.log('keywords...', keywords) @@ -200,6 +213,7 @@ export function baseQueryString(props: baseQueryStringProps) { // console.log('order...', order) // console.log('limit...', limit) // console.log('offset...', offset) + // console.groupEnd() // filter on keywords using AND if (keywords !== undefined && keywords !== null && @@ -254,6 +268,21 @@ export function baseQueryString(props: baseQueryStringProps) { query = `licenses=cs.%7B${licensesAll}%7D` } } + if (sources !== undefined && + sources !== null && + Array.isArray(sources) === true + ){ + // sort and convert array to comma separated string + const sourcesAll = sources + .toSorted(localeSort) + .map((item: string) => `"${encodeURIComponent(item)}"`).join(',') + + if (query){ + query = `${query}&source=in.(${sourcesAll})` + }else{ + query = `source=in.(${sourcesAll})` + } + } if (organisations !== undefined && organisations !== null && typeof organisations === 'object') {